개요
Requests
라이브러리를 사용하여 데이터 수집 중 아래와 같은 에러 로그가 발생하였다.
Connection aborted.', RemoteDisconnected('Remote end closed connection without response')
이는 서버 측에서 연결을 갑자기 끊었을 때 발생하는 에러이다.
왜 요청이 멈추는 걸까?
위 에러는 일반적으로 다음과 같은 이유로 발생할 수 있다고 한다.
- 서버의 요청 제한: 많은 서버가 짧은 시간에 다수의 요청을 보내는 클라이언트를 차단하도록하는 로직이 구현되어 있는 경우에 너무 빠른 빈도로 요청을 보내게 되면 서버로부터 차단 당할 수 있다.
- HTTP 헤더 부족: 서버로부터 요청할 때 헤더가 부족하면, 특히
User-Agent
가 설정되지 않은 경우 일부 서버는 크롤링을 차단하려고 연결을 닫을 수 있다. - SSL 인증서 문제: HTTPS로 요청할 때 SSL 인증서 확인 과정에서 문제가 발생하면 연결이 강제로 종료될 수 있다.
- 이때에는
requests.get(url=url, verify=False)
와 같이 인증서를 무시할 수 있지만 안전한 방법은 아니다.
- 이때에는
- 서버의 시간 초과: 서버가 일정 시간 동안 응답을 받지 못하면 연결을 종료할 수 있다.
- 이때에는
requests.get(url=url, timeout=(3,5))
처럼 타임아웃 값을 짧게 설정하는 것도 방법이다.
- 이때에는
requests 모듈의 예외 처리와 재시도 로직
requests
모듈은 HTTP 요청 중 발생하는 다양한 예외를 제공한다.
이를 활용하여 오류 발생 시 재시도하는 로직을 구현해보자.
import requests
from requests.exceptions import RequestException
import time
def fetch_url(url, retries=3, delay=2):
attempt = 0
while attempt < retries:
try:
response = requests.get(url, timeout=10)
response.raise_for_status() # HTTP 오류가 있으면 예외 발생
return response
except RequestException as e:
attempt += 1
print(f"시도 {attempt}/{retries} 실패: {e}")
if attempt < retries:
time.sleep(delay) # 다음 시도 전 대기
else:
print("최대 재시도 횟수에 도달했습니다. 이 URL을 건너뜁니다.")
return None
# 사용 예시
url = "https://example.com"
response = fetch_url(url)
if response:
print("요청 성공!")
# 추가 작업 수행
else:
print("재시도 후에도 요청에 실패했습니다.")
코드 설명
함수 매개변수
url
: 요청할 웹 페이지의 URLretries
: 최대 재시도 횟수로, 기본값은 3회delay
: 재시도 전 대기 시간(초)으로, 기본값은 2초
예외 처리
try
블록: requests.get()을 사용하여 HTTP 요청을 보낸다.timeout=10
을 설정하여 10초 이상 응답이 없으면 Timeout 예외를 발생시키도록 하였다.
response.raise_for_status()
: 응답 코드가 4xx 또는 5xx인 경우 HTTPError 예외를 발생시킨다.except RequestException as e
: requests에서 발생할 수 있는 모든 예외를 포괄한다.
재시도 로직
- 요청이 실패할 때마다 attempt 변수를 증가시켜 현재 시도 횟수를 추적한다.
- 최대 재시도 횟수에 도달하지 않았으면 지정된 delay 시간만큼 대기한 후 다음 요청을 시도한다.
- 최대 횟수에 도달하면
None
을 반환하여 호출 측에서 후속 조치를 취할 수 있게 하였음.
추가 개선 사항
백오프 전략 적용
재시도 시마다 대기 시간을 늘려서 서버에 과부하를 주지 않도록 할 수도 있다.
def fetch_url(url, retries=3, delay=2, backoff=2):
attempt = 0
current_delay = delay
while attempt < retries:
try:
# ... (생략)
except RequestException as e:
# ... (생략)
time.sleep(current_delay)
current_delay *= backoff # 대기 시간 증가
특정 예외에 대한 처리
어떤 예외는 재시도해도 소용없을 수 있으므로, 예외 유형에 따라 다른 처리를 할 수도 있다.
from requests.exceptions import HTTPError, Timeout, ConnectionError
try:
# ... (생략)
except Timeout:
# 타임아웃 예외에 대한 재시도
except HTTPError as e:
if e.response.status_code == 404:
# 404 오류는 재시도하지 않음
return None
else:
# 다른 HTTP 오류는 재시도