본문 바로가기
Artificial Intelligence/60. Python

[PYTHON] 비동기 코드에서 재시도(Retry) 로직을 우아하게 구현하는 3가지 방법과 에러 해결

by Papa Martino V 2026. 3. 18.
728x90

재시도(Retry) 로직
재시도(Retry) 로직

 

현대의 분산 시스템과 클라우드 네이티브 환경에서 네트워크 호출이나 외부 API 연동은 필수적입니다. 하지만 네트워크는 항상 안정적이지 않습니다. 일시적인 타임아웃, 서버 부하로 인한 503 에러, 혹은 쿼터 제한(Rate Limit) 등 '일시적 장애(Transient Fault)'는 언제든 발생할 수 있습니다. 이러한 상황에서 애플리케이션의 견고함을 결정짓는 것은 바로 우아한 재시도(Retry) 메커니즘입니다. 비동기 프로그래밍 환경인 asyncio에서 단순히 루프를 돌며 재시도하는 방식은 가독성을 해치고 유지보수를 어렵게 만듭니다. 본 가이드에서는 데코레이터 패턴과 전문 라이브러리를 활용하여 코드의 순수성을 유지하면서도 강력한 복구 능력을 갖추는 해결 방법을 제시합니다.


1. 왜 '우아한' 재시도가 필요한가?

단순한 while 문을 이용한 재시도는 다음과 같은 문제점을 야기합니다.

  • 지수 백오프(Exponential Backoff) 부재: 실패 즉시 재시도하면 타겟 서버에 더 큰 부하를 주어 장애를 심화시킵니다.
  • 지터(Jitter) 미적용: 여러 클라이언트가 동시에 재시도할 경우 발생하는 'Thundering Herd' 현상을 방어할 수 없습니다.
  • 코드 가독성 저하: 핵심 비즈니스 로직과 에러 핸들링 로직이 뒤섞여 유지보수가 힘들어집니다.

2. 재시도 전략별 핵심 차이와 특징

비동기 환경에서 선택할 수 있는 대표적인 재시도 구현 방식 3가지를 비교 분석합니다.

비교 항목 커스텀 비동기 데코레이터 Tenacity 라이브러리 Backoff 라이브러리
구현 난이도 중간 (직접 설계 필요) 낮음 (선언적 방식) 낮음 (데코레이터 위주)
유연성 매우 높음 (로직 완전 제어) 최상 (다양한 옵션 제공) 높음 (함수형 스타일)
지수 백오프 지원 직접 구현 내장 (Wait strategies) 내장
비동기 호환성 async/await 완벽 지원 async/await 완벽 지원 지원 가능
추천 상황 의존성을 줄여야 하는 경량 프로젝트 복잡한 재시도 조건이 필요한 서비스 단순하고 명확한 백오프가 필요할 때

3. 전문가의 해결 방법: Tenacity를 활용한 선언적 재시도

파이썬 생태계에서 비동기 재시도를 구현할 때 가장 권장되는 도구는 tenacity입니다. 이 라이브러리는 특정 예외 발생 시에만 재시도하거나, 최대 시도 횟수를 제한하고, 시도 사이의 간격을 지수적으로 늘리는 등의 복잡한 로직을 데코레이터 한 줄로 해결해 줍니다.

지터(Jitter)의 중요성

단순히 2초, 4초, 8초 간격으로 쉬는 것이 아니라, 여기에 무작위성(Randomness)을 부여하는 지터를 추가해야 합니다. 그래야 동시에 장애를 겪은 수많은 클라이언트가 한꺼번에 서버로 몰려드는 것을 방지할 수 있습니다.


4. 실전 Sample Example

다음은 tenacity를 사용하여 비동기 API 호출 시 지수 백오프와 지터를 적용하는 전문적인 예제입니다.


import asyncio
import random
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type

# 사용자 정의 예외
class TransientNetworkError(Exception):
    pass

# 우아한 재시도 로직 적용
# 1. 시도 횟수: 최대 5회
# 2. 대기 시간: 지수 백오프 적용 (2초부터 시작해 최대 10초까지)
# 3. 특정 예외(TransientNetworkError) 발생 시에만 재시도
@retry(
    stop=stop_after_attempt(5),
    wait=wait_exponential(multiplier=1, min=2, max=10),
    retry=retry_if_exception_type(TransientNetworkError),
    before_sleep=lambda retry_state: print(f"재시도 대기 중... 시도 횟수: {retry_state.attempt_number}")
)
async def fetch_api_data():
    print("API 호출 시도 중...")
    
    # 70% 확률로 실패를 가정하는 가상 로직
    if random.random() < 0.7:
        print("결과: 일시적 네트워크 에러 발생!")
        raise TransientNetworkError("서버 응답 없음")
    
    print("결과: 데이터 수신 성공!")
    return {"status": "success", "data": 200}

async def main():
    try:
        result = await fetch_api_data()
        print(f"최종 결과: {result}")
    except Exception as e:
        print(f"최종 실패: 모든 재시도가 소진되었습니다. ({e})")

if __name__ == "__main__":
    asyncio.run(main())

5. 운영 단계에서의 주의 사항

재시도 로직은 양날의 검입니다. 잘못 구현된 재시도는 자가 서비스 거부 공격(Self-DoS)으로 이어질 수 있습니다.

  • 최대 타임아웃 설정: 개별 재시도 간격뿐만 아니라 전체 프로세스가 대기하는 총합 시간(Total Timeout)을 반드시 설정하십시오.
  • 멱등성(Idempotency) 확인: 데이터를 수정하는 POST나 PATCH 요청의 경우, 동일한 요청이 여러 번 수행되어도 안전한지 반드시 확인해야 합니다.
  • 로깅과 모니터링: 재시도가 몇 번 발생하는지 로그를 남기고 알람을 설정하십시오. 빈번한 재시도는 시스템 하부 구조에 문제가 생겼다는 전조 현상일 수 있습니다.

6. 요약 및 결론

비동기 파이썬 코드에서 재시도 로직을 구현하는 가장 우아한 방법은 선언적 데코레이터를 활용하는 것입니다. 직접 while 문을 작성하기보다 검증된 라이브러리인 tenacity를 사용하여 지수 백오프와 지터를 적용하십시오. 이는 코드의 가독성을 높일 뿐만 아니라, 서버 부하를 최소화하면서 서비스 가용성을 극대화하는 가장 전문적인 해결 방법입니다.


내용 출처 및 참고 자료

  • Tenacity Documentation: "Retrying library for Python" (https://tenacity.readthedocs.io/)
  • Python official docs: "asyncio — Asynchronous I/O"
  • AWS Architecture Blog: "Exponential Backoff And Jitter"
  • Google Cloud Architecture Framework: "Design for Error Handling"
728x90