
파이썬의 asyncio 환경에서 복잡한 애플리케이션을 구축할 때, 단순히 await를 사용하는 것만으로는 부족합니다. 특히 네트워크 요청이나 대규모 데이터 처리를 비동기로 수행할 때, 특정 상황에서 작업을 중단(Cancellation)하거나 발생한 예외(Exception)를 부모 코루틴으로 안전하게 전파하는 설계 능력은 시니어 개발자와 주니어 개발자를 가르는 결정적인 차이가 됩니다.
본 포스팅에서는 실무에서 흔히 발생하는 비동기 설계 오류를 짚어보고, CancelledError의 특성과 예외 체이닝을 활용하여 안정적인 비동기 시스템을 구축하는 전문적인 가이드를 제시합니다.
1. 비동기 작업 취소(Cancellation)의 메커니즘과 차이점
파이썬 비동기 태스크의 취소는 강제 종료가 아닙니다. Task.cancel()이 호출되면 해당 코루틴이 대기 중인 포인트(주로 await 문)에서 asyncio.CancelledError 예외를 던지는 방식입니다. 여기서 가장 중요한 점은 "협력적 취소(Cooperative Cancellation)"라는 점입니다.
취소 요청과 처리 프로세스
- Step 01: 외부에서
task.cancel()호출 - Step 02: 다음 이벤트 루프 사이클에서 해당 Task에
CancelledError주입 - Step 03: 코루틴 내부의
try...finally블록을 통해 리소스 정리 수행 - Step 04: 최종적으로 Task가 중단됨
| 구분 | 일반 예외 (Exception) | 취소 예외 (CancelledError) |
|---|---|---|
| 발생 원인 | 코드 로직 오류, 런타임 에러 | 외부 시스템 또는 유저의 중단 요청 |
| 기본 동작 | 프로그램 중단 또는 except 블록 포착 |
정리(cleanup) 후 태스크 종료 유도 |
| 전파 방식 | 호출 스택을 타고 상위로 전달 | await 지점에서 즉시 발생 |
| 권장 해결 방법 | 에러 로깅 및 복구 로직 실행 | finally를 통한 자원 해제 필수 |
2. 예외 처리 전파 방식의 2가지 핵심 패턴
비동기 그룹(예: asyncio.gather vs asyncio.TaskGroup) 내에서 예외가 발생했을 때 이를 어떻게 전파하느냐에 따라 시스템의 안정성이 결정됩니다.
방법 01: asyncio.gather의 return_exceptions 활용
여러 태스크 중 하나가 실패해도 나머지는 계속 실행되어야 할 때 유용합니다. 하지만 예외가 묻힐 수 있다는 위험이 있습니다.
방법 02: Python 3.11+ TaskGroup 활용
최신 파이썬 버전에서는 TaskGroup 사용을 권장합니다. 그룹 내 한 태스크가 실패하면 나머지 태스크도 자동으로 취소되어 '좀비 프로세스'가 남는 것을 방지합니다.
3. 실전 Sample Example: 안전한 파일 다운로더 구현
아래 코드는 비동기 Task 취소와 예외 처리를 실무 수준으로 구현한 예시입니다. 네트워크 타임아웃 발생 시 어떻게 자원을 정리하고 상위로 에러를 보고하는지 확인하십시오.
import asyncio
import random
async def fetch_data(task_id: int):
try:
print(f"[시작] Task {task_id}: 데이터 다운로드 중...")
# 가상의 네트워크 지연
await asyncio.sleep(random.uniform(1, 5))
if task_id == 2:
raise ValueError("데이터 형식이 올바르지 않습니다.")
return f"Task {task_id} 결과 성공"
except asyncio.CancelledError:
print(f"[중단] Task {task_id}: 연결이 취소되어 세션을 닫습니다.")
raise # 반드시 다시 raise하여 Task 상태를 Cancelled로 유지해야 함
except Exception as e:
print(f"[에러] Task {task_id}: 예상치 못한 오류 발생 -> {e}")
raise
async def main():
tasks = []
try:
async with asyncio.TaskGroup() as tg:
for i in range(1, 4):
tasks.append(tg.create_task(fetch_data(i)))
except ExceptionGroup as eg:
print(f"[그룹 에러 발생] {len(eg.exceptions)}개의 문제가 발견되었습니다.")
for e in eg.exceptions:
print(f" - 상세 내용: {e}")
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
pass
4. 결론 및 요약
파이썬 비동기 프로그래밍에서 취소와 예외 처리는 단순히 에러를 막는 것이 아니라, 애플리케이션의 상태 정합성을 유지하는 과정입니다. 3.11 버전 이후 도입된 ExceptionGroup과 TaskGroup을 적극 활용하면, 기존의 파편화된 비동기 에러 핸들링 문제를 80% 이상 해결할 수 있습니다.
| 비동기 제어 포인트 | 권장 해결 전략 |
|---|---|
| 리소스 정리 | 반드시 finally 또는 async with 사용 |
| 다중 작업 관리 | asyncio.TaskGroup 사용 (3.11 이상) |
| 취소 처리 | CancelledError 포착 시 반드시 re-raise |
| 부분 성공 허용 | asyncio.gather(..., return_exceptions=True) |
5. 참고 문헌 및 자료 출처
- Python Software Foundation.
- Luciano Ramalho. "Fluent Python, 2nd Edition" - Chapter 20: Concurrent Executors
- PEP 654 – Exception Groups and except* specification
'Artificial Intelligence > 60. Python' 카테고리의 다른 글
| [PYTHON] 고성능 비동기 처리를 위한 Greenlet과 Fiber 개념의 3가지 차이점과 실전 구현 방법 (0) | 2026.03.18 |
|---|---|
| [PYTHON] asyncio.run() 내부의 3가지 작동 원리와 비동기 루프 해결 방법 (0) | 2026.03.18 |
| [PYTHON] Multiprocessing Manager 객체를 통한 상태 공유 시 발생하는 3가지 오버헤드 해결 방법 (0) | 2026.03.18 |
| [PYTHON] 고성능 서버를 위한 select, poll, epoll 3가지 차이와 해결 방법 (0) | 2026.03.18 |
| [PYTHON] 코드 커버리지(Code Coverage) 100%의 함정과 효율적인 해결 방법 5가지 차이 (0) | 2026.03.18 |