[Python 시리즈 16편] 컨텍스트 매니저와 with 문으로 자원 관리하기

English version

이터레이터로 데이터 흐름을 통제했다면, 이제는 파일·소켓·락처럼 반드시 닫아야 하는 자원을 안전하게 다루는 차례입니다. Python의 컨텍스트 매니저(context manager)with 문과 함께 사용되어 자원 사용 전후의 코드를 자동으로 실행해 줍니다. 겁먹을 필요가 없습니다. "열고 → 쓰고 → 닫는 세 줄을 묶는다"가 전부입니다.

이번 글에서 새로 나오는 용어

  1. 컨텍스트 매니저: with 블록이 시작되고 끝날 때 실행할 코드를 묶어 둔 구조
  2. with: 컨텍스트 매니저를 사용해 자원을 열고 닫는 흐름을 자동화하는 구문
  3. __enter__/__exit__: 컨텍스트 매니저 클래스가 반드시 구현해야 하는 메서드로 자원 준비와 정리를 담당
  4. ExitStack: 필요한 만큼 컨텍스트를 동적으로 쌓아 한 번에 정리해 주는 contextlib 도구

개념 정리

학습 메모

  • 소요 시간: 50~60분
  • 준비물: 파일 IO, 예외 처리, 함수·클래스 정의
  • 학습 목표: 직접 컨텍스트 매니저 클래스를 만들고 @contextmanager 버전으로도 구현해 자원 정리 자동화하기
  • 컨텍스트 매니저는 with 블록 전후에 실행할 코드를 모아둔 구조입니다.
  • __enter__는 자원을 열고, __exit__는 정리와 예외 처리를 담당합니다.
  • contextlib.contextmanager는 제너레이터로 컨텍스트 매니저를 쉽게 만드는 데코레이터입니다.
  • ExitStack은 필요한 수만큼 컨텍스트를 동적으로 쌓는 도구입니다.
  • Core 섹션만으로도 대부분의 자원 관리가 해결되며, Optional은 도구 상자 확장용입니다.
direction: right

open: "__enter__()
자원 열기"
work: "with 블록
업무 처리"
cleanup: "__exit__()
정리/롤백"

open -> work: "핸들 반환"
work -> cleanup: "성공/예외" 
cleanup -> open: "다음 자원 준비"

리소스 생명 주기를 눈으로 확인하면 __enter____exit__가 항상 한 쌍으로 작동한다는 점을 잊지 않게 됩니다.

코드로 이해하기

기본 예시 (Core)

with open("scores.csv", "r", encoding="utf-8") as f:
    data = f.read()
  • with 블록이 시작될 때 파일이 열리고, 블록이 끝나면 자동으로 닫힙니다.
  • 예외가 발생하더라도 close() 호출이 보장되므로 누수 위험이 줄어듭니다.

컨텍스트 매니저 프로토콜 (Core)

컨텍스트 매니저는 두 개의 메서드를 구현합니다.

  • __enter__(self): with 블록 시작 시 실행됩니다. 반환값이 as 뒤 변수에 할당됩니다.
  • __exit__(self, exc_type, exc_val, exc_tb): 블록 종료 시 실행됩니다. 예외 정보가 전달되며, True를 반환하면 예외가 억제됩니다.
class DatabaseSession:
    def __enter__(self):
        self.conn = connect()
        return self.conn

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type:
            self.conn.rollback()
        else:
            self.conn.commit()
        self.conn.close()


with DatabaseSession() as conn:
    conn.execute("INSERT ...")

contextlib로 간단하게 만들기 (Core)

클래스를 직접 작성하기 부담스럽다면 이 섹션부터 시작하세요. yield 앞뒤에 무엇을 배치하느냐만 기억하면 됩니다.

contextlib 모듈을 사용하면 클래스를 직접 작성하지 않아도 됩니다.

from contextlib import contextmanager


@contextmanager
def temporary_directory(path):
    os.makedirs(path, exist_ok=True)
    try:
        yield path
    finally:
        shutil.rmtree(path)


with temporary_directory("./tmp-build") as build_dir:
    ...
  • @contextmanager는 제너레이터 기반 컨텍스트 매니저를 쉽게 정의하는 데코레이터입니다.
  • yield 이전 코드가 __enter__, 이후 코드가 __exit__의 역할을 합니다.

다중 컨텍스트 관리 (Core → Plus)

from contextlib import ExitStack


with ExitStack() as stack:
    files = [stack.enter_context(open(path)) for path in file_list]
    ...

ExitStack은 런타임에 필요한 만큼 컨텍스트를 추가할 수 있게 해주는 도구입니다. 반복문 안에서 여러 파일을 열어야 할 때 유용합니다. 아직 한두 개 자원만 다룬다면 제목만 기억하고 넘어가도 괜찮습니다.

예외 처리 전략 (Optional)

  • __exit__에서 예외를 억제하면 버그가 숨겨질 수 있으므로 꼭 필요한 경우에만 True를 반환하세요.
  • 로깅이나 정리 작업은 finally 블록에서 수행하면 됩니다.
  • contextlib.suppress(*exceptions)로 특정 예외를 무시할 수 있지만, 원인을 알고 있을 때만 사용합니다.

실전 활용 예 (Optional)

  • 파일 핸들러: open, gzip.open
  • 데이터베이스 세션: SQLAlchemy Session
  • 동시성 제어: threading.Lock, asyncio.Lock
  • 환경 설정 변경: contextlib.ExitStack으로 임시 설정을 적용한 뒤 자동 복원

왜 중요할까

  • 컨텍스트 매니저는 자원 전후 처리 코드를 구조화합니다.
  • __enter__/__exit__ 또는 @contextmanager로 직접 구현할 수 있습니다.
  • 여러 자원을 한 번에 다루어야 한다면 ExitStack으로 동적으로 쌓을 수 있습니다.

실습

  • 따라 하기: DatabaseSession 클래스를 작성해 with 블록에서 커밋·롤백 로그를 출력하도록 만듭니다.
  • 확장하기: @contextmanager를 이용해 임시 디렉터리를 생성하고 블록이 끝나면 삭제되는지 검증합니다.
  • 디버깅: __exit__에서 예외를 억제하도록 return True를 넣어 보고 로그가 사라지는 문제를 파악한 뒤 다시 예외를 전달합니다.
  • 완료 기준: 클래스형·제너레이터형 두 방식 모두로 자원 관리 코드를 만들어 테스트 스크립트에서 통과시켰을 때입니다.

마무리

다음 편에서는 타입 힌트와 typing.Protocol을 활용해 인터페이스를 명시하고 도구 지원을 강화해 보겠습니다.

💬 댓글

이 글에 대한 의견을 남겨주세요