파이썬 콜 스택과 트레이스백 읽기
파이썬 콜 스택의 동작 원리, 트레이스백 해석 방법, traceback·inspect·sys 모듈로 스택 프레임에 프로그래밍으로 접근하는 방법을 설명합니다.
지난 글에서 함수 어노테이션을 살펴봤습니다. 이번에는 함수 호출이 실행 엔진 내부에서 어떻게 쌓이는지, 오류가 났을 때 트레이스백을 어떻게 읽는지 파고듭니다. 트레이스백을 제대로 읽을 수 있어야 디버깅이 빠릅니다.
콜 스택이란
파이썬 인터프리터는 함수를 호출할 때마다 스택 프레임(stack frame) 을 하나씩 쌓습니다. 프레임에는 해당 함수의 지역 변수, 현재 실행 위치(코드 라인), 반환 주소가 담깁니다. 함수가 반환되면 해당 프레임은 제거됩니다.
이 구조가 LIFO(Last In, First Out) 스택이기 때문에 콜 스택이라 부릅니다.
def c():
return 1 / 0 # ZeroDivisionError 발생
def b():
return c()
def a():
return b()
a()
실행 순서를 따라가면 스택은 아래처럼 쌓입니다.
[module] → [a] → [a, b] → [a, b, c] → 예외 발생
트레이스백 읽는 법
트레이스백(traceback)은 콜 스택의 스냅샷입니다. 아래에서 위로 읽는 것이 핵심입니다.
Traceback (most recent call last):
File "app.py", line 8, in <module>
a()
File "app.py", line 6, in a
return b()
File "app.py", line 4, in b
return c()
File "app.py", line 2, in c
return 1 / 0
ZeroDivisionError: division by zero
- 마지막 줄 — 예외 타입과 메시지. 가장 먼저 확인
- 그 바로 위 두 줄 — 실제 오류가 발생한 파일·라인·코드
- 그 위 줄들 — 오류로 이어진 호출 경로 (내 코드 → 라이브러리 경계 확인)
라이브러리 깊숙이 들어간 트레이스백은 대부분 내 코드의 마지막 진입 지점이 핵심입니다. 그 아래는 라이브러리 내부 경로이므로 빠르게 건너뛰면 됩니다.
체인 예외 트레이스백
try:
int("abc")
except ValueError as e:
raise RuntimeError("변환 실패") from e
Traceback (most recent call last):
File "...", line 2, in <module>
int("abc")
ValueError: invalid literal for int() with base 10: 'abc'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "...", line 4, in <module>
raise RuntimeError("변환 실패") from e
RuntimeError: 변환 실패
두 트레이스백이 “The above exception…” 구분자로 연결됩니다. 원인 예외가 위에, 결과 예외가 아래에 표시됩니다.
traceback 모듈
traceback 모듈은 트레이스백을 프로그래밍으로 처리합니다.
import traceback
import logging
logger = logging.getLogger(__name__)
def safe_execute(func, *args):
try:
return func(*args)
except Exception:
# 문자열로 변환해 로그에 기록
tb_str = traceback.format_exc()
logger.error("오류 발생:\n%s", tb_str)
return None
# 현재 스택 트레이스 출력 (예외 없이도)
traceback.print_stack()
# 예외 정보만 추출
try:
1 / 0
except ZeroDivisionError:
exc_type, exc_value, exc_tb = sys.exc_info()
frames = traceback.extract_tb(exc_tb)
for frame in frames:
print(f"{frame.filename}:{frame.lineno} in {frame.name}")
print(f" {frame.line}")
inspect 모듈 — 런타임 스택 탐색
import inspect
def current_function_name() -> str:
return inspect.currentframe().f_code.co_name
def caller_info() -> tuple[str, int]:
stack = inspect.stack()
caller_frame = stack[1] # 0=현재, 1=호출자
return caller_frame.function, caller_frame.lineno
def show_call_chain():
for frame_info in inspect.stack():
print(f"{frame_info.filename}:{frame_info.lineno} in {frame_info.function}")
inspect.stack() 은 프레임 객체 리스트를 반환합니다. 각 프레임에는 파일명, 라인 번호, 함수명, 소스 코드 컨텍스트가 있습니다.
프레임 객체 직접 다루기
import sys
def inspect_locals():
frame = sys._getframe(0) # 현재 프레임
print("함수명:", frame.f_code.co_name)
print("라인번호:", frame.f_lineno)
print("지역변수:", frame.f_locals)
print("전역변수 키:", list(frame.f_globals.keys())[:5])
x = 42
y = "hello"
inspect_locals()
# 함수명: inspect_locals
# 라인번호: 3
# 지역변수: {} ← 함수 진입 직후라 아직 없음
sys._getframe(n) 에서 n 은 스택 깊이입니다. 0이 현재, 1이 호출자, 2가 그 위입니다. 내부 API이므로 inspect.stack() 이 더 안전합니다.
재귀 깊이 초과 트레이스백 해석
def infinite():
return infinite()
infinite()
RecursionError: maximum recursion depth exceeded
이 경우 트레이스백이 수백~수천 줄로 출력됩니다. 반복되는 패턴이 보이면 재귀 루프가 원인입니다. sys.setrecursionlimit() 으로 한계를 높이는 것은 임시방편이며, 재귀를 반복문으로 바꾸거나 기저 조건을 점검하는 게 올바른 해결책입니다.
커스텀 예외 훅
import sys
import traceback
def custom_excepthook(exc_type, exc_value, exc_tb):
print("=== 예외 발생 ===")
traceback.print_exception(exc_type, exc_value, exc_tb)
print("=================")
# 여기서 Sentry, 로그 등에 기록 가능
sys.excepthook = custom_excepthook
sys.excepthook 을 교체하면 처리되지 않은 예외가 발생할 때마다 커스텀 로직을 실행할 수 있습니다. 모니터링 시스템 연동에 유용합니다.
핵심 정리
- 콜 스택 = 함수 호출마다 쌓이는 LIFO 프레임 구조
- 트레이스백은 아래에서 위로 — 마지막 줄이 실제 원인
traceback.format_exc()로 트레이스백을 문자열로 기록inspect.stack()으로 런타임에 호출 경로 탐색sys.excepthook으로 전역 예외 처리 훅 등록
지난 글: 파이썬 함수 어노테이션 완전 가이드
다음 글: 순수 함수 vs 부수 효과 — 파이썬에서의 함수형 사고
읽어주셔서 감사합니다. 😊