공공데이터포털의 OpenAPI나 대외 웹 서비스를 대상으로 정기적인 데이터 수집 파이프라인(ETL)을 운영하다 보면, 초기 개발 단계에서는 정상 동작하던 스크립트가 대량 배치 실행 환경에서 403 Forbidden 또는 429 Too Many Requests 에러를 뿜으며 중단되는 현상을 흔히 겪습니다.
이는 대상 서버의 WAF(웹 애플리케이션 방화벽)나 리버스 프록시(Nginx, Cloudflare 등)의 Rate Limiting 모듈이 기계적인 요청 패턴을 비정상 트래픽으로 탐지하여 일시적 또는 영구적으로 IP를 차단하기 때문입니다. 이번 글에서는 단순한 time.sleep() 방식의 한계를 짚어보고, urllib3 기반의 지능형 연결 재시도, 커스텀 어댑터, 그리고 AWS 아키텍처 팀에서 표준으로 권장하는 풀 지터(Full Jitter) 지수 백오프 알고리즘을 적용한 실무 파이썬 수집 파이프라인 구축법을 정리합니다.
1. 단순 루프와 time.sleep()이 차단을 부르는 이유
초기 수집 스크립트에서 가장 흔히 사용하는 방식은 고정 지연 시간을 주는 루프입니다.
# 취약한 요청 패턴 예시
for url in url_list:
resp = requests.get(url) # 매 요청마다 신규 TCP/TLS 핸드셰이크 발생
time.sleep(1) # 정확히 1.0초 간격으로 발생하는 기계적 리듬
이 방식에는 두 가지 명확한 결함이 존재합니다:
- TCP 소켓 고갈과 오버헤드: 매 루프마다
requests.get()을 단독 호출하면 HTTP 연결이 매번 새로 수립(SYN-ACK)되고 종료(TIME_WAIT)되어, 대량 수집 시 로컬 포트 고갈(Ephemeral Port Exhaustion) 및 서버 측 부하를 가중시킵니다. - 규칙적인 시간 간격(Fixed Cadence) 탐지: WAF의 이상 탐지 알고리즘은 요청 간격의 분산(Variance)이 0에 가까운 주기적 요청을 봇(Bot) 트래픽의 주요 시그니처로 식별합니다.
2. requests.Session과 urllib3 Retry 어댑터 결합
안정적인 수집 파이프라인의 기본은 HTTP 커넥션 풀 재사용과 상태 코드별 선별적 재시도 전략입니다.
import requests
from requests.adapters import HTTPAdapter
from urllib3.util import Retry
def create_resilient_session(pool_connections=10, pool_maxsize=10):
session = requests.Session()
# 429(Too Many Requests), 500, 502, 503, 504에 대해 선별적 재시도
retry_strategy = Retry(
total=5,
backoff_factor=1.0,
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["HEAD", "GET", "OPTIONS"],
raise_on_status=False
)
adapter = HTTPAdapter(
max_retries=retry_strategy,
pool_connections=pool_connections,
pool_maxsize=pool_maxsize
)
session.mount("https://", adapter)
session.mount("http://", adapter)
return session
커넥션 풀을 활성화하면 동일 호스트에 대한 Keep-Alive가 유지되어 TLS 핸드셰이크 비용이 제거되고, 일시적인 네트워크 순단이나 서버 측 5xx 에러 발생 시 지정된 백오프 팩터에 따라 자동으로 복구됩니다.
3. 쏠림 현상을 방지하는 풀 지터(Full Jitter) 알고리즘
단순 지수 백오프(Exponential Backoff)는 재시도 대기 시간이 2초, 4초, 8초 등으로 기하급수적으로 증가하지만, 다수의 워커가 동시에 재시도를 수행할 경우 특정 시점에 요청이 집중되어 대상 서버에 충격을 주는 ‘우르르 떼(Thundering Herd)’ 문제를 유발합니다.
AWS Architecture Blog에서 발표한 연구에 따르면, 가장 효율적인 대기 전략은 계산된 지수 상한선 내에서 균등한 무작위 지연을 부여하는 풀 지터(Full Jitter) 방식입니다.
import random
import time
def full_jitter_sleep(base_delay: float, max_delay: float, attempt: int):
"""
Full Jitter 알고리즘:
대기 시간 t = Uniform(0, min(max_delay, base_delay * 2^attempt))
"""
calculated_backoff = min(max_delay, base_delay * (2 ** attempt))
sleep_duration = random.uniform(0, calculated_backoff)
time.sleep(sleep_duration)
return sleep_duration
이 방식을 적용하면 재시도 시점이 분산되어 대상 서버의 레이트 리미터를 초과하지 않으면서도 수집 성공률을 극대화할 수 있습니다.
4. 엔터프라이즈 배치 수집 모범 체크리스트
- 브라우저 프로필 헤더 구성: 기본 Python-requests User-Agent는 많은 WAF에서 즉시 차단 대상이 됩니다. 최신 브라우저의
User-Agent,Accept,Accept-Language헤더를 세션 단위로 일관되게 주입하십시오. - 세션 주기적 리프레시: 장시간 구동되는 데몬형 수집기는 메모리 누수 방지 및 쿠키 오염 방지를 위해 1,000~2,000회 요청 주기마다 세션을 재생성하는 구조가 안전합니다.
- Robots.txt 및 약관 준수: 자동화 수집 배치 설계 시 대상 사이트의
robots.txt내Crawl-delay지침을 준수하고, API가 공식 제공되는 데이터는 스크래핑보다 공식 OpenAPI를 우선 활용하십시오.