네트워크 오류는 API를 사용하는 애플리케이션에서 자주 발생할 수 있는 문제이다. 네트워크 오류는 서버와의 연결이 끊어지거나 시간 초과가 발생할 때 나타날 수 있으며, 이러한 오류가 발생할 경우 요청이 실패하게 된다. 따라서 네트워크 오류에 대비하여 재시도 로직을 구현하는 것이 중요하다. 이 섹션에서는 네트워크 오류를 감지하고, 적절히 처리하며, 재시도 로직을 구현하는 방법에 대해 다룬다.
네트워크 오류의 종류
네트워크 오류는 여러 가지 형태로 발생할 수 있다. 주요 네트워크 오류 유형은 다음과 같다.
- 연결 오류(Connection Error): 서버에 도달하지 못할 때 발생한다. 서버 다운타임, DNS 문제 또는 네트워크 단절 등이 원인일 수 있다.
- 시간 초과(Timeout Error): 요청이 서버에서 처리되기 전에 시간이 초과되었을 때 발생한다. 일반적으로 네트워크가 느리거나 서버의 부하가 높은 경우에 발생한다.
- 임시 네트워크 장애(Transient Network Failures): 네트워크 상태가 일시적으로 불안정할 때 발생하는 오류로, 잠시 후 다시 시도하면 정상적으로 요청이 처리될 수 있다.
Python에서 네트워크 오류 처리
Python에서 네트워크 오류를 처리하기 위해서는 requests
라이브러리와 같은 HTTP 클라이언트를 사용할 수 있다. 이 라이브러리는 ConnectionError
, Timeout
, HTTPError
등의 예외를 제공하여 네트워크 오류를 감지할 수 있게 한다.
import requests
from requests.exceptions import ConnectionError, Timeout, HTTPError
url = "https://api.openai.com/v1/chat/completions"
headers = {"Authorization": "Bearer YOUR_API_KEY"}
try:
response = requests.post(url, headers=headers, json={"prompt": "Hello, world!"})
response.raise_for_status() # HTTP 오류가 발생하면 예외가 발생한다.
except ConnectionError:
print("네트워크 연결 오류가 발생하였다.")
except Timeout:
print("요청 시간이 초과되었다.")
except HTTPError as http_err:
print(f"HTTP 오류가 발생하였다: {http_err}")
except Exception as err:
print(f"예상치 못한 오류가 발생하였다: {err}")
else:
print("요청이 성공적으로 처리되었다.")
이 코드에서는 네트워크 연결 오류, 시간 초과 오류 및 HTTP 오류를 감지하고 적절한 메시지를 출력한다.
재시도 로직의 필요성
네트워크 오류는 일시적인 경우가 많으므로, 단순히 오류를 처리하는 것만으로는 충분하지 않을 수 있다. 예를 들어, 요청이 실패했을 때 즉시 재시도하는 것이 나중에 다시 시도하는 것보다 더 효과적일 수 있다. 이 때, 재시도 로직을 추가하여 실패한 요청을 자동으로 재시도하게 할 수 있다.
재시도 로직을 구현할 때는 재시도 횟수와 대기 시간을 설정하는 것이 중요하다. 너무 자주 재시도하면 서버에 과부하가 걸릴 수 있으며, 너무 적게 재시도하면 오류가 해결되지 않을 수 있다.
재시도 로직 구현
재시도 로직을 구현하기 위해서는 일정한 간격으로 재시도하거나, 지수 백오프(exponential backoff)를 사용할 수 있다. 지수 백오프는 처음에는 짧은 간격으로 재시도하다가, 실패할 때마다 점점 더 긴 간격으로 재시도하는 방식이다.
import time
import requests
from requests.exceptions import ConnectionError, Timeout
url = "https://api.openai.com/v1/chat/completions"
headers = {"Authorization": "Bearer YOUR_API_KEY"}
def make_request_with_retries(url, headers, retries=5, backoff_factor=0.5):
for i in range(retries):
try:
response = requests.post(url, headers=headers, json={"prompt": "Hello, world!"})
response.raise_for_status()
return response.json()
except (ConnectionError, Timeout) as err:
if i < retries - 1:
sleep_time = backoff_factor * (2 ** i)
print(f"오류 발생: {err}. {sleep_time}초 후 재시도한다.")
time.sleep(sleep_time)
else:
raise
except Exception as err:
raise
try:
result = make_request_with_retries(url, headers)
print("요청 성공:", result)
except Exception as final_err:
print(f"모든 재시도가 실패하였다: {final_err}")
이 예제에서는 make_request_with_retries
함수가 재시도 로직을 처리한다. 이 함수는 retries
매개변수로 최대 재시도 횟수를 받고, backoff_factor
매개변수로 지수 백오프의 초기 대기 시간을 설정한다. 요청이 실패할 때마다 대기 시간이 두 배로 증가한다.
재시도 로직의 최적화
재시도 로직을 구현할 때, 단순히 재시도하는 것만이 최적의 해결책은 아니다. 재시도 간격, 최대 재시도 횟수, 백오프 전략 등을 최적화하여 성능과 안정성을 동시에 확보할 수 있다.
재시도 간격 및 백오프 전략
앞서 설명한 지수 백오프 전략은 네트워크 오류를 처리하는 데 매우 효과적이다. 이 전략에서는 첫 번째 재시도에서 짧은 간격을 두고, 각 재시도 후에는 두 배씩 증가하는 간격으로 대기 시간을 늘려간다. 이러한 방식은 서버나 네트워크가 일시적으로 과부하 상태일 때, 시스템에 부하를 더 주지 않으면서 문제를 해결할 시간을 제공한다.
예를 들어, 백오프 인자를 \alpha라고 하고, 재시도 횟수를 n이라고 하면, n번째 재시도 전에 대기할 시간 t_n은 다음과 같이 계산할 수 있다.
이 식에 따라, 재시도 횟수가 증가할수록 대기 시간이 지수적으로 증가한다. 예를 들어, \alpha = 0.5이고 첫 번째 재시도라면 0.5초를 대기한 후 다시 시도한다. 두 번째 재시도는 1초, 세 번째 재시도는 2초, 네 번째 재시도는 4초를 대기한 후 재시도하게 된다.
최대 재시도 횟수 설정
최대 재시도 횟수는 시스템의 요구 사항과 네트워크 안정성에 따라 달라질 수 있다. 일반적으로 너무 많은 재시도는 성능 저하를 초래할 수 있으므로, 3~5회 정도의 재시도를 설정하는 것이 적절한다. 재시도가 반복되어도 문제가 해결되지 않으면, 네트워크 상태가 장기적으로 불안정하거나 API 서버에 심각한 문제가 발생했을 가능성이 크다. 이 경우에는 사용자가 직접 문제를 확인할 수 있도록 오류를 반환하고 재시도를 중단하는 것이 좋다.
특정 오류에 대한 재시도
모든 네트워크 오류에 대해 재시도를 시도하는 것이 적절하지 않을 수 있다. 예를 들어, 404 Not Found
와 같은 오류는 재시도를 통해 해결될 수 없는 오류이므로, 이러한 경우에는 즉시 실패로 처리해야 한다. 반면, 503 Service Unavailable
와 같은 오류는 서버 과부하 상태일 수 있으므로, 재시도 대상에 포함될 수 있다.
이를 처리하기 위해, 재시도할 오류 코드와 하지 않을 오류 코드를 구분하여 관리할 수 있다. 이를 위해 HTTP 상태 코드와 예외 처리를 함께 활용할 수 있다.
def make_request_with_custom_retries(url, headers, retries=5, backoff_factor=0.5, retry_on_statuses=[503]):
for i in range(retries):
try:
response = requests.post(url, headers=headers, json={"prompt": "Hello, world!"})
if response.status_code in retry_on_statuses:
raise requests.exceptions.HTTPError(f"Status code: {response.status_code}")
response.raise_for_status()
return response.json()
except (ConnectionError, Timeout) as err:
if i < retries - 1:
sleep_time = backoff_factor * (2 ** i)
print(f"네트워크 오류 발생: {err}. {sleep_time}초 후 재시도한다.")
time.sleep(sleep_time)
else:
raise
except requests.exceptions.HTTPError as http_err:
if i < retries - 1 and response.status_code in retry_on_statuses:
sleep_time = backoff_factor * (2 ** i)
print(f"서버 오류 발생: {http_err}. {sleep_time}초 후 재시도한다.")
time.sleep(sleep_time)
else:
raise
except Exception as err:
raise
try:
result = make_request_with_custom_retries(url, headers)
print("요청 성공:", result)
except Exception as final_err:
print(f"모든 재시도가 실패하였다: {final_err}")
이 코드에서는 retry_on_statuses
리스트에 포함된 HTTP 상태 코드에 대해 재시도를 시도한다. 기본적으로는 503
오류가 발생할 때만 재시도하도록 설정되어 있다.
재시도 로직 테스트
재시도 로직을 제대로 구현했다면, 다양한 네트워크 오류 상황에서 이 로직이 잘 동작하는지 테스트해야 한다. 이를 위해 다음과 같은 시나리오를 고려할 수 있다.
- 인위적인 네트워크 오류 발생: 테스트 환경에서 일부러 네트워크 오류를 발생시켜 재시도 로직이 예상대로 작동하는지 확인한다.
- 서버 응답 시뮬레이션: 다양한 서버 응답 코드(예: 503, 500, 404 등)를 시뮬레이션하여 재시도 로직이 올바르게 작동하는지 검증한다.
- 성능 테스트: 재시도 로직이 실제 환경에서 어떻게 동작하는지, 시스템에 미치는 영향을 평가하기 위해 성능 테스트를 진행한다.
이러한 테스트를 통해 재시도 로직이 예상치 못한 상황에서도 안정적으로 작동하는지 확인할 수 있다.