
Python ExitStack 사용법
with 문을 여러 개 중첩해야 하는데 개수가 입력값에 따라 달라지거나, 컨텍스트 매니저가 아닌 정리 함수까지 함께 묶어야 할 때가 있다. 2026년 7월 6일 기준 Python 3.14.6 공식 문서를 보면 contextlib.ExitStack은 이런 상황에서 여러 컨텍스트 매니저와 cleanup callback을 프로그램적으로 결합하도록 설계된 도구다.
실무에서는 "리소스 개수가 가변적인가", "정리만 필요한 콜백도 함께 등록해야 하는가", "동기와 비동기를 섞어야 하는가"를 먼저 나누면 선택이 빨라진다. 이 글은 ExitStack, AsyncExitStack, enter_context(), push(), callback(), pop_all()의 차이와 무난한 사용 기준만 정리한다.
먼저 무엇이 다를까?
| 도구 | 역할 | 무난한 사용 상황 |
|---|---|---|
ExitStack |
동기 컨텍스트 매니저와 정리 콜백을 하나의 스택으로 관리 | 열 파일 수, 락 수, 옵션 리소스 개수가 실행 중 달라질 때 |
AsyncExitStack |
비동기 컨텍스트 매니저와 코루틴 정리를 포함한 비동기 스택 관리 | async with 안에서 연결 여러 개를 열고 함께 닫아야 할 때 |
enter_context(cm) |
컨텍스트 매니저를 즉시 열고 __exit__()를 스택에 등록 |
일반 with를 반복문으로 확장하고 싶을 때 |
push(exit) |
__enter__() 없이 __exit__() 또는 exit 스타일 콜백만 등록 |
__enter__() 도중 실패 가능성이 있어 부분 정리가 필요할 때 |
callback(fn, ...) |
임의의 정리 함수를 등록 | 컨텍스트 매니저가 아닌 release 함수만 있을 때 |
pop_all() |
등록된 정리 스택을 새 ExitStack으로 이전 |
모두 성공한 뒤에는 자동 정리를 넘기고 싶을 때 |
ExitStack은 언제 쓰면 될까?
공식 문서는 ExitStack의 대표 용도로 "가변 개수의 컨텍스트 매니저를 하나의 with 문으로 다루는 경우"를 제시한다. 예를 들어 파일 목록 길이가 실행마다 다르면 정적인 with open(...), open(...) 형태보다 ExitStack이 더 직접적이다.
또 한 가지 장점은 정리 순서가 명확하다는 점이다. 각 콜백은 등록의 역순으로 실행되므로, 여러 with 문을 중첩한 것과 비슷한 정리 모델을 유지할 수 있다. 공식 문서도 예외가 바뀌거나 억제되면 바깥쪽 콜백은 그 갱신된 상태를 기준으로 호출된다고 설명한다.
from contextlib import ExitStack
filenames = ["a.txt", "b.txt", "c.txt"]
with ExitStack() as stack:
files = [stack.enter_context(open(name, "r", encoding="utf-8"))
for name in filenames]
first_lines = [f.readline().strip() for f in files]
enter_context()와 push()는 왜 구분될까?
enter_context(cm)는 일반적인 경우의 기본값이다. 공식 문서 기준 이 메서드는 컨텍스트 매니저의 __enter__()를 실행하고, 반환값을 돌려주며, 대응되는 __exit__()를 스택에 등록한다. 3.11부터는 컨텍스트 매니저가 아닌 값을 넘기면 AttributeError가 아니라 TypeError를 올린다.
반면 push()는 __enter__()를 호출하지 않는다. 그래서 리소스를 일부 확보한 뒤 나머지 검증이 실패할 수 있는 __enter__() 구현 내부나, 이미 열린 객체의 exit 동작만 나중에 묶고 싶을 때 더 적합하다. 공식 문서도 이 메서드를 "부분적으로 진행된 __enter__() 구현을 정리하는 용도"로 설명한다.
callback()은 무엇이 다를까?
callback()은 컨텍스트 매니저가 아닌 일반 함수도 정리 스택에 올릴 수 있게 해 준다. 선택적으로 생성한 임시 디렉터리 삭제 함수, 별도 API의 release 함수, 수동으로 열어 둔 핸들 정리 같은 흐름에서 유용하다.
다만 공식 문서는 callback()으로 등록한 함수는 예외 상세 정보를 받지 않기 때문에 예외를 억제할 수 없다고 명시한다. 예외를 보고 suppress 여부를 결정해야 한다면 callback()보다 push()로 exit 시그니처를 따르는 콜백을 등록하는 편이 맞다.
from contextlib import ExitStack
def release_resource(resource):
resource.close()
with ExitStack() as stack:
resource = acquire_resource()
stack.callback(release_resource, resource)
use(resource)
pop_all()은 언제 필요할까?
pop_all()은 "중간에 하나라도 실패하면 지금까지 연 것들을 자동으로 닫되, 모두 성공했다면 열린 상태를 유지하고 싶다"는 요구에 맞는다. 공식 문서는 여러 파일을 all-or-nothing 방식으로 여는 예제를 통해, 기존 스택의 정리 콜백을 새 ExitStack으로 옮긴 뒤 나중에 명시적으로 close()하게 할 수 있다고 설명한다.
즉, 자원 확보 단계에서는 실패 복구를 자동화하고, 성공 이후의 생명주기는 호출자가 넘겨받는 패턴이다. 이때 pop_all() 자체는 콜백을 실행하지 않고 이전만 수행한다.
AsyncExitStack은 언제 고르면 될까?
비동기 코드에서는 정리 단계도 await가 필요할 수 있다. 공식 문서 기준 AsyncExitStack은 동기·비동기 컨텍스트 매니저를 함께 다루고, 코루틴 기반 cleanup도 등록할 수 있도록 만든 비동기 버전이다. 닫기 메서드는 close()가 아니라 aclose()다.
예를 들어 비동기 데이터베이스 연결이나 네트워크 세션을 여러 개 열고 일부만 선택적으로 추가하는 흐름이라면 async with AsyncExitStack()이 더 자연스럽다. 비동기 컨텍스트 매니저는 enter_async_context(), 코루틴 cleanup은 push_async_callback() 같은 전용 메서드로 등록한다.
from contextlib import AsyncExitStack
async with AsyncExitStack() as stack:
connections = [
await stack.enter_async_context(get_connection())
for _ in range(3)
]
await run_query(connections)
운영에서 무난한 선택 기준
| 상황 | 무난한 선택 | 이유 |
|---|---|---|
| 컨텍스트 매니저 개수가 실행 중 달라진다 | ExitStack |
반복문과 조건문으로 with 구성을 만들 수 있음 |
| 정리 함수만 있고 컨텍스트 매니저는 없다 | callback() |
임의 함수와 인자를 그대로 등록할 수 있음 |
| 예외 억제 가능성이 있는 exit 로직을 붙여야 한다 | push() |
exit 시그니처를 따라 예외 정보를 받을 수 있음 |
| 모두 성공한 뒤 자원 닫기를 호출자에게 넘기고 싶다 | pop_all() |
현재 스택을 새 ExitStack으로 이전할 수 있음 |
| 비동기 컨텍스트와 코루틴 정리를 함께 관리한다 | AsyncExitStack |
aclose()와 async 전용 등록 메서드를 제공함 |
자주 놓치는 점
공식 문서는 등록된 콜백이 스택이 가비지 컬렉션될 때 자동 호출되지는 않는다고 분명히 적고 있다. 따라서 ExitStack를 단순히 변수에만 담아 두고 컨텍스트 종료나 close() 호출을 생략하면 정리가 보장되지 않는다.
또 하나는 재사용성과 재진입성의 차이다. 공식 문서 예시 기준 하나의 ExitStack 인스턴스는 여러 번 다시 사용할 수는 있지만, 같은 인스턴스를 중첩해서 쓰면 안쪽 with 종료 시점에 스택이 비워져 바깥쪽 정리 흐름이 깨질 수 있다. 중첩이 필요하면 새 인스턴스를 따로 만드는 편이 안전하다.
자주 헷갈리는 질문
Q. 파일 여러 개를 여는 정도면 꼭 ExitStack이 필요할까?
항상 그렇지는 않다. 개수가 고정돼 있다면 일반 with 문이 더 단순하다. ExitStack은 개수가 가변적이거나, 조건부 자원과 일반 cleanup 함수를 함께 묶어야 할 때 장점이 커진다.
Q. callback()으로 등록한 함수도 예외를 막을 수 있을까?
그렇지 않다. 공식 문서는 callback()으로 등록한 함수는 예외 세부 정보를 받지 않으므로 예외를 suppress할 수 없다고 설명한다. 그런 동작이 필요하면 push() 쪽을 봐야 한다.
Q. 같은 ExitStack 객체를 바깥 with와 안쪽 with에서 같이 써도 될까?
권장되지 않는다. 공식 문서 예시는 같은 인스턴스를 중첩 사용하면 안쪽 블록 종료 시 스택이 비워져 의도하지 않은 결과가 나온다고 설명한다. 중첩 구조라면 별도 인스턴스를 만들어야 한다.
정리
ExitStack은 "가변적인 with"를 안전하게 표현하는 도구로 이해하면 가장 실용적이다. 기본값은 enter_context()이고, 일반 정리 함수는 callback(), exit 시그니처가 필요한 정리는 push(), 성공 후 소유권 이전은 pop_all()로 나누면 판단이 빨라진다.
2026년 7월 6일 기준 Python 3.14.6 공식 문서만 놓고 보면, 동기 자원 정리는 ExitStack, 비동기 자원 정리는 AsyncExitStack으로 시작하는 편이 무난하다. 다만 자원 개수가 고정돼 있다면 일반 with 문이 여전히 가장 읽기 쉬운 선택이다.
참고 자료
'프로그래밍 > C, C++, Java, Python' 카테고리의 다른 글
| Python functools.Placeholder 사용법 (0) | 2026.07.10 |
|---|---|
| Python weakref.finalize 사용법 (0) | 2026.07.08 |
| Python asyncio timeout 기준 정리 (0) | 2026.07.01 |
| Python tarfile 추출 필터 기준 정리 (0) | 2026.06.25 |
| Python tempfile 사용법 (1) | 2026.06.19 |





