프로그램 종료 없이 전체 Python 예외 traceback 출력하기 — print_exc()의 함정과 print_exception() 활용

1. 문제 정의

Python으로 프로그램을 작성하다 보면 특정 코드가 실패해도 프로그램 전체는 종료시키지 않고, 에러의 전체 traceback을 로그로 남기고 싶은 상황이 자주 생깁니다.

아래처럼 try/except를 사용하면 예외를 잡을 수는 있지만, print(Exception, err)는 예외 클래스 이름과 메시지만 출력할 뿐 어디서 어떤 호출 경로를 통해 예외가 발생했는지 즉 전체 traceback을 보여주지 않습니다.

1
2
3
4
5
6
try:
    do_stuff()
except Exception as err:
    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은 잃어버리게 됩니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import traceback

try:
    raise TypeError("Oups!")
except Exception as err:
    try:
        raise TypeError("Again !?!")
    except:
        pass
    traceback.print_exc()

이 코드의 실제 출력을 보면 원래 예외(Oups!)가 아니라 두 번째 예외(Again !?!)의 traceback이 출력됩니다.

1
2
3
4
Traceback (most recent call last):
  File "e.py", line 7, in <module>
    raise TypeError("Again !?!")
TypeError: Again !?!

이것이 근본 원인입니다. print_exc()sys.exc_info()현재 상태를 그대로 출력하기 때문에, 예외 처리 도중 예외가 바뀌면 원본 정보를 놓칩니다. 실제 프로덕션 코드에서는 예외 처리 로직 안에서 로깅, 정리, 추가 연산을 수행하다가 중첩 예외가 발생하는 경우가 흔하므로 이 문제를 반드시 인지해야 합니다.

4. 코드 해결책

해결 방법은 명확합니다. 예외 정보를 처음 잡은 시점에 지역 변수로 캐시해 두고, 이후에는 캐시된 값을 traceback.print_exception()으로 출력하면 됩니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import traceback
import sys

try:
    raise TypeError("Oups!")
except Exception as err:
    # 1) 예외 처리 첫 줄에서 exc_info를 캐시한다
    exc_info = sys.exc_info()
    # 이 사이에서 유용한 작업 수행 (예외가 발생해도 무방)
    try:
        raise TypeError("Again !?!")
    except:
        pass
    # 2) 원본 예외의 traceback을 출력한다
    traceback.print_exception(*exc_info)
    # 3) 순환 참조 방지를 위해 캐시를 해제한다
    del exc_info

이 코드는 중간에 두 번째 예외(Again !?!)가 발생했음에도 원래 예외의 traceback을 올바르게 출력합니다.

1
2
3
4
Traceback (most recent call last):
  File "t.py", line 6, in <module>
    raise TypeError("Oups!")
TypeError: Oups!

주의할 점 — 순환 참조(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. 향후 예방 조치

같은 문제를 반복하지 않기 위한 실용적인 예방 조치입니다.

  1. print_exc()의 한계를 기억하세요. 예외 처리 블록 안에서 추가 작업(중첩 호출, 로깅, 정리)을 수행한다면 print_exc()는 원본 traceback을 보장하지 않습니다.

  2. 예외 처리 첫 줄에서 sys.exc_info()를 캐시하고, 이후에는 반드시 캐시된 값을 사용하세요. 이렇게 하면 예외 처리 로직이 아무리 복잡해져도 원본 정보가 보존됩니다.

  3. del exc_info로 순환 참조를 해제하세요. traceback을 더 이상 쓰지 않는 즉시 해제하는 것이 메모리 누수를 막는 안전한 습관입니다. 필요하다면 finally 블록에서 정리하세요.

  4. 문자열로 저장해야 한다면 format_exception()을 사용하세요. 로그 파일에 남기는 용도라면 format_exception(*exc_info)로 문자열을 얻는 것이 sys.stderr에 출력하는 것보다 유연합니다.

  5. 함수로 캡슐화해서 재사용하면 잊지 않고 일관되게 적용할 수 있습니다. 예외 정보를 받아 로깅하는 공용 유틸리티를 만들어 두면 모든 except 블록에서 동일한 패턴을 강제할 수 있습니다.


출처: StackOverflow — Catch and print full Python exception traceback without halting/exiting the program