""" HTTP 요청 처리를 위한 유틸리티 모듈 """ import time import logging import requests from typing import Dict, Any, Optional, Union from src.utils.exceptions import APIError from src import config logger = config.setup_logger("http_client") class HTTPClient: """ HTTP 요청을 처리하는 클라이언트 클래스 재시도 로직, 에러 처리 등의 공통 기능을 제공합니다. """ def __init__( self, base_url: str = None, default_headers: Dict[str, str] = None, timeout: int = None, retry_count: int = None, retry_delay: float = None ): """ HTTP 클라이언트 초기화 Args: base_url: 기본 URL default_headers: 기본 HTTP 헤더 timeout: 요청 타임아웃 (초) retry_count: 재시도 횟수 retry_delay: 재시도 간 지연 시간 (초) """ self.base_url = base_url self.default_headers = default_headers or {} self.timeout = timeout or config.REQUEST_TIMEOUT self.retry_count = retry_count or config.API_RETRY_COUNT self.retry_delay = retry_delay or config.API_RETRY_DELAY def _make_url(self, endpoint: str) -> str: """ 엔드포인트로부터 전체 URL 생성 Args: endpoint: API 엔드포인트 경로 Returns: 완전한 URL 문자열 """ if self.base_url and not endpoint.startswith(('http://', 'https://')): # 상대 경로인 경우 기본 URL과 결합 if endpoint.startswith('/'): return f"{self.base_url}{endpoint}" return f"{self.base_url}/{endpoint}" return endpoint def _prepare_headers(self, headers: Dict[str, str] = None) -> Dict[str, str]: """ 요청 헤더 준비 Args: headers: 요청별 헤더 Returns: 병합된 헤더 딕셔너리 """ merged_headers = self.default_headers.copy() if headers: merged_headers.update(headers) return merged_headers def request( self, method: str, endpoint: str, params: Dict[str, Any] = None, data: Any = None, json: Any = None, headers: Dict[str, str] = None, timeout: int = None, retry_count: int = None, retry_delay: float = None, raise_for_status: bool = True ) -> requests.Response: """ HTTP 요청 수행 Args: method: HTTP 메서드 (GET, POST 등) endpoint: API 엔드포인트 params: URL 쿼리 파라미터 data: 요청 데이터 json: JSON 요청 데이터 headers: 요청 헤더 timeout: 요청 타임아웃 (초) retry_count: 재시도 횟수 retry_delay: 재시도 간 지연 시간 (초) raise_for_status: 응답 상태가 에러면 예외 발생 여부 Returns: HTTP 응답 객체 Raises: APIError: API 통신 오류 발생 시 """ url = self._make_url(endpoint) merged_headers = self._prepare_headers(headers) timeout_value = timeout or self.timeout retry_count_value = retry_count if retry_count is not None else self.retry_count retry_delay_value = retry_delay if retry_delay is not None else self.retry_delay # 요청 로깅 logger.debug( f"HTTP {method} 요청: {url}, 파라미터: {params}, " f"재시도: {retry_count_value}, 타임아웃: {timeout_value}" ) # 재시도 루프 for attempt in range(retry_count_value + 1): try: response = requests.request( method=method, url=url, params=params, data=data, json=json, headers=merged_headers, timeout=timeout_value ) # 응답 상태 확인 if raise_for_status: response.raise_for_status() return response except requests.RequestException as e: # 마지막 시도에서 실패하면 예외 발생 if attempt >= retry_count_value: status_code = getattr(e.response, 'status_code', None) if hasattr(e, 'response') else None response_data = None # 응답 데이터 추출 시도 if hasattr(e, 'response') and e.response: try: response_data = e.response.json() except ValueError: response_data = e.response.text error_msg = f"HTTP {method} 요청 실패 (URL: {url}): {str(e)}" logger.error(error_msg) raise APIError( message=error_msg, status_code=status_code, response=response_data ) from e # 재시도 전 대기 wait_time = retry_delay_value * (2 ** attempt) # 지수 백오프 logger.warning( f"HTTP {method} 요청 실패 (URL: {url}), " f"{attempt+1}/{retry_count_value+1} 번째 시도, " f"{wait_time}초 후 재시도: {str(e)}" ) time.sleep(wait_time) def get(self, endpoint, params=None, **kwargs): """GET 요청 수행""" return self.request("GET", endpoint, params=params, **kwargs) def post(self, endpoint, json=None, data=None, **kwargs): """POST 요청 수행""" return self.request("POST", endpoint, json=json, data=data, **kwargs) def put(self, endpoint, json=None, data=None, **kwargs): """PUT 요청 수행""" return self.request("PUT", endpoint, json=json, data=data, **kwargs) def delete(self, endpoint, **kwargs): """DELETE 요청 수행""" return self.request("DELETE", endpoint, **kwargs)