o2o-castad-backend/insta_plan.md

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로 에러 타입별 처리 가능