Python 비동기 처리에서 asyncio.gather 대신 TaskGroup 사용하기

문제 상황

여러 외부 API를 병렬로 호출하는 배치 작업을 운영 중이었다. asyncio.gather를 사용했는데, 일부 API가 실패해도 나머지는 계속 실행되어야 했다.

results = await asyncio.gather(
    fetch_api_a(),
    fetch_api_b(),
    fetch_api_c(),
    return_exceptions=True
)

return_exceptions=True로 처리했지만, 결과 리스트에서 예외와 정상 값을 구분하는 로직이 지저분했다.

TaskGroup 도입

Python 3.11에서 추가된 asyncio.TaskGroup을 사용해 구조를 개선했다.

async def process_apis():
    async with asyncio.TaskGroup() as tg:
        task_a = tg.create_task(fetch_api_a())
        task_b = tg.create_task(fetch_api_b())
        task_c = tg.create_task(fetch_api_c())
    
    return {
        'a': task_a.result(),
        'b': task_b.result(),
        'c': task_c.result()
    }

TaskGroup은 컨텍스트 매니저로 동작하며, 내부 태스크 중 하나라도 실패하면 다른 태스크를 취소하고 예외를 전파한다.

개별 예외 처리

실패한 API만 로깅하고 나머지는 진행해야 하는 경우, 각 태스크에서 예외를 잡았다.

async def safe_fetch(name, coro):
    try:
        return await coro
    except Exception as e:
        logger.error(f"{name} failed: {e}")
        return None

async with asyncio.TaskGroup() as tg:
    tasks = [
        tg.create_task(safe_fetch('api_a', fetch_api_a())),
        tg.create_task(safe_fetch('api_b', fetch_api_b())),
    ]

결과

  • 예외 처리 로직이 명확해졌다
  • 각 태스크의 결과를 변수로 바로 접근 가능
  • 구조화된 동시성 패턴 적용

Python 3.11 이상 환경이라면 TaskGroup 사용을 권장한다.