print 디버깅의 함정과 졸업
print 디버깅이 왜 자주 우리를 배신하는지 — 지우는 걸 잊고, 위치를 모르고, 켜고 끌 수 없고, 버퍼링에 묻히는 네 가지 함정과, 더 나은 대안으로 넘어가는 법을 정리합니다.
지난 글에서 breakpoint()로 디버거를 부르는 표준 방법을 정리했다. 그런데 솔직해지자. 디버거를 알면서도 우리는 여전히 print를 가장 먼저 집어 든다. 빠르고, 아무것도 외울 필요 없고, 어디서나 통하기 때문이다. print 디버깅 자체가 죄는 아니다. 다만 그것이 자주 우리를 조용히 배신한다는 사실을 알아야 한다. 오늘은 print 디버깅이 새는 네 군데를 짚고, 같은 일을 더 안전하게 하는 법을 본다.
네 가지 함정
print로 디버깅할 때 우리는 대개 같은 방식으로 당한다. 문제를 추적하느라 출력문을 잔뜩 흩뿌리고, 문제를 고친 뒤에는 그것들이 어디 있었는지조차 잊는다.
첫째, 지우는 걸 잊는다. 디버깅용 출력이 커밋에 섞여 들어가 운영 로그를 어지럽힌다. 둘째, 어디서 찍혔는지 모른다. 42라는 숫자 하나만 덜렁 찍히면, 그게 어느 파일 몇 번째 줄에서 나온 것인지 알 길이 없다. 셋째, 켜고 끌 수가 없다. 레벨 개념이 없으니 전부 출력하거나 전부 지우거나 둘 중 하나다. 넷째, 버퍼링에 묻힌다. 표준 출력은 버퍼링되기 때문에, 프로그램이 크래시하면 마지막 print 내용이 화면에 닿기도 전에 사라질 수 있다.
그래도 print를 쓴다면, 최소한 이렇게
급할 땐 print가 정답일 때도 있다. 다만 같은 한 줄이라도 함정을 덜 밟는 방법이 있다. 핵심은 세 가지다. stderr로 보내고, flush=True로 즉시 내보내고, f"{변수=}" 문법으로 변수 이름까지 함께 찍는 것이다.
import sys
x = compute()
print(f"{x=}", file=sys.stderr, flush=True)
# 출력: x=42 ← 변수 이름과 값이 함께
파이썬 3.8부터 지원하는 f"{x=}"는 x=42처럼 식과 값을 같이 출력해 줘서 “이 숫자가 뭐였더라” 문제를 없앤다. file=sys.stderr는 디버그 출력을 프로그램의 실제 출력(stdout)과 섞이지 않게 분리하고, flush=True는 버퍼링으로 출력이 사라지는 일을 막는다. 이 세 가지만 챙겨도 print 디버깅의 함정 중 둘은 메운다.
버퍼링 함정을 직접 확인하기
버퍼링 문제는 추상적으로 들리지만 실제로 사람을 잡는다. 다음 코드는 크래시 직전의 출력이 어떻게 사라질 수 있는지 보여 준다.
import sys
for i in range(1000):
print(i) # stdout은 버퍼링됨
if i == 500:
sys.exit(1) # 비정상 종료 시 버퍼가 안 비워질 수 있음
파이프로 연결하거나 파일로 리다이렉트하면 stdout이 블록 버퍼링으로 바뀌어, 마지막에 찍은 숫자가 화면에 닿지 못한 채 프로세스가 끝나기도 한다. flush=True를 주거나 python -u로 버퍼링을 끄면 해결되지만, 이건 결국 print가 디버깅 도구로서 한계가 있다는 신호다.
졸업할 때
print 디버깅의 네 함정을 한 번에 메우는 도구가 표준 라이브러리에 있다. 바로 logging이다. 레벨로 켜고 끄고(셋째 함정), 포매터로 파일·줄·시각을 붙이고(둘째 함정), 핸들러로 어디로 보낼지 정하고, 운영에서는 레벨만 올려 디버그 출력을 통째로 숨긴다(첫째 함정). 임시로 한 번 보고 버릴 값이라면 print도 충분하다. 하지만 같은 진단을 며칠 뒤에도, 운영에서도 보고 싶다면 그때가 바로 졸업할 때다. 다음 글에서 그 logging의 기본기를 처음부터 다진다.
지난 글: breakpoint(): 디버거를 부르는 표준 한 줄
다음 글: logging 모듈: print를 졸업하는 법
읽어주셔서 감사합니다. 😊