프로그램 종료 없이 전체 Python 예외 traceback 출력하기 — print_exc()의 함정과 print_exception() 활용
1. 문제 정의
Python으로 프로그램을 작성하다 보면 특정 코드가 실패해도 프로그램 전체는 종료시키지 않고, 에러의 전체 traceback을 로그로 남기고 싶은 상황이 자주 생깁니다.
아래처럼 try/except를 사용하면 예외를 잡을 수는 있지만, print(Exception, err)는 예외 클래스 이름과 메시지만 출력할 뿐 어디서 어떤 호출 경로를 통해 예외가 발생했는지 즉 전체 traceback을 보여주지 않습니다.
| |
목표: try/except가 예외를 가로채지 않았을 때 터미널에 출력되는 것과 동일한 전체 traceback을 출력하면서도, 프로그램은 종료하지 않는 것입니다.
2. 원인 탐구
왜 print(Exception, err)만으로는 부족할까요?
Python의 예외 객체(err)에는 일반적으로 에러 메시지 문자열만 담겨 있습니다. traceback(역추적) 정보는 예외 객체 자체에 문자열로 저장되는 것이 아니라, 예외가 발생한 시점의 **호출 스택 프레임(실행 컨텍스트)**과 연결되어 있습니다.
즉 “전체 traceback"을 출력하려면 다음 정보가 필요합니다.
| 정보 | 의미 | 검색 예시 |
|---|---|---|
type | 예외 타입 (예: TypeError) | TypeError: Oups! |
value | 예외 메시지 | Oups! |
traceback | 예외가 전파된 호출 경로 | File "t.py", line 6, in <module> |
이 세 가지를 한 번에 얻을 수 있는 함수가 sys.exc_info()입니다. 이 함수는 (type, value, traceback) 튜플을 반환합니다.
3. 근본 원인 분석
가장 흔히 쓰는 해결책은 traceback.print_exc()입니다. 하지만 이 함수에는 치명적인 함정이 있습니다.
traceback.print_exc()는 현재 처리 중인(마지막으로 발생한) 예외의 traceback만 출력합니다. 만약 예외를 처리하는 도중에 또 다른 예외가 발생한다면, 원래 예외의 traceback은 잃어버리게 됩니다.
| |
이 코드의 실제 출력을 보면 원래 예외(Oups!)가 아니라 두 번째 예외(Again !?!)의 traceback이 출력됩니다.
| |
이것이 근본 원인입니다. print_exc()는 sys.exc_info()의 현재 상태를 그대로 출력하기 때문에, 예외 처리 도중 예외가 바뀌면 원본 정보를 놓칩니다. 실제 프로덕션 코드에서는 예외 처리 로직 안에서 로깅, 정리, 추가 연산을 수행하다가 중첩 예외가 발생하는 경우가 흔하므로 이 문제를 반드시 인지해야 합니다.
4. 코드 해결책
해결 방법은 명확합니다. 예외 정보를 처음 잡은 시점에 지역 변수로 캐시해 두고, 이후에는 캐시된 값을 traceback.print_exception()으로 출력하면 됩니다.
| |
이 코드는 중간에 두 번째 예외(Again !?!)가 발생했음에도 원래 예외의 traceback을 올바르게 출력합니다.
| |
주의할 점 — 순환 참조(circular reference)
sys.exc_info()의 traceback 객체는 예외 처리 중인 함수의 지역 변수와 순환 참조를 만들 수 있습니다. 공식 문서에 따르면, 예외를 처리하는 함수에서 traceback 반환 값을 지역 변수에 할당하면 순환 참조가 발생할 수 있으며, 이로 인해 해당 traceback이 참조하는 객체가 가비지 컬렉션되지 않고 메모리에 남을 수 있습니다.
따라서 traceback을 더 이상 사용하지 않는 시점에 반드시 del exc_info(또는 del traceback)로 참조를 명시적으로 해제해야 합니다. finally 블록에서 정리하는 것이 안전한 패턴입니다.
| 상황 | 사용 함수 | 결과 |
|---|---|---|
| 단순히 마지막 예외의 traceback만 필요할 때 | traceback.print_exc() | 간단하지만 중첩 예외 시 원본 손실 |
| 원본 예외의 traceback을 반드시 보존해야 할 때 | sys.exc_info() 캐시 + traceback.print_exception() | 안전, 순환 참조 해제 필수 |
| traceback 문자열을 변수로 저장하고 싶을 때 | traceback.format_exception(*exc_info) | 로그 파일/문자열 저장에 유용 |
예외 traceback을 로그 파일 등에 문자열로 저장해야 한다면 traceback.format_exception(*exc_info)로 문자열을 얻을 수 있습니다. 이 역시 동일하게 exc_info 캐시 방식과 함께 사용합니다.
5. 향후 예방 조치
같은 문제를 반복하지 않기 위한 실용적인 예방 조치입니다.
print_exc()의 한계를 기억하세요. 예외 처리 블록 안에서 추가 작업(중첩 호출, 로깅, 정리)을 수행한다면print_exc()는 원본 traceback을 보장하지 않습니다.예외 처리 첫 줄에서
sys.exc_info()를 캐시하고, 이후에는 반드시 캐시된 값을 사용하세요. 이렇게 하면 예외 처리 로직이 아무리 복잡해져도 원본 정보가 보존됩니다.del exc_info로 순환 참조를 해제하세요. traceback을 더 이상 쓰지 않는 즉시 해제하는 것이 메모리 누수를 막는 안전한 습관입니다. 필요하다면finally블록에서 정리하세요.문자열로 저장해야 한다면
format_exception()을 사용하세요. 로그 파일에 남기는 용도라면format_exception(*exc_info)로 문자열을 얻는 것이sys.stderr에 출력하는 것보다 유연합니다.함수로 캡슐화해서 재사용하면 잊지 않고 일관되게 적용할 수 있습니다. 예외 정보를 받아 로깅하는 공용 유틸리티를 만들어 두면 모든
except블록에서 동일한 패턴을 강제할 수 있습니다.
출처: StackOverflow — Catch and print full Python exception traceback without halting/exiting the program