[파이썬 100강] 86강. traceback으로 예외 원인을 빠르게 추적하고 운영 로그 품질 높이기
에러가 났을 때 except Exception as e: print(e)만 찍으면, "무슨 에러인지는 알겠는데 어디서 왜 터졌는지"를 놓치기 쉽습니다. 이번 강의에서는 traceback 모듈로 호출 흐름을 텍스트로 남기고, 운영 환경에서 재현 가능한 디버깅 단서를 확보하는 방법을 바로 예제로 정리합니다.
핵심 개념
traceback은 예외 발생 시점의 호출 스택(call stack) 정보를 문자열/리스트 형태로 다루게 해주는 표준 라이브러리입니다.traceback.format_exc()는 현재 처리 중인 예외의 전체 트레이스백을 문자열로 반환해 로그 저장에 유리합니다.traceback.format_exception()/extract_tb()를 쓰면 출력 포맷을 세밀하게 제어할 수 있어, "사용자에게는 짧게, 내부 로그에는 자세히" 같은 이중 전략이 가능합니다.
디버깅 시간을 줄이는 팀은 보통 "에러가 났다"보다 "어떤 경로로 여기까지 왔는지"를 더 중요하게 봅니다. 예외 메시지 한 줄은 증상이고, 트레이스백은 맥락입니다. 특히 함수가 여러 단계를 거치거나 외부 API/파일 입출력이 섞이면, 실제 원인은 에러가 보이는 줄보다 위쪽 호출 단계에 숨어 있는 경우가 많습니다. traceback을 쓰면 이 호출 체인을 그대로 복원해 텍스트로 남길 수 있습니다. 또한 운영 환경에서는 개인정보나 토큰이 로그에 섞일 수 있으므로, 트레이스백을 남기되 사용자 노출 메시지와 내부 보관 로그를 분리하는 습관이 중요합니다. 오늘 목표는 "원인 파악 속도"와 "운영 안전성"을 동시에 확보하는 것입니다.
기본 사용
예제 1) format_exc()로 전체 오류 흐름 남기기
>>> import traceback
>>>
>>> def divide(a, b):
... return a / b
...
>>> try:
... divide(10, 0)
... except Exception:
... err_text = traceback.format_exc()
... print("=== captured traceback ===")
... print(err_text.splitlines()[-1])
...
=== captured traceback ===
ZeroDivisionError: division by zero
해설:
format_exc()는 현재except블록의 예외를 문자열로 반환합니다.- 실제 운영에서는
print대신 파일 로그/중앙 로그 시스템으로 전달하면 됩니다. - 마지막 줄만 뽑으면 에러 타입 요약(
ZeroDivisionError)을 빠르게 확인할 수 있습니다.
예제 2) format_exception()으로 구조화된 로그 만들기
>>> import traceback
>>>
>>> def parse_user_age(text):
... return int(text)
...
>>> try:
... parse_user_age("twenty")
... except Exception as exc:
... lines = traceback.format_exception(type(exc), exc, exc.__traceback__)
... print(lines[0].strip())
... print(lines[-1].strip())
...
Traceback (most recent call last):
ValueError: invalid literal for int() with base 10: 'twenty'
해설:
format_exception()은 라인 리스트를 반환해 후처리(필터링/마스킹/요약)에 유리합니다.- 예를 들어 첫 줄은 고정 헤더, 마지막 줄은 에러 요약으로 분리 저장할 수 있습니다.
예제 3) extract_tb()로 프레임 단위 분석하기
>>> import traceback
>>>
>>> def layer3():
... return 1 / 0
...
>>> def layer2():
... return layer3()
...
>>> def layer1():
... return layer2()
...
>>> try:
... layer1()
... except Exception as exc:
... frames = traceback.extract_tb(exc.__traceback__)
... print(len(frames) >= 3)
... print(frames[-1].name, frames[-1].lineno)
...
True
layer3 4
해설:
- 프레임 리스트를 얻으면 "어느 함수에서 최종 실패했는지"를 코드로 판별할 수 있습니다.
- 장애 통계 시
frames[-1].name기반으로 실패 지점을 집계하면 반복 이슈 파악이 쉬워집니다.
자주 하는 실수
실수 1) except 밖에서 format_exc() 호출하기
>>> import traceback
>>> traceback.format_exc()
'NoneType: None\n'
원인:
- 처리 중인 활성 예외가 없으면
format_exc()는 의미 있는 트레이스백을 만들 수 없습니다. - "나중에 필요할 때 찍어야지" 하고 예외 컨텍스트를 벗어나면 정보가 사라집니다.
해결:
>>> import traceback
>>> try:
... int("x")
... except Exception:
... saved_tb = traceback.format_exc()
...
>>> "ValueError" in saved_tb
True
핵심은 예외를 잡는 즉시 저장하는 것입니다.
실수 2) 사용자에게 내부 트레이스백을 그대로 노출하기
- 증상: API 응답/웹 화면에 내부 파일 경로, 함수명, 구현 세부가 그대로 출력됩니다.
- 원인: 디버깅 편의 때문에
traceback.format_exc()문자열을 사용자 응답에 재사용합니다. - 해결: 사용자 응답은 짧고 안전한 메시지로 고정하고, 상세 트레이스백은 내부 로그에만 남깁니다.
>>> def public_error_message():
... return "요청 처리 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요."
...
>>> public_error_message()
'요청 처리 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.'
실수 3) 로그를 남겼지만 요청 맥락(request_id)을 함께 저장하지 않기
- 증상: 트레이스백은 많은데, 어떤 사용자 요청과 연결되는지 찾기 어렵습니다.
- 원인: 예외 문자열만 저장하고 요청 ID/입력 요약/시간대를 함께 기록하지 않았습니다.
- 해결: 트레이스백 저장 시 메타데이터를 같은 레코드로 묶어 저장합니다.
>>> log_record = {
... "request_id": "req-20260217-1405",
... "endpoint": "/users/create",
... "error_type": "ValueError",
... }
>>> sorted(log_record.keys())
['endpoint', 'error_type', 'request_id']
실무 패턴
- 입력 검증 규칙: 예외를 "막연히 catch"하기 전에 경계값 검증(빈 문자열, 형식 오류, 범위 초과)을 먼저 수행해 예상 가능한 실패를 줄입니다.
- 로그/예외 처리 규칙: 사용자 메시지와 내부 로그를 분리합니다. 사용자에게는 안전한 일반 문구, 내부에는
traceback전체 + request_id + 주요 파라미터 스냅샷을 남깁니다. - 재사용 함수/구조화 팁:
capture_exception(exc, context)같은 공용 함수 하나로 트레이스백 포맷을 통일합니다. 팀마다 출력 형식이 제각각이면 검색/집계가 어려워집니다. - 성능/메모리 체크 포인트: 트레이스백 문자열은 길 수 있으므로 대량 장애 시 로그 폭주가 발생할 수 있습니다. 동일 에러는 샘플링하거나, 동일 스택 해시 단위로 집계 저장하는 전략이 유효합니다.
운영 경험상 중요한 점은 "많이 남기는 것"보다 "다시 찾을 수 있게 남기는 것"입니다. 예를 들어 traceback 전체를 남겨도 request_id가 없으면 재현이 느려집니다. 반대로 request_id만 있어도 스택 정보가 없으면 원인 지점 특정이 지연됩니다. 결국 에러 타입 + 스택 + 맥락 메타데이터 3종 세트를 같이 저장하는 것이 가장 실용적입니다. 또한 보안 관점에서 쿼리 문자열, 토큰, 비밀번호 같은 민감 값이 스택/로컬 변수 덤프에 섞일 수 있으니 마스킹 정책을 반드시 함께 설계하세요.
오늘의 결론
한 줄 요약: traceback은 "에러 메시지 한 줄"을 "재현 가능한 원인 기록"으로 바꾸는 가장 기본적인 도구입니다.
기억할 것:
format_exc()는except안에서 즉시 저장해야 의미가 있습니다.- 사용자 노출 메시지와 내부 디버깅 로그는 반드시 분리하세요.
- request_id 같은 맥락 정보와 함께 저장해야 운영 대응 속도가 빨라집니다.
연습문제
safe_execute(func, *args)함수를 만들어 예외 발생 시traceback.format_exc()결과를 문자열로 반환하고, 정상 시 결과값을 반환해 보세요.traceback.extract_tb()를 사용해 마지막 프레임의 함수명과 라인 번호만 뽑아{"func": ..., "line": ...}딕셔너리로 만드는 코드를 작성해 보세요.- "사용자 응답 메시지"와 "내부 로그 레코드"를 분리하는 에러 처리 템플릿을 함수 형태로 설계해 보세요. 내부 로그에는 request_id를 포함해야 합니다.
이전 강의 정답
reprlib.Repr()정책 적용 후 중첩 딕셔너리 출력
>>> import reprlib
>>> r = reprlib.Repr()
>>> r.maxstring = 30
>>> r.maxlist = 4
>>> r.maxlevel = 2
>>> data = {"team": ["backend", "frontend", "data", "infra", "qa"], "meta": {"owner": "geonwoo", "env": ["dev", "staging", "prod"]}}
>>> print(r.repr(data))
{'meta': {'env': [...], 'owner': 'geonwoo'}, 'team': ['backend', 'frontend', 'data', 'infra', ...]}
설명: 리스트는 4개까지만 보이고 나머지는 ...로 축약됩니다. 중첩 레벨도 제한되어 깊은 구조가 과도하게 펼쳐지지 않습니다.
- 민감 키 마스킹 후 축약 문자열 반환하는
safe_repr(obj)
>>> import reprlib
>>> def safe_repr(obj):
... r = reprlib.Repr()
... r.maxstring = 40
... r.maxlist = 5
... if isinstance(obj, dict):
... masked = {k: ("***" if k in {"token", "password"} else v) for k, v in obj.items()}
... return r.repr(masked)
... return r.repr(obj)
...
>>> safe_repr({"user": "geonwoo", "token": "abcd-very-secret-token", "password": "p@ss", "roles": ["admin", "editor", "viewer", "ops", "audit", "guest"]})
"{'password': '***', 'roles': ['admin', 'editor', 'viewer', 'ops', 'audit', ...], 'token': '***', 'user': 'geonwoo'}"
설명: 노출되면 안 되는 키를 먼저 치환하고, 그다음 축약을 적용하면 보안성과 가독성을 동시에 확보할 수 있습니다.
pformat(data)vsreprlib.repr(data)사용 상황
>>> decision = {
... "pformat": "데이터 구조를 자세히 읽어야 하는 개발/분석 단계",
... "reprlib": "운영 로그에서 길이 폭주를 막아야 하는 단계"
... }
>>> decision["pformat"]
'데이터 구조를 자세히 읽어야 하는 개발/분석 단계'
>>> decision["reprlib"]
'운영 로그에서 길이 폭주를 막아야 하는 단계'
설명: pformat은 가독성 중심(상세 확인), reprlib은 안정성 중심(길이 제한)입니다. 운영에서는 둘을 상황별로 병행하는 것이 가장 현실적입니다.
실습 환경/재현 정보
- 실행 환경:
condaenvpython100(Python 3.11.14) - 가정한 OS: macOS/Linux 공통
- 예제 검증 방식: Python REPL(pycon) 기준으로 동작 확인