Python 비동기 프로그래밍의 혁신: 구조적 동시성과 TaskGroup 완전 정복
이 글에서는 파이썬 3.11부터 비동기 프로그래밍의 표준으로 자리 잡은 ‘구조적 동시성’과 TaskGroup
의 세계를 탐험합니다. 기존 방식의 함정을 피하고, 더 안전하며 예측 가능한 코드를 작성하는 실전 노하우를 확인해 보세요.
1. 서론: ‘일단 실행하고 잊어버리는’ 비동기 코드의 함정
파이썬의 asyncio
는 고성능 네트워크 애플리케이션을 구축하는 강력한 도구로 자리 잡았습니다. 여러 작업을 동시에 실행하기 위해 우리는 흔히 asyncio.create_task()
나 asyncio.gather()
를 사용해왔습니다. 이 방식은 간단해 보이지만, 보이지 않는 곳에서 심각한 문제를 야기할 수 있습니다. 바로 ‘비구조적 동시성(Unstructured Concurrency)’의 문제입니다.
여러 작업을 실행시켜놓고 어느 하나에서 오류가 발생했을 때, 나머지 작업들은 어떻게 될까요? 혹은 특정 조건에서 모든 작업을 한 번에 깨끗하게 취소해야 한다면 어떻게 해야 할까요? 기존 방식에서는 이러한 예외 처리와 자원 관리가 복잡하고, 오류가 발생하기 쉬웠습니다. 실행시킨 태스크가 좀비처럼 떠돌며 리소스를 누수시키거나, 오류가 조용히 무시되어 시스템 전체가 불안정해지는 상황이 비일비재했습니다. 2025년을 앞둔 지금, 마이크로서비스와 서버리스 환경이 보편화되면서 이러한 불안정성은 더 이상 용납하기 힘든 리스크가 되었습니다.
2. 구조적 동시성이란 무엇인가?
구조적 동시성(Structured Concurrency)은 이러한 혼돈에 질서를 부여하는 프로그래밍 패러다임입니다. 핵심 아이디어는 간단합니다. “동시 작업을 시작한 함수는, 그 작업들이 모두 끝날 때까지 종료되지 않아야 한다.” 이는 마치 if
블록이나 for
루프처럼, 동시성 작업의 시작과 끝이 코드 블록을 통해 명확하게 정의되는 것을 의미합니다.
2.1. 비구조적 동시성의 문제: 예측 불가능한 작업들
기존의 asyncio.create_task()
는 ‘실행하고 잊어버리는(fire-and-forget)’ 방식에 가깝습니다. 태스크를 생성하면 그 즉시 백그라운드에서 실행되며, 개발자가 직접 추적하고 관리하지 않으면 그 생명주기를 제어하기 어렵습니다. 예를 들어, 여러 API를 동시에 호출하는 함수가 있다고 가정해 봅시다.
import asyncio
async def fetch_data(api_name, delay):
print(f"{api_name}: fetching...")
await asyncio.sleep(delay)
if api_name == "API B":
raise ValueError("API B failed!")
print(f"{api_name}: fetched successfully.")
return f"Data from {api_name}"
async def main_unstructured():
task_a = asyncio.create_task(fetch_data("API A", 2))
task_b = asyncio.create_task(fetch_data("API B", 1))
task_c = asyncio.create_task(fetch_data("API C", 3))
# gather를 사용해 결과를 기다리지만, 오류 전파와 취소가 복잡함
results = await asyncio.gather(task_a, task_b, task_c, return_exceptions=True)
print(f"Results: {results}")
# 실행 결과: API B에서 오류가 나도 A와 C는 계속 실행됨
# [실행 결과]
# API A: fetching...
# API B: fetching...
# API C: fetching...
# API B: fetched successfully. -> 오류 발생!
# API A: fetched successfully.
# API C: fetched successfully.
# Results: ['Data from API A', ValueError('API B failed!'), 'Data from API C']
위 코드에서 API B
가 실패하더라도 API A
와 API C
는 계속 실행됩니다. 이는 불필요한 리소스 낭비일 뿐만 아니라, 연관된 작업들 간의 데이터 정합성을 해칠 수 있습니다. 하나의 작업 실패가 전체 로직의 실패를 의미한다면, 나머지 작업들을 즉시 ‘취소’하는 명확한 메커니즘이 필요합니다.
2.2. 구조적 동시성의 해법: 명확한 생명주기 관리
구조적 동시성은 TaskGroup
(혹은 Trio 라이브러리의 ‘Nursery’)이라는 개념을 도입하여 이 문제를 해결합니다. TaskGroup
은 동시성 작업들을 위한 컨텍스트 관리자(async with
)로 동작합니다. async with
블록 안에서 생성된 모든 태스크는 해당 블록이 끝날 때 반드시 종료됨이 보장됩니다.
만약 그룹 내의 한 태스크에서 예외가 발생하면, TaskGroup
은 다음과 같이 행동합니다.
- 그룹 내의 나머지 모든 태스크에 즉시 취소(cancellation) 요청을 보냅니다.
- 모든 태스크가 취소되고 종료될 때까지 기다립니다.
- 최초 발생한 예외를
async with
블록 밖으로 전파합니다.
이러한 동작 방식 덕분에 개발자는 더 이상 개별 태스크의 성공, 실패, 취소 상태를 수동으로 관리할 필요가 없습니다. 모든 것이 TaskGroup
이라는 울타리 안에서 안전하고 예측 가능하게 처리됩니다.
3. 실전 코드: asyncio.TaskGroup
으로 리팩토링하기
Python 3.11부터 asyncio
에 TaskGroup
이 정식으로 추가되면서, 이제 누구나 표준 라이브러리만으로 구조적 동시성의 혜택을 누릴 수 있게 되었습니다. 앞서 살펴본 비구조적 코드를 TaskGroup
으로 개선해 보겠습니다.
3.1. Before: asyncio.gather
의 숨겨진 위험
다시 한번 gather
를 사용한 코드를 보겠습니다. 오류가 발생해도 다른 작업이 계속 진행되며, return_exceptions=True
옵션 없이는 프로그램 전체가 멈추고 예외를 추적하기 어렵습니다. 모든 작업을 수동으로 취소하려면 복잡한 코드가 추가되어야 합니다.
3.2. After: asyncio.TaskGroup
으로 구현한 안전한 동시성
이제 TaskGroup
을 사용해 코드를 재작성해 봅시다. 코드가 훨씬 간결하고 직관적으로 변하는 것을 확인할 수 있습니다.
import asyncio
# fetch_data 함수는 이전과 동일
async def main_structured():
try:
async with asyncio.TaskGroup() as tg:
task_a = tg.create_task(fetch_data("API A", 2))
task_b = tg.create_task(fetch_data("API B", 1))
task_c = tg.create_task(fetch_data("API C", 3))
# 이 블록이 끝나면 모든 작업이 성공적으로 완료된 것
print("All tasks completed successfully.")
# 결과는 각 태스크 객체의 result() 메서드로 얻을 수 있음
print(f"Result A: {task_a.result()}")
except* ValueError as e:
print(f"One or more tasks failed: {e.exceptions}")
# [실행 결과]
# API A: fetching...
# API B: fetching...
# API C: fetching...
# API B failed! -> 오류 발생 즉시 다른 작업 취소 시작
# One or more tasks failed: (ValueError('API B failed!'),)
API B
에서 ValueError
가 발생하자마자 TaskGroup
은 즉시 API A
와 API C
에 취소 요청을 보냅니다. 따라서 불필요한 대기 시간 없이 프로그램이 정리되고, try...except*
블록을 통해 발생한 예외를 명확하게 처리할 수 있습니다. (Python 3.11에 추가된 ExceptionGroup
기능 덕분에 여러 예외를 한 번에 처리할 수 있습니다.)
3.3. 핵심 비교: gather
vs. TaskGroup
특징 | asyncio.gather |
asyncio.TaskGroup |
---|---|---|
오류 처리 | 기본적으로 첫 오류 발생 시 즉시 전파. return_exceptions=True 로 동작 변경 가능. |
첫 오류 발생 시 나머지 태스크를 취소하고, 모든 태스크 종료 후 예외 그룹(ExceptionGroup )으로 전파. |
자동 취소 | 자동 취소 기능 없음. 개발자가 수동으로 구현해야 함. | 그룹 내 한 태스크 실패 시 나머지 태스크를 자동으로 취소. |
태스크 추가 | 실행 전에 모든 태스크를 리스트로 전달해야 함. 동적 추가 불가. | async with 블록 내에서 동적으로 태스크 추가 가능. |
코드 구조 | 작업의 시작과 끝이 분리될 수 있어 생명주기가 불분명함. | async with 블록으로 작업의 범위와 생명주기가 명확하게 정의됨. |
4. 2025년, 구조적 동시성이 필수가 되는 이유
구조적 동시성은 단순히 코드를 예쁘게 만드는 기법이 아닙니다. 현대적인 분산 시스템 아키텍처에서 안정성과 확장성을 담보하기 위한 핵심적인 패러다임입니다.
4.1. 서버리스와 마이크로서비스 시대의 요구사항
하나의 사용자 요청을 처리하기 위해 내부적으로 여러 마이크로서비스를 호출하거나, 서버리스 함수(예: AWS Lambda)가 여러 비동기 작업을 처리하는 일은 이제 흔한 패턴입니다. 이러한 환경에서는 다음과 같은 특징이 중요합니다.
- 빠른 실패(Fail-fast): 연관된 작업 중 하나라도 실패하면, 즉시 전체 작업을 중단하고 리소스를 반환해야 비용과 지연 시간을 줄일 수 있습니다.
- 자원 누수 방지: 특히 실행 시간이 제한된 서버리스 환경에서 백그라운드 태스크가 좀비처럼 남아있는 것은 치명적입니다.
TaskGroup
은 모든 자원이 깨끗하게 정리됨을 보장합니다. - 복잡성 관리: 서비스 간의 호출 관계가 복잡해질수록, 동시성 로직의 예측 가능성은 시스템 전체의 안정성과 직결됩니다. 구조적 동시성은 이러한 복잡성을 효과적으로 관리하는 틀을 제공합니다.
4.2. 더 나아가기: anyio
와 trio
라이브러리
asyncio.TaskGroup
은 구조적 동시성의 훌륭한 시작점이지만, 이 패러다임을 더욱 깊이 있게 탐구하고 싶다면 trio
와 anyio
라이브러리를 주목할 필요가 있습니다.
- Trio: 파이썬에 구조적 동시성 개념을 처음으로 강력하게 도입한 선구적인 라이브러리입니다. 취소(cancellation)와 타임아웃 처리에 대해 매우 엄격하고 명확한 철학을 가지고 있어, 극도로 안정적인 코드를 작성하도록 유도합니다.
- AnyIO:
asyncio
와trio
위에서 동작하는 호환성 레이어입니다.anyio
의TaskGroup
을 사용하면 기반이 되는 라이브러리(백엔드)를 바꾸더라도 코드를 거의 수정하지 않고 사용할 수 있습니다. 또한asyncio
의 기본 기능보다 더 정교한 타임아웃, 동기화 프리미티브 등을 제공하여 많은 사랑을 받고 있습니다.
새로운 프로젝트를 시작하거나 기존 시스템의 안정성을 한 단계 높이고 싶다면, 표준 asyncio
를 넘어 이 라이브러리들을 검토해 보는 것이 좋습니다.
5. 결론: 더 나은 비동기 코드를 향한 패러다임 전환
구조적 동시성과 TaskGroup
의 도입은 파이썬 비동기 프로그래밍의 중요한 이정표입니다. 이는 단순히 새로운 API를 배우는 것을 넘어, 동시성 작업을 바라보는 관점 자체를 바꾸는 패러다임의 전환입니다. ‘일단 실행하고 잊는’ 방식에서 벗어나, 작업의 생명주기를 명확히 정의하고 책임지는 방식으로 나아가는 것입니다.
이제 우리는 더 이상 보이지 않는 오류나 리소스 누수를 걱정하며 복잡한 예외 처리 코드를 작성할 필요가 없습니다. async with asyncio.TaskGroup()
이라는 명확한 울타리 안에서, 파이썬은 우리의 비동기 코드가 시작부터 끝까지 질서정연하고 안전하게 실행되도록 보장해 줄 것입니다. 2025년의 복잡한 분산 환경을 위한 안정적이고 확장 가능한 시스템을 구축하고자 한다면, 구조적 동시성은 선택이 아닌 필수입니다.
참고 자료
- Task Groups – Python 3.12 documentation
- An Intro to Threading in Python – Real Python
- Task groups and cancellation — AnyIO
- Trio: a friendly Python library for async concurrency and I/O
- Notes on structured concurrency; or, Go statement considered harmful – Nathaniel J. Smith
- PEP 654 – Exception Groups and except*