Loading...
Loading...
02-reusable-code-python/httpx_utils/requests_client.py
"""
requests 기반 재시도 + 레이트리밋 + 세션 관리 HTTP 클라이언트
@source 260321-lega-tech
@extracted 2026-03-22
@version 1.0.0
의존성:
- requests (필수)
- urllib3 (필수, requests 의존성으로 자동 설치)
사용법:
from httpx_utils.requests_client import HTTPClient, get_client
# 개별 인스턴스
client = HTTPClient(timeout=30, retries=3, rate_limit=1.0)
resp = client.get("https://example.com")
# 싱글톤 (전역 공유)
client = get_client(timeout=30)
resp = client.get("https://example.com")
"""
import logging
import time
from typing import Dict, Optional
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
logger = logging.getLogger(__name__)
# 기본 브라우저 헤더
DEFAULT_HEADERS = {
'User-Agent': (
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
'AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/120.0.0.0 Safari/537.36'
),
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7',
'Accept-Encoding': 'gzip, deflate, br',
}
class HTTPClient:
"""
재시도 + 레이트리밋 + 세션 관리 HTTP 클라이언트
Args:
timeout: 요청 타임아웃 (초, 기본 30)
retries: 최대 재시도 횟수 (기본 3)
rate_limit: 요청 간격 (초, 기본 1.0)
"""
def __init__(self, timeout: int = 30, retries: int = 3, rate_limit: float = 1.0):
self.timeout = timeout
self.rate_limit = rate_limit
self.last_request_time = 0.0
# requests 세션 설정
self.session = requests.Session()
self.session.headers.update(DEFAULT_HEADERS)
# 재시도 전략 (429, 5xx 자동 재시도, 지수 백오프)
retry_strategy = Retry(
total=retries,
backoff_factor=1, # 1s, 2s, 4s, ...
status_forcelist=[429, 500, 502, 503, 504],
)
adapter = HTTPAdapter(max_retries=retry_strategy)
self.session.mount("http://", adapter)
self.session.mount("https://", adapter)
def get(self, url: str, **kwargs) -> Optional[requests.Response]:
"""
GET 요청 (레이트리밋 적용, 실패 시 None 반환)
Args:
url: 요청 URL
**kwargs: requests.Session.get에 전달할 추가 파라미터
Returns:
Response 객체 또는 None (실패)
"""
self._wait_rate_limit()
try:
resp = self.session.get(url, timeout=self.timeout, **kwargs)
resp.raise_for_status()
return resp
except requests.RequestException as e:
logger.error(f"GET 요청 실패: {url} - {e}")
return None
def post(self, url: str, **kwargs) -> Optional[requests.Response]:
"""
POST 요청 (레이트리밋 적용, 실패 시 None 반환)
Args:
url: 요청 URL
**kwargs: requests.Session.post에 전달할 추가 파라미터
Returns:
Response 객체 또는 None (실패)
"""
self._wait_rate_limit()
try:
resp = self.session.post(url, timeout=self.timeout, **kwargs)
resp.raise_for_status()
return resp
except requests.RequestException as e:
logger.error(f"POST 요청 실패: {url} - {e}")
return None
def set_headers(self, headers: Dict[str, str]) -> None:
"""추가 헤더 설정 (기존 헤더에 병합)"""
self.session.headers.update(headers)
def set_cookies(self, cookies: Dict[str, str]) -> None:
"""쿠키 설정"""
self.session.cookies.update(cookies)
def close(self) -> None:
"""세션 종료"""
self.session.close()
def _wait_rate_limit(self) -> None:
"""요청 간격 유지 (레이트리밋)"""
elapsed = time.time() - self.last_request_time
if elapsed < self.rate_limit:
time.sleep(self.rate_limit - elapsed)
self.last_request_time = time.time()
# ──────────────────────────────────────────────────────
# 싱글톤 인터페이스
# ──────────────────────────────────────────────────────
_default_client: Optional[HTTPClient] = None
def get_client(**kwargs) -> HTTPClient:
"""
전역 싱글톤 HTTP 클라이언트 반환
Args:
**kwargs: HTTPClient 생성자 파라미터 (최초 호출 시만 적용)
Returns:
HTTPClient 인스턴스
"""
global _default_client
if _default_client is None:
_default_client = HTTPClient(**kwargs)
return _default_client
def reset_client() -> None:
"""싱글톤 클라이언트 초기화 (테스트용)"""
global _default_client
if _default_client:
_default_client.close()
_default_client = None