Skip to main content

파이썬 컨텍스트 매니저: 실제로 필요한 세 가지 경우

파이썬 컨텍스트 매니저: 실제로 필요한 세 가지 경우. with 문, contextlib 및 커스텀 컨텍스트 매니저를 마스터하여 더 나은 리소스 관리를 구현하세요.

Go Python
应用领域: Ai Tools

{</* 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 코드에서는 @asynccontextmanagerasync 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%는 특이한 경우이며, 직접 마주하게 되면 알게 되실 겁니다.

관련 기사 #

发布于 Friday, May 15, 2026 · 最后更新 Friday, May 15, 2026