
파이썬의 asyncio 라이브러리를 활용하여 고성능 비동기 애플리케이션을 개발할 때, 개발자들이 가장 빈번하게 마주치는 고민 중 하나는 "여러 개의 태스크를 어떻게 효율적으로 동시에 실행하고 제어할 것인가?"입니다. 특히 실행 중 발생할 수 있는 예외(Exception)를 어떻게 처리하느냐에 따라 프로그램의 안정성이 결정됩니다. 본 포스팅에서는 실무 환경에서 가장 많이 쓰이는 두 함수, asyncio.gather와 asyncio.wait의 기술적 메커니즘을 심층 분석하고, 에러 핸들링 시 발생하는 결정적인 차이점과 상황별 최적의 해결 전략을 제시합니다.
1. asyncio.gather vs asyncio.wait: 동작 원리의 이해
두 함수 모두 여러 코루틴을 동시에 실행하는 목적은 같지만, 반환 값의 형태와 예외가 발생했을 때의 전파 방식에서 큰 차이를 보입니다.
asyncio.gather의 특징
gather는 결과 중심적입니다. 전달된 모든 코루틴이 완료되면 그 결과값들을 리스트 형태로 순서대로 반환합니다. 만약 실행 도중 예외가 발생하면 기본적으로 호출자에게 즉시 예외를 전파하며, 다른 태스크들은 배경에서 계속 실행되지만 제어가 까다로워질 수 있습니다.
asyncio.wait의 특징
wait는 상태 중심적입니다. 태스크의 완료 여부(Done, Pending)를 집합(Set) 형태로 반환하며, 예외가 발생하더라도 이를 즉시 던지지 않고 완료된 태스크 객체 안에 보관합니다. 개발자는 return_when 옵션을 통해 제어권을 언제 되찾아올지 정교하게 설계할 수 있습니다.
2. 에러 핸들링 방식의 결정적 차이 요약
두 방식의 차이를 한눈에 파악할 수 있도록 표로 정리하였습니다.
| 비교 항목 | asyncio.gather | asyncio.wait |
|---|---|---|
| 주요 반환값 | 태스크 결과값의 리스트 (List) | (Done set, Pending set) 튜플 |
| 기본 예외 처리 | 첫 번째 에러 발생 시 즉시 예외 발생 | 에러가 발생해도 직접 던지지 않음 |
| return_exceptions 옵션 | 지원함 (True 설정 시 에러를 결과로 취급) | 지원하지 않음 (태스크 객체에서 확인 필요) |
| 태스크 순서 보장 | 입력 순서대로 결과 리스트 구성 | 순서 보장 없음 (Set 자료형 사용) |
| 사용 권장 상황 | 모든 결과값이 한꺼번에 필요한 경우 | 태스크 진행 상태를 세밀하게 제어할 경우 |
3. Sample Example: 에러 발생 시의 코드 흐름 비교
실제 코드를 통해 에러가 어떻게 전파되는지 살펴보겠습니다. 두 코루틴 중 하나에서 의도적으로 ValueError를 발생시키는 상황입니다.
예제 1: asyncio.gather를 이용한 에러 처리
import asyncio
async def task_success():
await asyncio.sleep(1)
return "성공"
async def task_fail():
await asyncio.sleep(0.5)
raise ValueError("작업 중 에러 발생!")
async def run_gather():
# return_exceptions=True로 설정하면 에러 객체가 리스트에 포함됩니다.
results = await asyncio.gather(task_success(), task_fail(), return_exceptions=True)
print(f"Gather 결과: {results}")
asyncio.run(run_gather())
예제 2: asyncio.wait를 이용한 에러 처리
import asyncio
async def run_wait():
t1 = asyncio.create_task(task_success())
t2 = asyncio.create_task(task_fail())
done, pending = await asyncio.wait([t1, t2], return_when=asyncio.FIRST_EXCEPTION)
for task in done:
try:
result = task.result()
print(f"작업 완료 결과: {result}")
except Exception as e:
print(f"에러 확인: {e}")
asyncio.run(run_wait())
4. 전문가가 제안하는 3가지 실전 해결 전략
대규모 트래픽을 처리하는 백엔드 시스템에서는 다음과 같은 전략을 권장합니다.
- 결과 데이터의 무결성이 중요한 경우 (gather):
return_exceptions=True를 사용하여 전체 결과를 수집한 뒤, 리스트 내부에Exception인스턴스가 있는지isinstance()함수로 필터링하여 처리하세요. - 타임아웃 및 조기 중단이 필요한 경우 (wait):
FIRST_EXCEPTION옵션을 사용하여 하나라도 실패하면 즉시 로깅하고 나머지 작업을 취소(cancel)하는 로직을 구성하여 리소스를 방어하세요. - 독립적인 작업 수행: 각 코루틴 내부에서
try-except문으로 감싸 에러를 자체적으로 소화하게 만들면, 상위 호출부의 로직이 단순해집니다.
5. 결론 및 요약
비동기 프로그래밍에서 에러 핸들링은 단순히 try-except를 쓰는 것 이상의 설계적 고민이 필요합니다. 결과값의 순서와 집합이 중요하다면 gather를, 실행 상태에 따른 동적인 제어가 우선이라면 wait를 선택하는 것이 가장 효과적인 해결 방법입니다.
참조 및 출처:
- Python Software Foundation 공식 문서 (docs.python.org/3/library/asyncio-task.html)
- Real Python: Async IO in Python: A Complete Walkthrough
- Stack Overflow Python Asyncio Performance Discussion