두 개의 노트로 디버깅하기: ‘생각’과 ‘실행’을 나눠서 어려운 버그를 더 빨리 푸는 방법
‘생각’과 ‘실행’을 단순한 두 개의 노트로 분리하면, 더 침착하고 체계적이며 효과적인 디버깅을 할 수 있다.
디버깅이야말로 진짜 소프트웨어 엔지니어링이 이루어지는 곳이다. 기능 개발은 재미있지만, 버그를 통해서야 비로소 내가 이 시스템을 정말 이해하고 있는지 드러난다.
하지만 마감이 가까워지면, 대부분의 우리는 이렇게 디버깅한다:
- 깨진 동작을 멍하니 쳐다본다
- 어렴풋한 추측을 하나 한다
- 코드를 여기저기 찔러본다
- 다시 실행한다
- 버그가 사라지거나, 내 의지가 먼저 바닥날 때까지 반복한다
이보다 더 나은 방법이 있다. 디버깅 연구와 인간 두뇌의 작동 방식 양쪽에 모두 잘 맞는 방법이다. 바로 간단한 두 개의 노트 시스템으로 ‘생각(thinking)’과 ‘실행(doing)’을 분리하는 것이다.
이 글에서 다룰 내용은 다음과 같다:
- 디버깅이 주로 도구가 아니라 가설과 멘탈 모델의 문제인 이유
- 인지 부하(cognitive load) 가 어떻게 디버깅을 망치는지
- 어려운 버그를 위한 실전 두 노트 워크플로우
- 이 방법이 디버깅을 학습 가능한 하위 스킬로 쪼개는 방식
디버깅은 도구 문제가 아니라 생각의 문제다
수십 년간 진행된 디버깅 및 프로그램 이해 연구는 몇 가지 공통된 결론으로 모인다.
-
뛰어난 디버거는 명확한 가설을 세운다.
“인증 플로우(auth flow) 어딘가가 이상한 것 같다”라고만 말하지 않는다. 대신 이렇게 말한다. “서비스 A에서는token.expiry를 초 단위로, 서비스 B에서는 밀리초 단위로 해석하는 것 같다.” 이런 아이디어는 구체적이고, 검증 가능하며, 반증 가능하다. -
그들은 멘탈 모델을 만들고 계속 다듬는다.
가설을 검증하면서 코드, 데이터, 환경이 어떻게 동작하는지에 대한 내부 모델을 계속 업데이트한다. 모델과 맞지 않는 것이 발견되면, 그 틈이 곧 단서가 된다. -
그들은 의도적으로 반복(iterate)한다.
실험을 설계하고, 결과를 해석하고, 방향을 조정한다. 무작정 이리저리 헤매지 않는다.
디버거, 프로파일러, 로그 같은 도구는 중요하다. 하지만 어디까지나 생각을 증폭시키는 앰프일 뿐이다. 멘탈 모델과 가설이 흐릿하면, 데이터가 많아질수록 단지 노이즈만 늘어난다.
숨은 적: 디버깅 중 인지 부하
어려운 버그에는 보통 한 가지 공통점이 있다. 바로 작업 기억(working memory)을 과부하 시킨다는 점이다.
디버깅하면서 동시에 떠안는 것들:
- 여러 가지 가능한 원인 후보들
- 어설프게만 기억나는 콜 스택과 데이터 흐름
- 엣지 케이스와 기묘한 입력들
- 직감의 속삭임 (“레이스 컨디션 같기도 하고… 캐싱 문제 같기도 하고… 둘 다일 수도 있고?”)
- 팀과 마감에서 오는 압박감
인지 심리학 연구는 이 점에 대해 단호하다. 작업 기억은 작고 쉽게 망가진다. 과부하가 걸리면, 추론 능력이 떨어진다. 그 결과:
- 이미 시도했던 가설을 잊어버린다
- 같은 실패한 실험을 또 돌린다
- 한 가지 이론에 집착해서, 모순되는 증거를 무시한다
해결책은 정신력으로 “더 열심히 버틴다”가 아니다. 생각을 적절한 외부 구조로 옮겨서(offload), 머리는 기억이 아니라 추론에 집중하게 만드는 것이다.
그때 필요한 것이 바로 두 노트 디버거(two-notebook debugger)다.
두 노트 디버거: 개요
아이디어: 생각하는 공간과 실행하는 공간을 물리적으로든 디지털로든 분리한다.
-
노트 1: Thinking Notebook (생각 노트)
목적: 가설, 멘탈 모델, 계획, 결론을 기록한다. -
노트 2: Doing Notebook (실행 노트)
목적: 실제 행동, 실험, 명령, 관찰 내용을 기록한다.
실물 노트 두 권일 수도 있고, 노트 앱의 두 개 문서, 디버깅 템플릿 안의 두 섹션, 지식 관리 도구의 두 개 패널일 수도 있다. 핵심은 개념적 분리다.
이 방식이 통하는 이유:
- 디버깅을 실험 루프(experiment loop) 구조로 만들어서, 혼돈 대신 반복 가능한 과정으로 바꾼다.
- 인지 부하를 줄이고, 기억을 외부 노트로 옮긴다.
- “지금 내가 실제로 무슨 일이 일어난다고 생각하는가?”를 스스로에게 묻게 하면서, 생각을 선명하게 만든다.
- 나중에 다시 보고 개선할 수 있는 학습 흔적(learning trail) 이 생긴다.
이제 각각의 노트를 어떻게 쓰는지 살펴보자.
노트 1: Thinking Notebook (생각 노트)
이 노트는 계획하고, 모델링하고, 해석하는 공간이다. 여기에 적히는 내용은 구체적인 명령이나 구현 세부사항이 아니라, 개념적으로 어떤 일이 일어나고 있다고 보는지에 관한 것이다.
간단한 템플릿은 다음과 같다.
1. 문제 정의 (Problem Statement)
버그에 대한 명확한 설명을 적는다.
- 관찰된 동작(Observed behavior): 실제로 무슨 일이 일어나고 있는가?
- 기대 동작(Expected behavior): 원래는 무엇이 일어나야 하는가?
- 최소 재현(minimal reproduction, 알고 있다면): 입력, 재현 단계, 환경.
이 과정은 디버깅 도중에 버그에 대한 이해가 슬그머니 바뀌어 버리는, 이른바 “발 밑이 바뀌는 버그(bugs that shift under your feet)” 문제를 막아 준다.
2. 환경 & 컨텍스트 (Environment & Context)
중요해 보이는 제약 조건을 적는다.
- 브랜치/커밋
- 연관된 서비스/버전
- 관련 설정값(config)이나 기능 플래그(feature flag)
버그가 어떤 세계(환경) 안에서 살아 움직이고 있는지 스냅샷을 남기는 셈이다.
3. 현재 멘탈 모델 (Current Mental Model)
이 부분의 시스템이 어떻게 동작한다고 생각하는지 적는다. 확신이 없어도 괜찮다.
예를 들어:
“요청은
Gateway → AuthService → UserService순으로 전달된다. AuthService에서 인증 토큰을 검증하고, 그 결과를 요청 컨텍스트에 붙인다. UserService는 그 컨텍스트를 신뢰하며, 별도의 재검증은 하지 않는다.”
시스템 전체를 문서화하는 것이 아니다. 버그와 관련된 슬라이스(slice) 만 다룬다.
4. 가설 목록 (Hypotheses List)
이 부분이 핵심이다.
각 가설을 번호가 붙은, 검증 가능한 문장으로 적는다.
- H1: “모바일 클라이언트에서 오는 요청에만 구식 SDK 때문에 인증 토큰이 누락된다.”
- H2: “토큰은 존재하지만 만료되었고, 만료 체크에서 타임존 처리가 잘못됐다.”
- H3: “토큰은 유효하지만, UserService가 캐시 응답을 반환할 때 가끔 인증을 건너뛴다.”
각 가설마다 다음을 덧붙인다.
- 신뢰도(예: 20%, 50%)
- 테스트 계획(Test plan): “AuthService에 로그를 추가해 토큰과 만료 시각을 출력하고, 모바일과 웹 요청을 비교한다.”
모호한 직감을 명시적인 베팅(explicit bets) 과 구체적인 실험으로 바꾸는 과정이다.
5. 결과 & 모델 업데이트 (Results & Model Updates)
각 실험을 마친 후, 다시 이 노트로 돌아와서 적는다.
- 어떤 가설을 테스트했는지
- 무엇을 볼 것이라 예상했는지
- 실제로 무엇을 관찰했는지
- 그 결과 멘탈 모델이 어떻게 바뀌는지
틀린 가설을 단순히 버리는 것이 아니라, 실패한 가설로부터 배우는 습관을 기르는 공간이다.
노트 2: Doing Notebook (실행 노트)
이 노트는 실험실 노트(lab log) 다. 실제로 무엇을 했는지 시간 순서대로 적는다.
각 항목에는 예를 들어 다음 내용이 들어갈 수 있다.
- 타임스탬프
- 관련 가설 번호 (예: “H2 테스트 중”)
- 실행한 명령, 호출한 엔드포인트, 확인한 데이터
- 핵심 로그나 출력 스니펫
예를 들면:
15:42 — H2(만료 타임존) 테스트
- AuthService에 임시 로그 추가:
log.info("expiry={}, now={}", token.expiry, Instant.now())- 모바일 클라이언트로 재현 시도
- expiry 값이 현재 시각보다 5분 뒤로 찍히고,
now는 올바른 타임존으로 출력됨- 결론: 만료 타임존 문제 아님 → H2 신뢰도 40% → 5%로 하향
왜 이걸 생각 노트와 분리해야 할까?
- Thinking Notebook이 장황한 실행 기록으로 더러워지는 것을 막는다.
- 나중에 포스트모템이나 문서를 쓸 때, 실행 순서를 재구성하기 쉽다.
- 같은 실패한 실험을 반복하는 일을 줄인다.
아주 길게 쓸 필요는 없다. 나중에 내가 또는 다른 사람이 다시 실행하거나 이해할 수 있을 정도면 충분하다.
두 노트 시스템이 디버깅 스킬을 쪼개는 방법
이 접근법의 또 다른 장점은 디버깅을 더 작고 훈련 가능한 하위 스킬로 나눈다는 점이다.
-
가설 생성(Hypothesis Generation) — Thinking Notebook
연습: 어떤 테스트를 하기 전에, 가능한 원인 세 개를 반드시 적어 본다. 첫 번째 이론에 너무 빨리 집착하는 경향을 상쇄한다. -
실험 설계(Experiment Design) — Thinking Notebook
연습: 각 가설마다, 그 가설을 반증할 수 있는 가장 싸고 빠른 테스트를 설계한다. “서브시스템 X를 리팩터링한다”가 아니라, “Y 값을 로깅해서 null이 되는지 확인한다” 정도로. -
실험 실행(Experiment Execution) — Doing Notebook
연습: 설계한 테스트를 “이것만 살짝 더” 같은 스코프 크리프 없이 그대로 수행한다. -
결과 해석 & 모델 업데이트(Result Interpretation & Model Updating) — Thinking Notebook
연습: 항상 한 줄 요약을 쓴다. “이 실험 결과로 H3에 대한 믿음이 높아졌다. 이유는 …” -
탐색 전략(Search Strategy) — 두 노트 모두
여러 버그에 대한 노트를 모아 보면, 원인 공간을 탐색하는 자기 패턴이 드러난다. 이 전략을 의도적으로 개선할 수 있게 된다.
이렇게 하위 스킬의 이름을 붙이고 분리하면, 디버깅은 더 이상 “경험 쌓다 보면 언젠가 나아지겠지”가 아니라 의식적으로 연습할 수 있는 기술이 된다.
두 노트를 위한 도구 선택
복잡한 도구는 필요 없다. 다만 약간의 구조는 도움이 된다.
가능한 세팅 몇 가지:
- 종이 + 디지털: 손으로 쓰는 노트를 Thinking 용으로 (스케치와 자유로운 메모에 좋음), 에디터의 텍스트 파일이나 scratchpad를 Doing 용으로.
- 레포 안 두 개의 파일:
/debug-notes폴더 아래bug-123-thinking.md,bug-123-doing.md같은 식으로. - 노트 앱: 하나의 노트 안에
# Thinking,# Doing두 개의 큰 헤딩을 두고 섹션을 나눈다.
도구를 고를 때는 다음이 쉬운지 살펴보자.
- 타임스탬프 추가
- 코드, 로그, PR에 대한 링크 첨부
- 과거 버그를 키워드나 컴포넌트로 검색
노트를 업데이트하는 데 마찰이 적을수록, 실제로 꾸준히 적게 될 가능성이 커진다.
두 노트로 디버깅을 가르치고 코칭하기
주니어 엔지니어를 멘토링하거나 프로그래밍을 가르치는 입장이라면, 두 노트 시스템은 피드백을 위한 좋은 구조를 제공한다.
- Thinking Notebook 리뷰: 가설이 구체적인가? 실험 결과에 따라 멘탈 모델을 업데이트하고 있는가? 아니면 그냥 이리저리 휘젓고만 있는가?
- Doing Notebook 리뷰: 너무 크고 느린 실험만 반복하고 있지는 않은가? 같은 단계를 여러 번 반복하지는 않는가?
디버깅은 인지적으로 매우 부담이 크기 때문에, 생각과 실행을 분리하면 학습자 입장에서 과부하가 줄어든다. 버그 전체를 머릿속에 다 떠안지 않아도 되고, 노트가 그 일부를 대신 짊어져 준다.
심지어 본인의 디버깅 과정을 라이브로 보여 줄 수도 있다. 화면을 공유하고, 두 노트를 채워 나가면서 생각을 구두로 설명해 보는 식으로.
결론: 천천히 하는 것처럼 보여도 더 빨리 디버깅하는 방법
두 노트 디버거 방식은 처음엔 더 느리게 느껴질 수 있다. 바로 코드로 뛰어들지 않고, 먼저 적는 시간을 갖기 때문이다.
하지만 실제로는 이 방식이:
- 무작정 휘젓기(random thrashing) 를 줄이고
- 추론 과정을 눈에 보이고 리뷰 가능하게 만들며
- 인지 부하를 줄여 더 맑은 정신으로 생각하게 하고
- 디버깅을 개선 가능한 스킬 세트 로 바꿔 준다.
다음에 골치 아픈 버그를 만나면, 무턱대고 여기저기 코드를 찌르기 전에 그 충동을 잠시 참아 보자. 노트 두 개를 연다. 하나는 생각(Thinking), 하나는 실행(Doing) 용이다. 그리고 진짜 해야 할 일—내 시스템에 대한 올바른 멘탈 모델을 만들고 다듬는 작업—의 짐을 노트에 나눠 맡겨 보자.
아마 더 빨리 버그를 고치게 될 것이다. 더 중요한 건, 버그가 나올 때마다 코드를 더 깊이 이해하게 된다는 점이다. 디버깅이 장기적으로 남기는 가장 값진 성과는, 바로 이 이해의 진전 이기 때문이다.