Menu
Playground에서 시도하기

파이썬 with문과 컨텍스트 매니저 완벽 정리

파일, 락, DB 커넥션처럼 뒷정리가 중요한 자원을 안전하게 닫아주는 with문의 동작 원리를 제대로 알아봅니다.

알아서 뒷정리까지 해주는 구문

프로그램에서 열어 쓰는 자원 — 파일, 네트워크 연결, 데이터베이스 핸들, 락 등 — 은 다 쓰고 나면 반드시 닫아줘야 합니다. 깜빡하면 메모리 누수가 생기거나, 락이 계속 잡혀서 다른 프로세스를 막거나, 프로그램이 뻗었을 때 파일이 깨질 수도 있죠. 파이썬의 with 문은 이 뒷정리를 알아서 처리해 줍니다.

처음 배울 때 가장 흔히 만나는 패턴은 파일 읽기입니다:

with open("notes.txt") as f:
    contents = f.read()
    print(contents)

자동으로 두 가지 일이 일어납니다. 블록에 진입할 때 open()이 파일 객체를 반환해 f에 바인딩하고, 블록을 빠져나갈 때 — 정상적으로 끝나든, 중간에 return으로 빠져나가든, 예외가 발생하든 상관없이 — 파이썬이 알아서 f.close()를 호출해 줍니다.

끝입니다. 이것이 with의 존재 이유 전부예요.

with가 대체하는 코드

컨텍스트 매니저가 없던 시절에는 같은 일을 안전하게 처리하려면 try/finally를 써야 했습니다:

f = open("notes.txt")
try:
    contents = f.read()
    print(contents)
finally:
    f.close()

"파일을 열어서 읽고, 끝나면 닫는다"는 간단한 동작에 형식적인 코드가 다섯 줄이나 붙습니다. 규모가 좀 있는 프로그램에서 open이 등장할 때마다 이런 코드가 반복된다고 생각해 보세요. with 문이 왜 매력적인지 바로 와닿을 겁니다. 코드가 짧아지고, 실수할 여지도 줄어들며, 자원 정리를 까먹는 일이 아예 불가능해집니다.

여러 리소스를 한 번에 열기

하나의 with 문에서 여러 컨텍스트 매니저를 동시에 사용할 수 있습니다:

with open("input.txt") as src, open("output.txt", "w") as dst:
    dst.write(src.read().upper())

두 파일 모두 진입 시점에 열리고, 종료 시점에 닫힙니다. 첫 번째 open은 성공했는데 두 번째에서 예외가 발생해도, 파이썬이 알아서 첫 번째 파일을 닫아줍니다. 부분적으로 열린 리소스도 제대로 정리해 준다는 뜻이죠.

리소스가 많아질수록 괄호 문법(Python 3.10 이상)이 훨씬 읽기 좋습니다:

with (
    open("a.txt") as a,
    open("b.txt") as b,
    open("c.txt") as c,
):
    ...

컨텍스트 매니저란 도대체 무엇인가

__enter____exit__를 정의한 객체라면 뭐든 컨텍스트 매니저가 됩니다. 프로토콜 자체는 정말 단순합니다.

  • __enter__(self)with 블록이 시작될 때 호출됩니다. 이 메서드가 반환하는 값이 as name에 바인딩됩니다.
  • __exit__(self, exc_type, exc_value, traceback)는 블록이 끝날 때 호출되는데, 어떤 식으로 끝나든 상관없이 실행됩니다. 만약 예외 때문에 빠져나오는 경우라면 예외 정보가 인자로 넘어오기 때문에, 컨텍스트 매니저 쪽에서 이를 들여다보거나 삼켜버릴 수도 있습니다.

아래는 감싼 블록의 실행 시간을 재주는 가장 간단한 예시입니다.

main.py
Output
Click Run to see the output here.

with Timer():를 실행하면 객체가 만들어지고, __enter__가 호출되고, 본문이 실행된 뒤, __exit__가 호출됩니다. 파일도 락도 없어요. 그냥 "뭔가를 하고, 걸린 시간을 재는" 동작을 감싼 작은 래퍼일 뿐입니다.

contextlib.contextmanager로 간단하게 만들기

컨텍스트 매니저를 만들 때마다 클래스를 정의하는 건 좀 과합니다. contextlib.contextmanager를 쓰면 제너레이터 함수를 컨텍스트 매니저로 바꿀 수 있어요. yield 하나를 기준으로 "이전"과 "이후"가 나뉩니다.

main.py
Output
Click Run to see the output here.

yield 이전 코드는 __enter__에 해당하고, 이후 코드는 __exit__에 해당합니다. try/finally로 감싸둔 덕분에 블록 안에서 예외가 터져도 정리 코드가 반드시 실행됩니다.

직접 만드는 컨텍스트 매니저는 대부분 이런 모양으로 충분합니다. 먼저 데코레이터 방식으로 시도해 보고, 제너레이터로 표현하기 어려운 경우에만 클래스로 내려가세요.

값을 잠깐 바꿨다가 되돌리기

자주 쓰는 패턴이 있습니다. 어떤 값을 설정한 뒤 사용하고, 끝나면 원래 상태로 복원하는 흐름이죠. 컨텍스트 매니저를 쓰면 이런 로직이 깔끔하게 표현됩니다:

main.py
Output
Click Run to see the output here.

"설정하고 나중에 복원하는" 패턴이라면 — 환경 변수, 로깅 레벨, 피처 플래그, 테스트 픽스처 무엇이든 — 컨텍스트 매니저로 자연스럽게 녹여낼 수 있습니다. 호출하는 쪽에서 복원하는 걸 깜빡할 일도 없고요.

예외 삼키기(Suppressing)

__exit__ 메서드가 True를 반환하면 파이썬에게 "이 예외는 내가 처리했으니 무시해도 돼"라고 알리는 셈입니다. 자주 쓸 일은 없고 대개는 코드 냄새에 가깝지만, contextlib.suppress가 바로 이 방식으로 동작합니다:

main.py
Output
Click Run to see the output here.

suppress(FileNotFoundError)FileNotFoundError를 그냥 무시해 버립니다. 말 그대로 있어도 그만 없어도 그만인 작업, 즉 "한번 해보고 안 되면 말고" 식의 상황에 쓰세요. 아직 원인을 제대로 파악하지 못한 예외를 덮어버리는 용도로는 절대 쓰면 안 됩니다.

알아두면 좋은 다른 컨텍스트 매니저들

눈여겨보기 시작하면 표준 라이브러리 곳곳에서 컨텍스트 매니저를 발견할 수 있습니다:

import threading
from pathlib import Path

# Locks — guarantee release even if the critical section raises.
lock = threading.Lock()
with lock:
    ...

# tempfile — delete the temp file when you're done.
from tempfile import TemporaryDirectory
with TemporaryDirectory() as tmp:
    path = Path(tmp) / "scratch.txt"
    path.write_text("hello")

# Database connections — close the connection (or end the transaction).
import sqlite3
with sqlite3.connect(":memory:") as conn:
    conn.execute("CREATE TABLE t (x INTEGER)")

서드파티 라이브러리들도 같은 관례를 따릅니다. with something as x: 같은 코드를 보면 거의 예외 없이 "이 블록 안에서는 x를 쓰고, 끝나면 알아서 정리한다"는 뜻이라고 보면 됩니다.

with를 쓰지 않는 게 나은 경우

  • 초기화와 정리가 사실상 필요 없을 때. 딱히 이유도 없이 아무 코드나 컨텍스트 매니저로 감싸면 오히려 가독성만 떨어집니다.
  • 여러 구간에 걸쳐 자원을 유지해야 할 때. 긴 스크립트 전체 수명 동안 with 블록을 열어두면 정리 범위가 어디까지인지 오히려 흐려집니다. 이럴 땐 해당 자원을 소유하는 클래스를 만드는 편이 낫습니다.
  • 데코레이터가 더 어울릴 때. 재시도, 로깅, 시간 측정처럼 반복되는 패턴은 함수 안에 with ...:로 감싸는 것보다 함수 위에 @decorator로 붙이는 게 훨씬 자연스럽게 읽히기도 합니다. 호출 지점에서 어느 쪽이 더 깔끔한지 보고 고르면 됩니다.

대부분의 상황에서는 with가 정답입니다. 그렇지 않은 드문 경우는, 한 번 감을 잡고 나면 쉽게 알아볼 수 있습니다.

다음 장: 실제 파일 다루기

이제 with open(...) as f: 뒤에서 어떤 일이 벌어지는지 알게 됐습니다. 실전에서 with를 쓰는 상황의 90%가 바로 이 파일 처리입니다. 다음 장에서는 이 지식을 바탕으로 디스크에 있는 파일을 읽고, 쓰고, 다루는 방법을 직접 해봅니다.

자주 묻는 질문

with open은 정확히 어떤 일을 하나요?

with open(path) as f:를 쓰면 파일을 열어서 블록 안에서만 f로 쓸 수 있게 바인딩해 줍니다. 블록이 끝나는 순간 — 정상 종료든, 예외가 터져서 중간에 튕겨 나가든 — 파이썬이 알아서 파일을 닫아줍니다. f.close()를 직접 호출할 필요가 없고, with문이 닫히는 걸 보장해 줘요.

그냥 open()을 쓰면 안 되고 왜 with를 써야 하나요?

블록 중간에 예외가 터져도 with는 파일을 확실히 닫아주기 때문이에요. 그냥 open()만 쓰면 모든 분기에서 close()를 빠짐없이 호출해야 하는데, 에러 처리 경로까지 전부 챙기는 건 쉽지 않거든요. with가 더 짧고, 더 안전합니다.

하나의 with문으로 파일 여러 개를 동시에 열 수 있나요?

가능합니다. 컨텍스트 매니저를 쉼표로 나열하면 돼요: with open('a.txt') as a, open('b.txt') as b:. 진입할 때 두 파일이 모두 열리고, 종료 시에는 역순으로 닫힙니다. 여러 자원을 한꺼번에 다뤄야 할 때 with를 중첩하지 않아도 되니 훨씬 깔끔해요.

Coddy로 코딩 배우기

시작하기