파이썬 컨텍스트 매니저: 실제로 필요한 세 가지 경우
파이썬 컨텍스트 매니저: 실제로 필요한 세 가지 경우. with 문, contextlib 및 커스텀 컨텍스트 매니저를 마스터하여 더 나은 리소스 관리를 구현하세요.
{</* resource-info */>}
파이썬 컨텍스트 매니저에 대한 대부분의 입문서는 with open("file.txt") as f:라는 단 하나의 예제만 보여주고 마무리합니다. 이것만으로도 컨텍스트 매니저를 사용하기에는 충분하지만, 언제 직접 작성해야 하는지는 알려주지 않습니다.
수년간 파이썬 서비스를 작성해 오면서, 저는 특정 세 가지 상황에서 컨텍스트 매니저를 반복해서 사용하게 된다는 것을 깨달았습니다. 각 상황은 try/finally로 기술적으로 해결할 수 있지만 실전에서는 실수하기 쉬운 문제들을 해결해 줍니다.
사례 1: 획득(Acquire)과 해제(Release)의 쌍 맞추기 #
가장 고전적인 사례입니다. 락(lock), 데이터베이스 연결, 임시 파일, 네트워크 소켓 등 반드시 해제해야 하는 리소스가 있을 때, 중간 코드에서 예외가 발생하더라도 확실히 해제되도록 보장하고 싶을 때 사용합니다.
from contextlib import contextmanager
import threading
_lock = threading.Lock()
@contextmanager
def critical_section():
_lock.acquire()
try:
yield
finally:
_lock.release()
with critical_section():
do_dangerous_thing()
왜 그냥 try/finally를 쓰지 않나요? 쓸 수 있습니다. 호출하는 쪽에서 컨텍스트 매니저가 확장되는 형태가 바로 그것이니까요. 하지만 이득은 try/finally가 호출하는 곳이 아닌 헬퍼 함수 안에 존재한다는 점입니다. 모든 호출자는 이를 무료로 이용할 수 있고, 누구도 finally 블록 작성을 잊어버리지 않게 됩니다.
코드베이스에서 try: thing.acquire(); ...; finally: thing.release() 패턴이 다섯 번 이상 반복된다면, 그것은 컨텍스트 매니저로 추출될 준비가 되었다는 신호입니다.
사례 2: 전역 상태를 일시적으로 변경하기 #
자주 언급되지는 않지만, 컨텍스트 매니저가 진가를 발휘하는 부분입니다. 특정 블록이 실행되는 동안만 설정을 변경하고, 블록이 어떻게 종료되든 관계없이 원래 상태로 되돌리고 싶을 때 사용합니다.
import os
from contextlib import contextmanager
@contextmanager
def env(**overrides):
"""환경 변수를 일시적으로 설정하고, 종료 시 이전 값으로 복원합니다."""
saved = {k: os.environ.get(k) for k in overrides}
os.environ.update({k: str(v) for k, v in overrides.items()})
try:
yield
finally:
for k, prev in saved.items():
if prev is None:
os.environ.pop(k, None)
else:
os.environ[k] = prev
with env(DEBUG="1", REGION="us-east-1"):
run_test_suite()
# 여기서 환경 변수는 원래 상태로 돌아갑니다.
동일한 패턴이 sys.path, logging 레벨, decimal 컨텍스트, 모킹된 속성(mocked attributes) 등 “저장, 변경, 복원"의 형태를 따르는 모든 곳에 적용됩니다. 특히 테스트에서 유용합니다. 그렇지 않으면 테스트 도중 예외가 발생했을 때 설정이 복구되지 않고 남는(leak) 문제가 발생할 수 있습니다.
여기서 미묘한 부분은 None을 올바르게 복원하는 것입니다. 흔히 하는 실수는 확인 없이 os.environ[k] = saved[k]를 하는 것인데, 이전에 존재하지 않았던 변수에 문자열 "None"이 쓰여버리게 됩니다. 항상 “없음” 상태는 문자열이 아닌 pop으로 복원해야 합니다.
사례 3: 정말로 무시하고 싶은 예외 억제하기 #
때로는 특정 예외 클래스를 의도적으로 무시하고 계속 진행하고 싶을 때가 있습니다. 파이썬은 이를 위해 contextlib.suppress를 제공합니다.
from contextlib import suppress
with suppress(FileNotFoundError):
os.unlink("maybe-stale.lock")
이것은 동일한 기능의 try/except: pass보다 훨씬 명확합니다. 제한된 표면적이 사용자에게 구체적일 것을 강제하기 때문입니다. 실수로 모든 것을 억제할 수 없으며, 반드시 클래스 이름을 지정해야 합니다. 또한 with 블록의 범위가 명확하므로 정리 코드 아래의 코드까지 실수로 억제하지 않게 됩니다.
저는 소멸자(destructors)나 atexit 핸들러의 정리 작업에서 이 기능이 매우 유용하다는 것을 알게 되었습니다. 정리 작업 자체가 예외를 발생시켜서는 안 되는 상황에서 말이죠.
컨텍스트 매니저를 작성하지 말아야 할 때 #
컨텍스트 매니저는 공짜가 아닙니다. 각 with 문은 약간의 오버헤드를 유발하며, 너무 많이 겹쳐 쓰면 가독성이 급격히 떨어집니다. 저는 다음과 같은 경우 사용을 피합니다:
- “획득” 단계에 쌍을 이루는 “해제"가 실제로 필요하지 않은 경우 - 그냥 함수를 호출하세요.
- 정리 작업이 최선형(best-effort)이고 범위가 충분히 작아 인라인
try/finally가 더 읽기 편한 경우. - 관리 대상이 이미 다른 것에 의해 관리되고 있는 경우 (예: 이미 자체 수명 주기를 컨텍스트 관리하고 있는 프레임워크의
Session을 다시 감싸지 마세요).
제가 사용하는 기준은 이렇습니다: “내가 정리를 빠뜨렸을 때, 다음 사람이 조용히 리소스를 누출하게 될까?” 만약 그렇다면 컨텍스트 매니저를 작성하세요. 그렇지 않다면 평범한 함수로 충분합니다.
비동기(Async)에 대하여 #
async 코드에서는 @asynccontextmanager와 async with를 사용하세요. 형태는 동일합니다. 유일하게 기억할 점은 본문 안에서 await를 사용할 수 있다는 것이며, 이는 “풀에서 연결 획득, 쿼리 실행, 반환"과 같은 패턴에서 더욱 유용하게 쓰입니다.
from contextlib import asynccontextmanager
@asynccontextmanager
async def borrowed(pool):
conn = await pool.acquire()
try:
yield conn
finally:
await pool.release(conn)
이것이 전부입니다. 제가 작성한 컨텍스트 매니저의 90%는 이 세 가지 패턴에 해당합니다. 나머지 10%는 특이한 경우이며, 직접 마주하게 되면 알게 되실 겁니다.
관련 기사 #
- Scrapling 리뷰: 더 빠르고 은밀한 파이썬 스크래핑 제안 — 고급 파이썬 스크래핑
- 길을 잃지 않고 Postgres에서 EXPLAIN ANALYZE 읽기 — 데이터베이스 성능 최적화
- 무료 Claude Code: 모든 AI 제공업체와 함께 Claude Code CLI를 무료로 사용하기 — AI 지원 코딩