Post

디버깅에 대한 고찰

디버깅에 대한 고찰

🔵 Intro

예전 면접에서 받았던 질문 중에 기억에 남는 질문이 ‘디버깅 하는 방식을 저에게 알려주시겠어요?’ 라는 질문이었다.

아니.. 디버깅하는데 무슨 방식이 있어? 과거 C/C++ 면접에서 디버깅을 하는 방식은 비주얼스튜디오의 기능인 중단점(break point)였다.

그런데 이 기능은 어느 정도 숙달도 필요할 뿐더러 수많은 변수의 값을 step-by-step으로 관찰하는 건 힘들 것이다.

그래도 면접에서 뭐라도 있어보이게 얘기해야해서 break point를 설명했지만 정작 제대로 써본 적은 몇 번 없었다.

그리고 시간이 흐르고 C/C++이 아닌 파이썬을 사용하는 환경으로 바뀌었고 비주얼 스튜디오라는 앱도 VScode로 더 가벼워지고 강력한 앱으로 변경되었다.

그래서 이번 기회에 어떤 디버깅 방식이 있는지, 혹시 내가 모르는 체계적인 디버깅 이론이 있는지 한 번 알아보았다.


⚪ 디버깅의 기본 자세

디버깅은 범인을 찾는 과정과 똑같다. 코드는 용의자고, 에러는 범죄 현장, 로그는 증거물이다. 무작정 코드를 고치기 전에 탐정처럼 생각하는 습관이 중요함.

  1. 버그 재현하기 (Reproduce the bug): 어떤 조건에서 에러가 나는지 100% 재현할 수 있어야 한다. ‘어쩔 땐 되고 어쩔 땐 안 돼요’가 가장 잡기 힘든 범인임.
  2. 범위 좁히기 (Isolate the problem): 에러가 발생한 코드 블록, 함수, 모듈 단위로 범위를 좁혀나가야 함. “이 함수에 데이터를 넣기 전엔 괜찮았는데, 나온 뒤엔 이상해졌다”처럼.
  3. 가설 세우고 검증하기 (Hypothesize and Test): “아마 데이터의 shape이 문제일 거야”라고 가설을 세우고, print()나 디버거로 가설이 맞는지 확인하는 과정을 반복함.

⚪ 고전적이지만 가장 강력한 방법들

  • 가장 원시적이지만, 가장 강력하고 직관적인 방법. “이 코드가 실행은 되는 건가?” 싶을 때 print("여기는 실행됨") 한 줄이면 바로 알 수 있음.
  • 무엇을 찍어봐야 할까?
    • Shape 확인: 텐서나 numpy 배열의 .shape은 무조건 찍어보는 습관을 들이면 좋다. 딥러닝 에러의 80%는 shape 불일치에서 오니깐.
    • Type 확인: type() 함수로 데이터의 자료형(e.g., torch.Tensor, numpy.ndarray, PIL.Image)을 확인. 함수가 기대하는 타입과 다른 경우가 많음.
    • 값 확인: 반복문 안에서 특정 변수의 값이 어떻게 변하는지, 혹은 NaN이나 inf 같은 이상한 값이 섞여있는지 확인.
고무 오리 디버깅 (Rubber Duck Debugging)
  • 이거 농담 같지만, 구글 같은 회사에서도 쓰는 실제 방법론이라고 함. 옆에 오리 인형(이나 사람, 혹은 그냥 허공)에게 “나는 지금 이 함수에 이런 모양의 데이터를 넣고 있는데, 여기서 이런 계산을 거치면 이런 결과가 나와야 하거든? 그런데…” 라고 말로 설명해보면 순간적으로 버그 지점을 생각해낼 수 있다.
  • 왜 효과가 있을까? 남에게 설명하려면 자기 생각을 논리적으로 정리해야 함. 이 과정에서 스스로 코드의 모순점을 발견하는 경우가 정말 많음.

⚪ 2. 주피터 노트북 환경에서의 꿀팁

“일단 커널 재시작부터”
  • 주피터 노트북의 가장 큰 함정은 ‘숨겨진 상태(hidden state)’임. 셀 실행 순서가 꼬이거나, 지웠던 셀의 변수가 메모리에 남아있는 등 눈에 보이지 않는 상태 때문에 에러가 나기도 함.
  • 코드가 이상하게 동작한다 싶으면, 일단 Kernel > Restart & Clear Output 이나 Restart & Run All 을 눌러서 깨끗한 상태에서 다시 실행해보는 것이 중요.
매직 커맨드: %debug
  • 셀을 실행하다 에러가 발생했을 때, 바로 다음 셀에 %debug 라고 입력하고 실행해.
  • 그러면 에러가 발생한 바로 그 시점의 대화형 디버거(pdb)가 실행돼. 여기서 변수 값을 직접 확인하거나, 코드를 한 줄씩 실행해보는 등 훨씬 깊이 있는 분석이 가능함. print()를 미리 넣지 않아도 돼서 아주 유용함.

⚪ 3. 딥러닝 모델 디버깅: 가장 어려운 과제

코드는 돌아가는데 학습이 안 되거나 결과가 이상할 때 쓰는 방법들.

작게 시작해서 검증하기 (Start Small)
  • Overfit on a small batch: 전체 데이터셋으로 학습시키기 전에, 아주 작은 데이터셋(예: 1~2개 배치)만으로 모델을 학습을 진행. 모델이 정상이라면 이 작은 데이터에 대해서는 Loss가 0에 가깝게 떨어지면서 완벽하게 과적합(overfitting)이 일어나야 함. 만약 이것조차 안된다면, 모델 구조나 데이터 파이프라인에 심각한 버그가 있다는 뜻.
학습 과정에서 흔히 만나는 문제들
  • Loss가 줄어들지 않을 때:
    • 의심: Learning rate가 너무 낮거나 높다. 데이터 정규화(Normalization)가 잘못되었음.
    • 검증: Learning rate를 10배 높이거나 낮춰본다. 데이터가 실제로 [0, 1]이나 평균 0, 표준편차 1 사이로 정규화되었는지 print로 확인.
  • Loss가 NaN으로 폭발할 때:
    • 의심: Learning rate가 너무 높다. log(0)처럼 불안정한 연산이 있다.
    • 검증: Learning rate를 확 낮춘다. torch.autograd.set_detect_anomaly(True) 코드를 추가해서 어디서 그래디언트가 터지는지 확인한다.

⚪ 4. 전문 디버깅 툴

print()만으로는 한계가 올 때, VS Code나 PyCharm 같은 IDE에 내장된 디버거를 사용하는 게 좋아.

  • Breakpoint (중단점): 코드의 특정 줄에 ‘멈춤’ 표시를 해두고, 코드가 거기까지 실행되면 잠시 멈추게 할 수 있음.
  • Step-by-step 실행: 멈춘 상태에서 코드를 한 줄씩(step over), 또는 함수 안으로 들어가며(step into) 실행할 수 있음.
  • 변수 검사: 실행되는 모든 시점의 변수 값을 실시간으로 확인할 수 있어서 print를 일일이 넣을 필요가 없음.

🔵 결론적으로

에러 로그만 보는 수동적인 디버깅에서 벗어나서, ‘가설을 세우고 -> print나 디버거로 검증’하는 능동적인 자세를 갖는 것이 핵심.

특히 딥러닝에서는 ‘작은 데이터로 먼저 검증하는 습관’이 수많은 시간을 아껴줄 수 있다.

This post is licensed under CC BY 4.0 by the author.