Python에서 프로그램 종료 없이 전체 예외 트레이스백 출력하는 방법

1. 문제 정의

Python에서 예외를 발생시키지 않고 로그만 남기고 싶을 때가 많습니다. 많은 초보 개발자는 다음과 같이 try/except 블록을 작성합니다.

1
2
3
4
5
6
try:
    do_stuff()
except Exception as err:
    print(Exception, err)
    # 이 시점에는 예외 이름과 상세 메시지만 출력되고,
    # 전체 트레이스백(어느 파일의 몇 번째 줄에서 발생했는지)이 출력되지 않습니다.

이 코드는 프로그램을 종료하지 않는 점은 지켜지지만, 예외가 발생한 정확한 위치(파일명, 라인 번호, 호출 스택)를 알 수 없습니다. 디버깅에는 예외 이름과 메시지만으로는 부족합니다.

원하는 동작: try/except가 예외를 가로채지 않았을 때 Python이 출력하는 것과 똑같은 전체 트레이스백을 출력하면서도, 프로그램은 계속 실행되게 하는 것입니다.

2. 원인 탐구

print(Exception, err)만으로는 전체 트레이스백이 나오지 않을까요?

except Exception as err: 로 잡은 err 객체는 예외의 값(value) 만 담고 있습니다. 예외가 어느 모듈의 몇 번째 줄에서 발생했는지, 어떤 함수들을 거쳐 전파되었는지에 대한 정보는 예외 객체가 아니라 별도의 트레이스백(traceback) 객체에 들어 있습니다.

그래서 예외 이름과 값만 출력해서는 호출 스택 정보가 빠지게 됩니다. 전체 정보를 얻으려면 Python의 traceback 모듈을 사용해야 합니다.

가장 널리 알려진 방법이 traceback.print_exc()입니다.

1
2
3
4
5
6
import traceback

try:
    do_stuff()
except Exception:
    traceback.print_exc()

이 코드는 동작하고 전체 트레이스백을 출력합니다. 그런데 이 방법에도 숨은 함정이 있습니다.

3. 근본 원인 분석

traceback.print_exc()가장 최근에 발생한(마지막) 예외를 출력합니다. 문제는 except 블록 안에서 다른 작업을 하다가 또 다른 예외가 발생하거나, 예외를 다시 던지는(raise) 경우입니다.

실제로 그런 상황을 재현해 봅시다.

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

try:
    raise TypeError("Oups!")
except Exception, err:          # Python 2.x 문법 (Python 3은 except Exception as err:)
    try:
        raise TypeError("Again !?!")
    except:
        pass
    traceback.print_exc()

이 코드가 출력하는 결과는 기대와 다릅니다.

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

print_exc()except 블록 안에서 새로 던져진 TypeError("Again !?!"), 즉 마지막 예외를 출력한 것입니다. 원래 잡으려고 했던 첫 번째 TypeError("Oups!")의 트레이스백은 출력되지 않았습니다.

이처럼 traceback.print_exc()예외 체인에서 가장 늦게 발생한 예외만 보여주므로, “원본 예외"의 전체 트레이스백을 보존해야 하는 상황에서는 부적합합니다.

4. 코드 해결책

해결책: sys.exc_info()로 원본 예외 정보를 캐시하고 print_exception()으로 출력

원본 예외의 트레이스백을 확실히 보존하려면, 예외를 잡는 순간 sys.exc_info()로 예외 정보를 로컬 변수에 캐시한 다음, traceback.print_exception(*exc_info)로 출력하면 됩니다.

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

try:
    raise TypeError("Oups!")
except Exception, err:                    # Python 3: except Exception as err:
    try:
        exc_info = sys.exc_info()         # 원본 예외 정보를 로컬 변수에 캐시

        # 여기서 원하는 유용한 작업 수행 (예: 로깅, 정리)
        # 주의: 이 사이에 또 다른 예외가 발생해도 괜찮음
        try:
            raise TypeError("Again !?!")
        except:
            pass
    finally:
        # *원본* 예외의 트레이스백을 출력
        traceback.print_exception(*exc_info)
        del exc_info                      # 순환 참조 해제

이제 출력은 원래 잡으려던 예외를 올바르게 보여줍니다.

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

핵심 동작 원리

sys.exc_info()는 현재 처리 중인 예외에 대한 튜플 (exception_type, exception_value, traceback) 을 반환합니다. 이 3가지를 traceback.print_exception(type, value, traceback)에 그대로 넘기면(*exc_info 언패킹) 원본 예외의 전체 트레이스백이 출력됩니다.

반드시 기억해야 할 함정: 순환 참조 (circular reference)

sys.exc_info() 문서에 따르면, 예외를 처리하는 함수에서 트레이스백 반환 값을 로컬 변수에 할당하면 순환 참조가 발생합니다. 트레이스백 객체는 프레임의 로컬 변수를 참조하고, 그 로컬 변수(exc_info)가 다시 트레이스백을 참조하기 때문입니다.

이 순환 참조는 같은 함수나 트레이스백이 참조하는 로컬 변수들이 가비지 컬렉션(GC)되지 못하게 막습니다. 따라서 트레이스백이 더 이상 필요 없어졌다면, 로컬 변수에서 참조를 명시적으로 제거해야 합니다. 위 코드의 finally 블록에서 del exc_info로 정리하는 이유가 바로 이것입니다.

해결책 비교 디버깅 표

방법출력 내용함정적합한 상황
print(Exception, err)예외 이름 + 값만스택 정보 없음간단한 값 확인
traceback.print_exc()마지막 예외의 전체 트레이스백예외 체인에서 원본 예외를 놓칠 수 있음일반적인 단일 예외 처리
sys.exc_info() 캐시 + traceback.print_exception(*exc_info)원본 예외의 전체 트레이스백순환 참조 → del로 해제 필요원본 예외 보존이 중요한 다중 예외 처리

5. 향후 예방 조치

  1. 예외를 잡은 직후에 트레이스백을 출력하세요. except 블록 안에서 많은 작업을 하기 전에 sys.exc_info()를 캐시해 두면 예외 체인이 뒤섞이는 문제를 피할 수 있습니다.

  2. 원본 예외가 중요한 경우 print_exc() 대신 print_exception(*exc_info)를 사용하세요. 특히 로깅 과정에서 부수적인 예외가 발생할 수 있는 환경이라면 원본 예외를 캐시해 두는 것이 안전합니다.

  3. 캐시한 exc_info는 반드시 finally에서 del로 해제하세요. 순환 참조를 남기면 해당 함수의 로컬 변수들이 메모리에서 해제되지 않아, 장시간 실행되는 서버/스크립트에서 메모리 누수로 이어질 수 있습니다.

  4. 실제 운영 환경에서는 로깅 모듈을 활용하세요. logging 모듈의 logger.exception() 또는 logger.error(..., exc_info=True)는 트레이스백을 포함해 로그로 남겨주므로, 표준출력보다 유지보수에 유리합니다. (단, 여기서도 예외가 다중으로 발생하는 경우 원본 예외 보존에 주의해야 합니다.)

  5. sys.exc_info()가 아닌 except Exception as err:로 받은 예외 객체만으로 전체 트레이스백을 얻으려면, 예외 객체의 __traceback__ 속성을 활용하는 방법도 있지만, 이 역시 순환 참조 주의가 필요합니다. 가장 안전한 기본 패턴은 위에서 설명한 캐시 + print_exception 방식입니다.


출처