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

[PYTHON] 비동기 처리 효율을 높이는 asyncio.gather, wait, as_completed 3가지 핵심 차이와 해결 방법

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

asyncio.gather, wait, as_completed
asyncio.gather, wait, as_completed

 

파이썬의 asyncio 라이브러리는 현대적인 고성능 네트워크 애플리케이션과 데이터 처리 시스템을 구축하는 데 있어 필수적인 도구입니다. 하지만 단순히 await를 사용하는 수준을 넘어, 여러 개의 코루틴(Coroutine)을 동시에 관리해야 할 때 개발자들은 선택의 기로에 서게 됩니다. 바로 asyncio.gather, asyncio.wait, 그리고 asyncio.as_completed 중 어떤 것을 사용해야 하느냐는 문제입니다. 이 글에서는 각 함수의 내부 동작 원리와 에러 핸들링 메커니즘, 그리고 실제 현업에서 마주치는 성능 병목 현상을 해결하는 구체적인 가이드를 제공합니다. 단순한 문법 나열이 아닌, 메모리 효율성과 실행 흐름 제어 관점에서 깊이 있게 분석합니다.


1. 왜 동시성 제어 함수를 구분해서 사용해야 하는가?

비동기 프로그래밍의 핵심은 '기다리지 않는 것'입니다. 하지만 수천 개의 API 호출이나 DB 쿼리를 동시에 던질 때, 이를 어떻게 수집하고 관리하느냐에 따라 프로그램의 안정성이 결정됩니다.

  • 결과값의 순서가 중요한가?
  • 예외 발생 시 즉시 중단해야 하는가, 아니면 나머지는 계속 실행해야 하는가?
  • 실행 중인 작업의 상태(성공/실패/진행중)를 실시간으로 추적해야 하는가?

위 질문에 대한 답에 따라 우리는 최적의 도구를 선택해야 합니다.


2. asyncio.gather vs wait vs as_completed 상세 비교

각 함수의 특성을 한눈에 파악할 수 있도록 구조화된 표로 정리하였습니다. 이 표는 설계 단계에서 의사결정을 내릴 때 중요한 기준이 됩니다.

비교 항목 asyncio.gather asyncio.wait asyncio.as_completed
주요 목적 모든 결과를 한꺼번에 수집 작업의 완료 상태 및 세부 제어 완료되는 순서대로 즉시 처리
반환값 형태 결과값의 리스트 (List) (Done Set, Pending Set) 튜플 결과를 Yield하는 이터레이터
결과 순서 입력한 코루틴 순서 보장 순서 보장 없음 완료된 시간 순서 (빠른 순)
예외 발생 시 기본적으로 첫 예외 발생 시 전파 예외와 상관없이 지정 조건까지 대기 루프 내에서 개별 예외 처리 필요
권장 사용처 병렬 작업의 결과값이 모두 필요할 때 Timeout 설정이나 세밀한 상태 관리가 필요할 때 먼저 끝난 데이터부터 화면에 보여줄 때

3. 심층 분석: 상황별 최적화 방법과 해결책

① asyncio.gather: 결과 수집의 정석

gather는 가장 직관적입니다. 여러 작업을 던지고, 작업이 입력된 순서대로 결과 리스트를 받습니다. return_exceptions=True 옵션을 사용하면 특정 작업에서 에러가 나더라도 전체 프로세스가 중단되지 않고 결과 리스트에 에러 객체를 포함시켜 반환합니다. 이는 안정적인 데이터 크롤링 시 매우 유용한 해결 방법이 됩니다.

② asyncio.wait: 유연한 제어의 끝판왕

wait는 단순히 결과를 기다리는 것을 넘어 return_when 인자를 통해 제어권을 가집니다. 예를 들어, 5개의 서버에 요청을 보내고 가장 먼저 응답이 오는 하나만 처리(FIRST_COMPLETED)하고 싶을 때 최고의 선택입니다.

③ asyncio.as_completed: 실시간 반응형 처리

사용자 경험(UX)을 중시한다면 as_completed가 답입니다. 100개의 이미지를 다운로드할 때, 모든 다운로드가 끝날 때까지 기다리는 것이 아니라, 완료되는 대로 즉시 브라우저에 뿌려줄 수 있기 때문입니다.


4. 실전 Sample Example

다음은 세 가지 방식의 차이를 극명하게 보여주는 파이썬 코드 예제입니다.


import asyncio
import random

async def heavy_task(name, delay):
    await asyncio.sleep(delay)
    if delay > 2.5:
        raise ValueError(f"Task {name} 실패 (지연시간 초과)")
    return f"Task {name} 결과"

async def main():
    tasks = [
        heavy_task("A", 3.0),
        heavy_task("B", 1.0),
        heavy_task("C", 2.0)
    ]

    print("--- 1. asyncio.gather 활용 (에러 허용) ---")
    results = await asyncio.gather(*tasks, return_exceptions=True)
    print(f"Gather 결과: {results}")

    print("\n--- 2. asyncio.as_completed 활용 (순서 무관) ---")
    # 새 작업 생성 (위에서 이미 실행된 태스크 재사용 불가 방지)
    new_tasks = [heavy_task("D", 0.5), heavy_task("E", 1.5)]
    for coro in asyncio.as_completed(new_tasks):
        res = await coro
        print(f"완료된 작업: {res}")

    print("\n--- 3. asyncio.wait 활용 (타임아웃 적용) ---")
    wait_tasks = [asyncio.create_task(heavy_task("F", i)) for i in [1, 5]]
    done, pending = await asyncio.wait(wait_tasks, timeout=2.0)
    print(f"완료된 수: {len(done)}, 남은 수: {len(pending)}")

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

5. 결론 및 요약

파이썬 비동기 프로그래밍에서 '정답'은 없습니다. 하지만 '최적'은 존재합니다. 데이터의 순서가 중요하다면 gather를, 먼저 끝나는 작업부터 처리하여 반응성을 높이고 싶다면 as_completed를, 작업의 시간 제한이나 조건부 완료가 필요하다면 wait를 사용하는 것이 프로페셔널한 접근입니다. 이러한 차이를 명확히 이해하고 코드를 작성한다면, 단순한 스크립트를 넘어 대규모 트래픽을 견디는 견고한 백엔드 시스템을 설계할 수 있습니다.


내용 출처

  • Python 공식 문서 (docs.python.org): asyncio — Asynchronous I/O
  • Real Python: Async IO in Python: A Complete Walkthrough
  • Stack Overflow: Difference between asyncio.gather and asyncio.wait
728x90