Context Manager

python에서 context manager는 자원의 획득과 해제를 자동화하여 코드의 안정성과 가독성을 높여주는 문법입니다. 예외 발생 여부와 상관없이 항상 리소스의 정리를 보장하며 복잡한 코드에서도 자원 관리와 예외 처리를 투명하게 만들어줍니다.
기본 사용법
with
문법은 아래와 같이 표현할 수 있습니다.
with EXPRESSION as TARGET:
SUITE
표현식(EXPRESSION
) 추출된 값인 TextIOWrapper 객체 (TARGET
) 을 SUITE
에서 사용이 가능하며 아래 예제처럼 파일을 열고 Hello, World!
라는 글자를 입력하면 with문 블록을 빠져나오면서 파일이 닫히게 됩니다.
with open('example.txt', 'w') as file:
file.write('Hello, World!')
우리가 자주 쓰는 Context Manager 예시들
개발하면서 알게 모르게 사용하고 있는 context manager가 여럿 있습니다. 몇 가지 예시들을 살펴보겠습니다:
-
파일 입출력 (open): 앞서 언급했듯 가장 기본적인 예시입니다.
with open('data.txt', 'r') as f:
형태로 파일을 열면 블록이 끝날 때 자동으로f.close()
가 호출되어 파일이 닫힙니다. -
스레드 락 (threading.Lock): 멀티스레드 환경에서 공유 자원을 보호하기 위해 threading.Lock 객체를 사용하는데, 이 또한 context manager로 쓸 수 있습니다. with lock: 구문을 쓰면
lock.acquire()
와lock.release()
를 일일이 호출하는 대신 블록이 시작할 때 락을 획득하고 끝날 때 자동으로 반납합니다. -
데이터베이스 세션 (예: SQLAlchemy Session): SQLAlchemy ORM을 사용한다면
Session = sessionmaker(engine)
으로 세션 팩토리를 만든 뒤,with Session() as session:
형태로 세션을 열 수 있습니다. 이렇게 열면 블록이 끝난 후 세션이 자동으로close()
되므로 명시적으로 닫지 않아도 됩니다 . 그리고with Session.begin() as session:
같은 구문을 쓰면 트랜잭션을 시작하고 블록이 정상 종료되면 commit, 예외 발생 시 rollback까지 자동으로 수행해줍니다. -
임시 파일/디렉토리 (tempfile.TemporaryDirectory): 백엔드 작업을 하다 보면 일시적인 파일이나 폴더를 만들어 쓰는 일이 많습니다(예: 파일 업로드 처리 후 임시 저장 등). 이때
tempfile.TemporaryDirectory()
를 context manager로 사용하면with
블록 내에서 임시디렉토리를 만들고 사용한 뒤, 블록이 끝날 때 디렉토리와 그 안의 파일들을 자동으로 삭제해줍니다.
Context Manager 만들기
context manager를 직접 만들 수도 있습니다. 두 가지 방식으로 만들 수 있는데, 하나는 클래스에 __enter__
와 __exit__
메서드를 구현하는 것이고, 다른 하나는 contextlib.contextmanager
데코레이터를 활용할 수 있습니다.
클래스 기반 Context Manager
클래스로 context manager를 만들려면 __enter__와 exit 메서드를 정의하면 됩니다. with 문이 시작될 때 __enter__가 호출되고, 블록이 끝날 때(정상이든 예외든) __exit__이 호출되는 식입니다. 아래는 간단한 데이터베이스 연결 예시입니다.
import sqlite3
class DatabaseConnection:
def __init__(self, db_name):
self.db_name = db_name
self.conn = None # 연결 객체를 저장할 속성
def __enter__(self):
# context 진입 시 데이터베이스 연결 열기
self.conn = sqlite3.connect(self.db_name)
return self.conn # 연결 객체를 with문 안으로 반환
def __exit__(self, exc_type, exc_val, exc_tb):
# context 종료 시 (예외 여부에 따라) 처리
if exc_type:
self.conn.rollback() # 예외 발생 -> 트랜잭션 rollback
else:
self.conn.commit() # 정상 종료 -> 트랜잭션 commit
self.conn.close() # 연결 항상 닫기
# 사용 예시
with DatabaseConnection('example.db') as conn:
cursor = conn.cursor()
cursor.execute('SELECT * FROM users')
rows = cursor.fetchall()
# ... (예외 발생 시 자동 rollback, 정상 시 commit)
위 코드에서 DatabaseConnection 클래스는__enter__에서 sqlite3.connect를 통해 DB 연결을 맺고 그 객체를 반환하여 with 블록 안에서 conn으로 사용할 수 있게 합니다 . __exit__에서는 exc_type 등을 통해 블록 실행 중 예외가 있었는지 체크하고, 예외가 있었다면 rollback(), 없었다면 commit()을 호출한 뒤 연결을 닫습니다.
with 블록을 빠져나오는 순간 트랜잭션이 알아서 처리되고, 연결도 깨끗이 정리됩니다.
이처럼 클래스 기반 context manager는 자원 초기화와 해제 로직을 한 군데에 담아두고, with 구문을 통해 쉽게 사용할 수 있게 만드는 방법입니다. 실제로 파이썬의 많은 내장 클래스들이 이 원리를 따릅니다 (파일 객체, 락 객체 등은 내부적으로 enter/__exit__이 구현되어 있습니다).
함수 기반 Context Manager(contextlib.contextmanager)
두 번째 방법은 contextlib 모듈의 @contextmanager
데코레이터를 사용하는 것입니다. 이 데코레이터를 함수에 붙이면, 그 함수의 내부 구현을 통해 context manager를 간편히 만들 수 있습니다 . 함수 기반 context manager는 간단한 상황에서 유용히다는 장점이 있습니다.
사용 방법은 함수 안에 yield
를 사용하여 yield 이전 코드를 enter 시 실행할 설정, yield 이후 코드를 exit 시 실행할 정리 작업으로 구분합니다.
from contextlib import contextmanager
import sqlite3
@contextmanager
def db_session(db_name):
conn = sqlite3.connect(db_name)
try:
yield conn
conn.commit()
except Exception:
conn.rollback()
raise # 예외를 상위로 다시 발생시킴
finally:
conn.close()
# 사용 예시
with db_session('example.db') as conn:
cur = conn.cursor()
cur.execute('INSERT INTO users VALUES (?, ?)', ("Alice", 25))
# 블록 끝나면 자동 commit 또는 예외 시 rollback
위 db_session 함수는 앞서 클래스의 동작과 동일하지만 훨씬 간결하게 작성되었습니다. yield conn
이전의 conn = connect(...)
부분이 진입 시 실행되고, yield
이후의 conn.commit()
과 conn.close()
등이 블록 종료 시 실행됩니다. 예외가 발생하면 except
에서 rollback
을 하고 raise
로 예외를 다시 던져주어 블록 밖에서도 예외가 전파되도록 했습니다. 이렇게 함으로써 with db_session(...)
블록 안에서는 트랜잭션이 관리되는 연결 conn을 얻어 쓰기만 하면 되고, 커밋/롤백/닫기는 모두 contextmanager 함수가 알아서 처리합니다.