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