비동기 컨텍스트 매니저
__aenter__와 __aexit__을 구현해 async with 문에서 비동기 자원을 관리하는 방법, @asynccontextmanager 데코레이터, AsyncExitStack 활용법을 설명합니다.
지난 글에서 ExitStack으로 동적 컨텍스트 매니저를 관리하는 방법을 살펴봤다. 비동기 I/O를 다루다 보면 await가 필요한 자원 획득·해제를 with 문으로 감싸야 할 때가 생긴다. Python은 이를 위해 비동기 컨텍스트 매니저 프로토콜을 제공한다.
왜 비동기 컨텍스트 매니저가 필요한가
동기 __enter__/__exit__는 await를 쓸 수 없다. 데이터베이스 연결이나 HTTP 세션처럼 비동기 I/O가 필요한 자원은 __aenter__/__aexit__ 쌍이 필요하다.
# 잘못된 예: 동기 __enter__ 안에서 await 불가
class AsyncDB:
def __enter__(self):
self.conn = await db.connect() # SyntaxError!
클래스로 구현: aenter / aexit
두 메서드 모두 async def로 선언하고 필요하면 await를 사용한다.
import asyncio
class AsyncDBConn:
async def __aenter__(self):
await asyncio.sleep(0) # 비동기 연결 시뮬레이션
self.conn = "connected"
return self.conn
async def __aexit__(self, exc_type, exc_val, exc_tb):
await asyncio.sleep(0) # 비동기 종료
return False
async def main():
async with AsyncDBConn() as conn:
print(conn) # connected
@asynccontextmanager
contextlib.asynccontextmanager 데코레이터는 @contextmanager의 비동기 버전이다. async def 제너레이터 함수에 적용하면 async with에서 쓸 수 있는 컨텍스트 매니저가 된다.
from contextlib import asynccontextmanager
import aiofiles # 예시용
@asynccontextmanager
async def managed_file(path):
f = await aiofiles.open(path)
try:
yield f
finally:
await f.close()
async def main():
async with managed_file("data.txt") as f:
content = await f.read()
패턴은 동기 버전과 동일하다: yield 앞이 진입, yield value가 as에 바인딩, yield 뒤가 탈출. try/finally로 예외 안전하게 작성하는 것도 같다.
asyncio.timeout (Python 3.11+)
Python 3.11부터 asyncio.timeout이 비동기 컨텍스트 매니저로 제공된다.
import asyncio
async def fetch_with_timeout():
try:
async with asyncio.timeout(5.0): # 5초 타임아웃
result = await slow_network_call()
except TimeoutError:
result = None
return result
실용 예제: HTTP 세션 관리
aiohttp 라이브러리의 ClientSession이 대표적인 비동기 컨텍스트 매니저다.
import aiohttp
async def fetch(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
return await response.text()
# 블록 종료 시 세션과 응답 자동 종료
AsyncExitStack
동적 개수의 비동기 컨텍스트 매니저를 관리할 때는 contextlib.AsyncExitStack을 쓴다.
from contextlib import AsyncExitStack
async def multi_connect(hosts):
async with AsyncExitStack() as stack:
conns = [
await stack.enter_async_context(connect(h))
for h in hosts
]
await do_parallel_work(conns)
동기/비동기 혼용
비동기 컨텍스트 매니저 안에서 동기 컨텍스트 매니저를 with로 쓰는 건 괜찮다. 반대로 비동기 컨텍스트 매니저를 일반 with에 쓰면 오류가 난다.
async def example():
async with async_resource() as ar: # OK
with sync_resource() as sr: # OK — 중첩 가능
pass
# 이건 안 됨
with async_resource(): # TypeError: 'async with' required
pass
요약
__aenter__+__aexit__→async with문에서 사용 가능한 비동기 컨텍스트 매니저- 두 메서드 모두
async def, 내부에서await사용 가능 @asynccontextmanager→async def제너레이터로 클래스 없이 구현AsyncExitStack→ 동적 개수의 비동기 컨텍스트 매니저 관리
지난 글: ExitStack — 동적 컨텍스트 매니저 스택
다음 글: GIL — 전역 인터프리터 잠금
읽어주셔서 감사합니다. 😊