Python 비동기 프로그래밍에서 asyncio.gather vs asyncio.create_task

문제 상황

외부 API 10개를 동시에 호출하는 배치 작업에서 하나의 API가 실패하면 전체가 멈추는 문제가 발생했다. asyncio.gather를 사용하고 있었는데, 기본 동작이 예상과 달랐다.

# 기존 코드 - 하나만 실패해도 전체 중단
results = await asyncio.gather(
    fetch_api_1(),
    fetch_api_2(),
    fetch_api_3()
)

해결 과정

1. gather의 return_exceptions

첫 번째 시도는 return_exceptions=True 옵션이었다.

results = await asyncio.gather(
    fetch_api_1(),
    fetch_api_2(),
    fetch_api_3(),
    return_exceptions=True
)

# results에 정상 값과 Exception이 섞여서 반환됨
for i, result in enumerate(results):
    if isinstance(result, Exception):
        logger.error(f"API {i} failed: {result}")

이 방식은 간단하지만 개별 태스크의 라이프사이클 제어가 어려웠다.

2. create_task로 분리

더 세밀한 제어가 필요해서 태스크를 개별 생성하는 방식으로 변경했다.

tasks = [
    asyncio.create_task(fetch_api(url))
    for url in api_urls
]

results = []
for task in asyncio.as_completed(tasks):
    try:
        result = await task
        results.append(result)
    except Exception as e:
        logger.error(f"Failed: {e}")
        results.append(None)

차이점 정리

  • gather: 모든 태스크를 하나의 Future로 묶음. 취소 시 전체 취소.
  • create_task: 개별 태스크로 관리. 독립적인 라이프사이클.

실무에서는 대부분 gather로 충분하지만, 개별 재시도나 타임아웃이 필요한 경우 create_task가 유용했다.

추가: 타임아웃 적용

async def fetch_with_timeout(url, timeout=5):
    try:
        return await asyncio.wait_for(fetch_api(url), timeout)
    except asyncio.TimeoutError:
        logger.warning(f"Timeout: {url}")
        return None

Python 3.9부터는 asyncio.timeout() 컨텍스트 매니저도 고려할 만하다.

Python 비동기 프로그래밍에서 asyncio.gather vs asyncio.create_task