559 lines
14 KiB
Markdown
559 lines
14 KiB
Markdown
# Instagram POC 예외 처리 단순화 작업 계획서
|
|
|
|
## 개요
|
|
|
|
`poc/instagram/exceptions.py` 파일을 삭제하고, `client.py` 상단에 **ErrorState Enum과 에러 처리 유틸리티**를 정의하여 일관된 에러 처리 구조를 구현합니다.
|
|
|
|
---
|
|
|
|
## 최종 파일 구조
|
|
|
|
```
|
|
poc/instagram/
|
|
├── client.py # ErrorState + parse_instagram_error + InstagramClient
|
|
├── models.py
|
|
├── __init__.py # client.py에서 ErrorState, parse_instagram_error export
|
|
└── (exceptions.py 삭제)
|
|
```
|
|
|
|
---
|
|
|
|
## 작업 계획
|
|
|
|
### 1단계: client.py 상단에 에러 처리 코드 추가
|
|
|
|
**파일**: `poc/instagram/client.py`
|
|
|
|
**위치**: import 문 다음, InstagramClient 클래스 이전
|
|
|
|
**추가할 코드**:
|
|
```python
|
|
import re
|
|
from enum import Enum
|
|
|
|
# ============================================================
|
|
# Error State & Parser
|
|
# ============================================================
|
|
|
|
class ErrorState(str, Enum):
|
|
"""Instagram API 에러 상태"""
|
|
RATE_LIMIT = "rate_limit"
|
|
AUTH_ERROR = "auth_error"
|
|
CONTAINER_TIMEOUT = "container_timeout"
|
|
CONTAINER_ERROR = "container_error"
|
|
API_ERROR = "api_error"
|
|
UNKNOWN = "unknown"
|
|
|
|
|
|
def parse_instagram_error(e: Exception) -> tuple[ErrorState, str, dict]:
|
|
"""
|
|
Instagram 예외를 파싱하여 상태, 메시지, 추가 정보를 반환
|
|
|
|
Args:
|
|
e: 발생한 예외
|
|
|
|
Returns:
|
|
tuple: (error_state, message, extra_info)
|
|
|
|
Example:
|
|
>>> error_state, message, extra_info = parse_instagram_error(e)
|
|
>>> if error_state == ErrorState.RATE_LIMIT:
|
|
... retry_after = extra_info.get("retry_after", 60)
|
|
"""
|
|
error_str = str(e)
|
|
extra_info = {}
|
|
|
|
# Rate Limit 에러
|
|
if "[RateLimit]" in error_str:
|
|
match = re.search(r"retry_after=(\d+)s", error_str)
|
|
if match:
|
|
extra_info["retry_after"] = int(match.group(1))
|
|
return ErrorState.RATE_LIMIT, "API 호출 제한 초과", extra_info
|
|
|
|
# 인증 에러 (code=190)
|
|
if "code=190" in error_str:
|
|
return ErrorState.AUTH_ERROR, "인증 실패 (토큰 만료 또는 무효)", extra_info
|
|
|
|
# 컨테이너 타임아웃
|
|
if "[ContainerTimeout]" in error_str:
|
|
match = re.search(r"\((\d+)초 초과\)", error_str)
|
|
if match:
|
|
extra_info["timeout"] = int(match.group(1))
|
|
return ErrorState.CONTAINER_TIMEOUT, "미디어 처리 시간 초과", extra_info
|
|
|
|
# 컨테이너 상태 에러
|
|
if "[ContainerStatus]" in error_str:
|
|
match = re.search(r"처리 실패: (\w+)", error_str)
|
|
if match:
|
|
extra_info["status"] = match.group(1)
|
|
return ErrorState.CONTAINER_ERROR, "미디어 컨테이너 처리 실패", extra_info
|
|
|
|
# Instagram API 에러
|
|
if "[InstagramAPI]" in error_str:
|
|
match = re.search(r"code=(\d+)", error_str)
|
|
if match:
|
|
extra_info["code"] = int(match.group(1))
|
|
return ErrorState.API_ERROR, "Instagram API 오류", extra_info
|
|
|
|
return ErrorState.UNKNOWN, str(e), extra_info
|
|
```
|
|
|
|
---
|
|
|
|
### 2단계: client.py import 문 수정
|
|
|
|
**파일**: `poc/instagram/client.py`
|
|
|
|
**변경 전** (line 24-30):
|
|
```python
|
|
from .exceptions import (
|
|
ContainerStatusError,
|
|
ContainerTimeoutError,
|
|
InstagramAPIError,
|
|
RateLimitError,
|
|
create_exception_from_error,
|
|
)
|
|
```
|
|
|
|
**변경 후**:
|
|
```python
|
|
# (삭제 - ErrorState와 parse_instagram_error를 직접 정의)
|
|
```
|
|
|
|
**import 추가**:
|
|
```python
|
|
import re
|
|
from enum import Enum
|
|
```
|
|
|
|
---
|
|
|
|
### 3단계: 예외 발생 코드 수정
|
|
|
|
#### 3-1. Rate Limit 에러 (line 159-162)
|
|
|
|
**변경 전**:
|
|
```python
|
|
raise RateLimitError(
|
|
message="Rate limit 초과 (최대 재시도 횟수 도달)",
|
|
retry_after=retry_after,
|
|
)
|
|
```
|
|
|
|
**변경 후**:
|
|
```python
|
|
raise Exception(f"[RateLimit] Rate limit 초과 (최대 재시도 횟수 도달) | retry_after={retry_after}s")
|
|
```
|
|
|
|
---
|
|
|
|
#### 3-2. API 에러 응답 (line 177-186)
|
|
|
|
**변경 전**:
|
|
```python
|
|
if "error" in response_data:
|
|
error_response = ErrorResponse.model_validate(response_data)
|
|
err = error_response.error
|
|
logger.error(f"[API Error] code={err.code}, message={err.message}")
|
|
raise create_exception_from_error(
|
|
message=err.message,
|
|
code=err.code,
|
|
subcode=err.error_subcode,
|
|
fbtrace_id=err.fbtrace_id,
|
|
)
|
|
```
|
|
|
|
**변경 후**:
|
|
```python
|
|
if "error" in response_data:
|
|
error_response = ErrorResponse.model_validate(response_data)
|
|
err = error_response.error
|
|
logger.error(f"[API Error] code={err.code}, message={err.message}")
|
|
error_msg = f"[InstagramAPI] {err.message} | code={err.code}"
|
|
if err.error_subcode:
|
|
error_msg += f" | subcode={err.error_subcode}"
|
|
if err.fbtrace_id:
|
|
error_msg += f" | fbtrace_id={err.fbtrace_id}"
|
|
raise Exception(error_msg)
|
|
```
|
|
|
|
---
|
|
|
|
#### 3-3. 예외 재발생 (line 190-191)
|
|
|
|
**변경 전**:
|
|
```python
|
|
except InstagramAPIError:
|
|
raise
|
|
```
|
|
|
|
**변경 후**:
|
|
```python
|
|
except Exception:
|
|
raise
|
|
```
|
|
|
|
---
|
|
|
|
#### 3-4. 최대 재시도 초과 (line 201)
|
|
|
|
**변경 전**:
|
|
```python
|
|
raise last_exception or InstagramAPIError("최대 재시도 횟수 초과")
|
|
```
|
|
|
|
**변경 후**:
|
|
```python
|
|
raise last_exception or Exception("[InstagramAPI] 최대 재시도 횟수 초과")
|
|
```
|
|
|
|
---
|
|
|
|
#### 3-5. 컨테이너 타임아웃 (line 217-218)
|
|
|
|
**변경 전**:
|
|
```python
|
|
raise ContainerTimeoutError(
|
|
f"컨테이너 처리 타임아웃 ({timeout}초 초과): {container_id}"
|
|
)
|
|
```
|
|
|
|
**변경 후**:
|
|
```python
|
|
raise Exception(f"[ContainerTimeout] 컨테이너 처리 타임아웃 ({timeout}초 초과): {container_id}")
|
|
```
|
|
|
|
---
|
|
|
|
#### 3-6. 컨테이너 상태 에러 (line 235)
|
|
|
|
**변경 전**:
|
|
```python
|
|
raise ContainerStatusError(f"컨테이너 처리 실패: {container.status}")
|
|
```
|
|
|
|
**변경 후**:
|
|
```python
|
|
raise Exception(f"[ContainerStatus] 컨테이너 처리 실패: {container.status}")
|
|
```
|
|
|
|
---
|
|
|
|
### 4단계: __init__.py 수정
|
|
|
|
**파일**: `poc/instagram/__init__.py`
|
|
|
|
**변경 전** (line 18-25):
|
|
```python
|
|
from poc.instagram.client import InstagramClient
|
|
from poc.instagram.exceptions import (
|
|
InstagramAPIError,
|
|
AuthenticationError,
|
|
RateLimitError,
|
|
ContainerStatusError,
|
|
ContainerTimeoutError,
|
|
)
|
|
```
|
|
|
|
**변경 후**:
|
|
```python
|
|
from poc.instagram.client import (
|
|
InstagramClient,
|
|
ErrorState,
|
|
parse_instagram_error,
|
|
)
|
|
```
|
|
|
|
**__all__ 수정**:
|
|
```python
|
|
__all__ = [
|
|
# Client
|
|
"InstagramClient",
|
|
# Error handling
|
|
"ErrorState",
|
|
"parse_instagram_error",
|
|
# Models
|
|
"Media",
|
|
"MediaList",
|
|
"MediaContainer",
|
|
"APIError",
|
|
"ErrorResponse",
|
|
]
|
|
```
|
|
|
|
---
|
|
|
|
### 5단계: main.py 수정
|
|
|
|
**파일**: `poc/instagram/main.py`
|
|
|
|
**변경 전** (line 13):
|
|
```python
|
|
from poc.instagram.exceptions import InstagramAPIError
|
|
```
|
|
|
|
**변경 후**:
|
|
```python
|
|
from poc.instagram import ErrorState, parse_instagram_error
|
|
```
|
|
|
|
**예외 처리 수정**:
|
|
```python
|
|
# 변경 전
|
|
except InstagramAPIError as e:
|
|
logger.error(f"API 에러: {e}")
|
|
|
|
# 변경 후
|
|
except Exception as e:
|
|
error_state, message, extra_info = parse_instagram_error(e)
|
|
|
|
if error_state == ErrorState.RATE_LIMIT:
|
|
retry_after = extra_info.get("retry_after", 60)
|
|
logger.error(f"Rate Limit: {message} (재시도: {retry_after}초)")
|
|
elif error_state == ErrorState.AUTH_ERROR:
|
|
logger.error(f"인증 에러: {message}")
|
|
elif error_state == ErrorState.CONTAINER_TIMEOUT:
|
|
logger.error(f"타임아웃: {message}")
|
|
elif error_state == ErrorState.CONTAINER_ERROR:
|
|
status = extra_info.get("status", "UNKNOWN")
|
|
logger.error(f"컨테이너 에러: {message} (상태: {status})")
|
|
else:
|
|
logger.error(f"API 에러: {message}")
|
|
```
|
|
|
|
---
|
|
|
|
### 6단계: main_ori.py 수정
|
|
|
|
**파일**: `poc/instagram/main_ori.py`
|
|
|
|
**변경 전** (line 271-274):
|
|
```python
|
|
from poc.instagram.exceptions import (
|
|
AuthenticationError,
|
|
InstagramAPIError,
|
|
RateLimitError,
|
|
)
|
|
```
|
|
|
|
**변경 후**:
|
|
```python
|
|
from poc.instagram import ErrorState, parse_instagram_error
|
|
```
|
|
|
|
**예외 처리 수정** (line 289-298):
|
|
```python
|
|
# 변경 전
|
|
except AuthenticationError as e:
|
|
print(f"[성공] AuthenticationError 발생: {e}")
|
|
except RateLimitError as e:
|
|
print(f"[성공] RateLimitError 발생: {e}")
|
|
except InstagramAPIError as e:
|
|
print(f"[성공] InstagramAPIError 발생: {e}")
|
|
|
|
# 변경 후
|
|
except Exception as e:
|
|
error_state, message, extra_info = parse_instagram_error(e)
|
|
|
|
match error_state:
|
|
case ErrorState.RATE_LIMIT:
|
|
print(f"[성공] Rate Limit 에러: {message}")
|
|
case ErrorState.AUTH_ERROR:
|
|
print(f"[성공] 인증 에러: {message}")
|
|
case ErrorState.CONTAINER_TIMEOUT:
|
|
print(f"[성공] 타임아웃 에러: {message}")
|
|
case ErrorState.CONTAINER_ERROR:
|
|
print(f"[성공] 컨테이너 에러: {message}")
|
|
case _:
|
|
print(f"[성공] API 에러: {message}")
|
|
```
|
|
|
|
---
|
|
|
|
### 7단계: exceptions.py 삭제
|
|
|
|
**파일**: `poc/instagram/exceptions.py`
|
|
|
|
**작업**: 파일 삭제
|
|
|
|
---
|
|
|
|
## 최종 client.py 구조
|
|
|
|
```python
|
|
"""
|
|
Instagram Graph API Client
|
|
"""
|
|
|
|
import asyncio
|
|
import logging
|
|
import re
|
|
import time
|
|
from enum import Enum
|
|
from typing import Any, Optional
|
|
|
|
import httpx
|
|
|
|
from .models import ErrorResponse, Media, MediaContainer
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# ============================================================
|
|
# Error State & Parser
|
|
# ============================================================
|
|
|
|
class ErrorState(str, Enum):
|
|
"""Instagram API 에러 상태"""
|
|
RATE_LIMIT = "rate_limit"
|
|
AUTH_ERROR = "auth_error"
|
|
CONTAINER_TIMEOUT = "container_timeout"
|
|
CONTAINER_ERROR = "container_error"
|
|
API_ERROR = "api_error"
|
|
UNKNOWN = "unknown"
|
|
|
|
|
|
def parse_instagram_error(e: Exception) -> tuple[ErrorState, str, dict]:
|
|
"""Instagram 예외를 파싱하여 상태, 메시지, 추가 정보를 반환"""
|
|
# ... (구현부)
|
|
|
|
|
|
# ============================================================
|
|
# Instagram Client
|
|
# ============================================================
|
|
|
|
class InstagramClient:
|
|
"""Instagram Graph API 비동기 클라이언트"""
|
|
# ... (기존 코드)
|
|
```
|
|
|
|
---
|
|
|
|
## 에러 메시지 형식
|
|
|
|
| 에러 유형 | 메시지 prefix | ErrorState | 예시 |
|
|
|----------|--------------|------------|------|
|
|
| Rate Limit | `[RateLimit]` | `RATE_LIMIT` | `[RateLimit] Rate limit 초과 \| retry_after=60s` |
|
|
| 인증 에러 | `[InstagramAPI]` + code=190 | `AUTH_ERROR` | `[InstagramAPI] Invalid token \| code=190` |
|
|
| API 에러 | `[InstagramAPI]` | `API_ERROR` | `[InstagramAPI] Error \| code=100` |
|
|
| 컨테이너 타임아웃 | `[ContainerTimeout]` | `CONTAINER_TIMEOUT` | `[ContainerTimeout] 타임아웃 (300초 초과)` |
|
|
| 컨테이너 에러 | `[ContainerStatus]` | `CONTAINER_ERROR` | `[ContainerStatus] 처리 실패: ERROR` |
|
|
|
|
---
|
|
|
|
## 작업 체크리스트
|
|
|
|
- [ ] 1단계: client.py 상단에 ErrorState Enum 및 parse_instagram_error 추가
|
|
- [ ] 2단계: client.py import 문 수정 (re, Enum 추가, exceptions import 삭제)
|
|
- [ ] 3단계: client.py 예외 발생 코드 6곳 수정
|
|
- [ ] line 159-162: RateLimitError → Exception
|
|
- [ ] line 177-186: create_exception_from_error → Exception
|
|
- [ ] line 190-191: InstagramAPIError → Exception
|
|
- [ ] line 201: InstagramAPIError → Exception
|
|
- [ ] line 217-218: ContainerTimeoutError → Exception
|
|
- [ ] line 235: ContainerStatusError → Exception
|
|
- [ ] 4단계: __init__.py 수정 (ErrorState, parse_instagram_error export)
|
|
- [ ] 5단계: main.py 수정 (ErrorState 활용)
|
|
- [ ] 6단계: main_ori.py 수정 (ErrorState 활용)
|
|
- [ ] 7단계: exceptions.py 파일 삭제
|
|
|
|
---
|
|
|
|
## 사용 예시
|
|
|
|
### 기본 사용법
|
|
```python
|
|
from poc.instagram import InstagramClient, ErrorState, parse_instagram_error
|
|
|
|
async def publish_video(video_url: str, caption: str):
|
|
async with InstagramClient(access_token="TOKEN") as client:
|
|
try:
|
|
media = await client.publish_video(video_url=video_url, caption=caption)
|
|
return {"success": True, "state": "completed", "data": media}
|
|
|
|
except Exception as e:
|
|
error_state, message, extra_info = parse_instagram_error(e)
|
|
return {
|
|
"success": False,
|
|
"state": error_state.value,
|
|
"message": message,
|
|
**extra_info
|
|
}
|
|
```
|
|
|
|
### match-case 활용 (Python 3.10+)
|
|
```python
|
|
except Exception as e:
|
|
error_state, message, extra_info = parse_instagram_error(e)
|
|
|
|
match error_state:
|
|
case ErrorState.RATE_LIMIT:
|
|
retry_after = extra_info.get("retry_after", 60)
|
|
await asyncio.sleep(retry_after)
|
|
# 재시도 로직...
|
|
|
|
case ErrorState.AUTH_ERROR:
|
|
# 토큰 갱신 로직...
|
|
|
|
case ErrorState.CONTAINER_TIMEOUT:
|
|
# 재시도 또는 알림...
|
|
|
|
case ErrorState.CONTAINER_ERROR:
|
|
# 실패 처리...
|
|
|
|
case _:
|
|
# 기본 에러 처리...
|
|
```
|
|
|
|
### 응답 예시
|
|
```python
|
|
# Rate Limit 에러
|
|
{
|
|
"success": False,
|
|
"state": "rate_limit",
|
|
"message": "API 호출 제한 초과",
|
|
"retry_after": 60
|
|
}
|
|
|
|
# 인증 에러
|
|
{
|
|
"success": False,
|
|
"state": "auth_error",
|
|
"message": "인증 실패 (토큰 만료 또는 무효)"
|
|
}
|
|
|
|
# 컨테이너 타임아웃
|
|
{
|
|
"success": False,
|
|
"state": "container_timeout",
|
|
"message": "미디어 처리 시간 초과",
|
|
"timeout": 300
|
|
}
|
|
|
|
# 컨테이너 에러
|
|
{
|
|
"success": False,
|
|
"state": "container_error",
|
|
"message": "미디어 컨테이너 처리 실패",
|
|
"status": "ERROR"
|
|
}
|
|
|
|
# API 에러
|
|
{
|
|
"success": False,
|
|
"state": "api_error",
|
|
"message": "Instagram API 오류",
|
|
"code": 100
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 장점
|
|
|
|
1. **단일 파일 관리**: client.py 하나에서 클라이언트와 에러 처리 모두 관리
|
|
2. **일관된 에러 형식**: ErrorState Enum으로 타입 안전한 에러 구분
|
|
3. **IDE 지원**: 자동완성, 타입 힌트 지원
|
|
4. **파싱 유틸리티**: parse_instagram_error로 에러 메시지에서 정보 추출
|
|
5. **유연한 처리**: match-case 또는 if-elif로 에러 타입별 처리 가능
|