모래 위의 디버깅 — 9규칙 중 하나를 건너뛰면 나머지가 무너진다
디버깅 9규칙 중 여덟 개를 지켜도 1번(시스템 이해)을 건너뛰면 나머지가 전부 모래 위의 집이 된다. '이거 간단하네' 한마디로 이틀을 잘 정리된 오답에 쓴 기록.
“이거 간단하네.” — 그 한마디에 나는 디버깅 9규칙의 1번을 건너뛰었다. 나머지 여덟 개는 나름 지켰다. 그런데도 이틀 뒤, 아주 잘 정리된 내 로그북은 통째로 틀린 답을 가리키고 있었다. 어쩌다 이렇게 됐을까.
잘 기록된 오답
이틀 동안 나는 아주 성실한 디버깅 로그를 남겼다. 측정값마다 숫자를 달았고, 가설마다 “반증됨/확정됨”을 표시했고, 조사 문서를 v1, v2, v3로 버전까지 매겼다. 그리고 첫날 저녁, 나는 자신 있게 결론을 냈다. “이건 우리가 고칠 수 있는 문제가 아니다. 외부 소프트웨어(벤더) 쪽 버그다.” 팀에도 그렇게 공유했다.
다음 날, 나는 그 결론을 통째로 철회했다.
잘 정리된 로그북이 통째로 틀린 답을 가리키고 있었다. 어떻게 이런 일이 벌어질까?
돌아보면 나는 디버깅의 고전적인 규칙 대부분을 “하긴 했다”. 다만 딱 하나 — 1번 — 을 건너뛴 채로 나머지를 했고, 그래서 나머지 전부가 모래 위에 지은 집이 됐다.
문제
나는 치과 CAD/CAM 쪽 일을 한다. 크라운(보철물)을 3D로 설계하고, 이걸 두 개의 파일로 내보낸다 — 형상 자체(mesh)와, 그 형상을 설명하는 사이드카 메타데이터 파일(가장자리 경계선인 “마진 라인”, 밀링 파라미터, 이 물체가 무엇인지 알려주는 “객체 타입” 태그 등). 이 둘을 MillBox(외부 밀링 소프트웨어)에 넘기면, MillBox가 그걸 깎아 실제 보철물을 만든다. MillBox 내부는 우리가 볼 수 없는 블랙박스다.
버그는 이랬다. 임플란트 크라운을 MillBox에 넘기면, 마진 라인이 매끄러운 원이 아니라 물결처럼 꼬불꼬불하게 그려졌다. 그것도 특정 치아에서만. QA가 재현 케이스와 함께 보고했다.
내부를 볼 수 없으니 전형적인 블랙박스 디버깅이다.
아홉 개의 규칙
디버깅에는 이미 잘 정리된 프레임이 있다. David Agans의 Debugging: The 9 Indispensable Rules(2002)가 대표적이다. 각 규칙은 “이걸 안 하면 이렇게 헛발질한다”의 뒤집힌 형태다.
- 시스템을 이해하라. 고치기 전에, 이 시스템이 무엇을 해야 하고 어떻게 동작하는지를 안다 — 특히 경계 너머(라이브러리·외부 SW)의 동작 계약을. 안다고 가정한 그 함수가 나를 문다.
- 재현하라. 원할 때 언제든 실패를 다시 만들 수 있어야 한다. 그래야 관찰하고, 원인을 좁히고, 고친 걸 확인한다. 드물게 터지면 증폭·자동화하되 실패 메커니즘은 건드리지 않는다.
- 추측 말고 관찰하라. 머리로 원인을 짓지 말고 로그·디버거로 실제 실패를 낮은 수준에서 눈으로 본다. “데이터도 없이 이론부터 세우는 건 중대한 실수다.”
- 이분해서 좁혀라. 문제 공간을 정상 반쪽/이상 반쪽으로 가르고, 이상 반쪽만 다시 반으로.
git bisect가 이 규칙의 화신이다. - 한 번에 하나만 바꿔라. 한 사이클에 한 변수만, 나머지는 고정. 안 먹히면 즉시 되돌린다. 산탄총 말고 저격총.
- 기록을 남겨라. 무엇을, 어떤 순서로, 어떤 결과로. “세부를 기억에 맡기지 말고 적어라.” Zeller가 말한 디버깅 로그북이 이것이다.
- 기본 전제를 확인하라. 코드가 진짜 로드됐나, 맞는 환경을 보고 있나, 캐시는 아닌가 — 가장 기본적인 가정을 의심한다. 도구도 사람이 만든 거라 안 틀리란 법이 없다.
- 새 시각을 구하라. 새 관점·전문성·경험을 위해 묻는다. 이때 이론이 아니라 증상을 보고해야 상대가 내 편향의 늪에 함께 빠지지 않는다.
- 고친 걸 증명 못 하면 안 고친 것. fixed → broken → fixed로 순환시켜 증명한다. 우연히 사라진 증상을 “고쳤다”로 착각하지 마라.
Andreas Zeller가 말한 과학적 디버깅(가설 → 반증 가능한 실험 설계 → 관찰 → 이론 수정 → 반복, 그리고 명시적 로그북)과도 겹친다. 그리고 이 규칙들이 막아주는 건 대개 두 가지 인지 편향이다 — 내 가설을 확인해주는 정보만 보는 확인 편향, 그리고 초기 가정이 무효가 됐는데도 계속 매달리는 고착.
나는 이 규칙들을 안다. 심지어 몇 개는 잘 지켰다. 그런데도 하루를 오답에 썼다. 왜?
규칙은 체크리스트가 아니라 구조를 가진다
돌아보며 내가 도달한 결론은 이거다. 9개 규칙은 골라 쓰는 평평한 목록이 아니다. 서로 의존한다. 그리고 그 의존의 맨 밑바닥, 나머지 여덟 개가 딛고 서는 토대가 1번(시스템을 이해하라) 이다.
1번은 조용히 두 가지를 준다.
- 오라클(oracle) — 무엇이 “올바른” 상태인가, 즉 무엇이 “실패”인가의 정의.
- 모델(model) — 무엇이 무엇과 연결돼 있는가, 즉 내 수정이 실제로 무엇에 영향을 주는가.
1번을 건너뛰면 나머지 규칙들은 모래 위에 짓는 집이 되기 쉽다. 재현도 하고, 이분도 하고, 한 번에 하나만 바꾸기도 하고, 수정도 한다. 그런데 아무것도 지지되지 않는다. 잔인한 건, 그 사실을 당장은 모른다는 것이다 — 모래 위의 집도 일단은 집처럼 보이니까.
1번이 무너지면서, 그 균열은 두 갈래로 번졌다.
오라클을 잃다 — “무엇이 실패인지”를 몰랐다
나는 이 케이스에서 “올바른 결과”가 무엇인지 끝내 확정하지 못한 채 시작했다. 저 꼬불한 마진이 버그인지, 아니면 의도된 동작인지조차 판단하지 못했다. 이게 얼마나 무서운 결핍인지는 아래로 내려가면서 드러난다.
재현(2번)이 안 됐다. 나는 2번을 평소 무의식적으로 중요하게 여긴다. 이유가 있다 — 완전한 재현이 나온다는 건, 실패를 일으키는 원인이 이미 충분히 좁혀졌다는 뜻이기 때문이다. 재현 자체가 곧 원인 좁히기다. 그런데 이번엔 시스템을 몰라서(1번), 어떤 보철물에서 문제가 나고 어떤 경우에 나아지는지를 짐작조차 못 했다. 같은 입력이 어떤 땐 매끈, 어떤 땐 꼬불로 나왔고 나는 그걸 예측할 수 없었다. 오라클이 없으니 입력을 “매끈해야 정상”과 “꼬불해야 정상”으로 나눌 수조차 없었다.
그래서 이분정복(4번)도 무너졌다. 4번은 원인을 좁히는 좋은 도구지만, 정상 상태를 만드는 경우와 이상 상태를 만드는 경우를 2번에서 깨끗이 갈라놓지 못하면 나눌 대상이 없다. 나는 지금도 원인을 정확히 좁히지 못했다.
그리고 9번(고친 걸 증명하기)이 원천 불가능했다. 이게 오라클 결핍의 최종 형태다. 수정을 하고 나서, 나는 이걸 판단할 수 없었다 — 실패하던 게 성공으로 바뀐 건지, 여전히 실패인지, 아니면 실패인 줄 알았는데 사실 처음부터 의도대로 동작하던 것이라 실패처럼 보였을 뿐인지. “올바름”의 정의가 없으면, “고쳐졌다”는 판정 자체가 성립하지 않는다.
모델을 잃다 — “무엇이 연결됐는지”를 몰랐다
다른 갈래는 더 구체적이다. 나는 MillBox가 우리 파일을 어떻게 받아 읽는지를 몰랐다. MillBox는 사이드카 파일을, 형상 파일과 같은 폴더에 같은 이름으로 있을 때만 읽는다. 그것도 MillBox가 자기 로컬 캐시 폴더에 우리가 올린 파일들을 넣어두고 거기서 직접 불러가는 방식이다. 나는 이 사실을 이틀째 막바지에야 문서로 확인했다. 게다가 나는 우리 코드조차 정확히 몰랐다 — 그 “객체 타입” 태그가 어디서 설정되는지, 보철물 종류마다 그 값이 어떻게 정해지는지, 애초에 그 사이드카 파일이 정확히 뭔지. 시작부터 토대가 없었던 것이다.
이 무지 위에서 벌어진 일들:
관찰(3번) 대신 추측이 길었다. 현상 자체를 면밀히 뜯어보지 않았다 — 각 현상이 정확히 어떤 데이터 형태로 나타나는지를. 대신 한 가지 경우만 보고 “이것 때문인가?” 하고 바로 수정→확인으로 갔다. 이게 패착이었다.
“한 번에 하나만 바꿨다”(5번)는 착각이었다. 나는 마진의 “내용”만 바꿨다고 믿었다. 아니었다. 무엇이 연결돼 있는지 몰랐기 때문에, 매 수정이 내가 추적하지 않던 숨은 변수를 함께 흔들었다.
- 합성 사이드카에 짧은 임시 파일명을 붙였다 → 형상 파일과 이름이 안 맞았다 → MillBox가 둘을 못 엮었다 → MillBox는 사이드카를 무시하고 자동 검출로 떨어졌다. “출력이 안 바뀐 것”은 “MillBox가 내용을 무시한 것”이 아니라 애초에 내 입력을 본 적이 없던 것이었다.
- 같은 형상 파일을 이름만 바꿔 반복 로드했다 → MillBox가 캐시된 이전 결과를 재사용했다. 내 변경이 MillBox의 처리에 닿지도 않았다.
- 마진을 2배로 균일하게 키운 프로브를 넣었다 → MillBox가 그걸 형상 표면에 스냅해 원복시켰다 → “안 바뀜”으로 보였다. 나중에 알았지만, 각도를 비튼 모양(꽃잎 같은)만이 스냅에서 살아남는다.
이 세 가지 교란요인은 전부 1번(모델)을 몰라서 생긴 직접적 산물이었다. “MillBox가 같은 폴더+같은 이름으로, 자기 캐시에서 읽는다”만 알았어도 첫 번째는 없었고, “MillBox가 캐시한다”만 알았어도 두 번째는 없었다.
그리고 나머지 규칙들도 같은 뿌리에서 흔들렸다. 기록(6번) — 실험 중 실시간 계측이 부실했다. 간단한 문제라고 여겼으니 그렇게까지 남길 필요를 못 느낀 것이다. 기본 전제 확인(7번) — “내 입력이 전달은 됐나, 링크는 됐나, 캐시가 아니라 새로 처리된 건가”라는 가장 기본적인 전제를 확인하지 않았다. 새 시각(8번) — 간단하다고 여겨 혼자 오래 붙들었고, MillBox 내부를 유일하게 아는 벤더에게는 한참 늦게야 물으려 했다.
내가 반복하는 악순환
돌아보니 이건 내 긴 디버깅마다 반복되던 패턴이었다.
간단하다고 생각한다(1번을 건너뛴다) → 빠르게 추측하고 검증부터 한다(3번을 어기고, 제대로 재현하지 않은 채 2번을 건너뛴다) → 알고 보니 간단하지 않아 수정이 안 먹는다(여기서 7번을 놓친다 — 이미 첫 가설에 갇혔다) → 이걸 해결하려 또 다른 수정을 얹는다(5번을 어긴다) → 나는 나름 한 번에 하나씩만 바꾸며 4번을 지키려 하지만, 이미 어긴 규칙들 때문에 제대로 나뉘지 않고, 수정이 버그를 계속 가린다.
버그가 리오픈되는 것도 아마 이 마지막 고리와 무관하지 않을 것이다 — 9번을 대충 하거나 건너뛰면, 우연히 사라진 증상을 “고쳤다”로 착각하기 때문이다.
아이러니 — 규칙이 통했던 순간들
그런데 잘된 것도 있었고, 그게 오히려 규칙의 힘을 증명한다.
첫째, 발견을 정리한 로그북 자체는 훌륭했다. 버전을 매기고, 숫자마다 근거를 달고, 가설마다 반증/확정을 표시했다. 여기서 배운 구분 하나 — “발견을 정리하는 로그북”(잘했다)과 “실험을 재현 가능하게 만드는 실시간 계측”(못했다)은 다른 것이다.
둘째, 딱 한 번, 실험을 규칙대로 했다. 다른 모든 걸 고정한 채 “객체 타입” 태그 하나만 자연치용에서 임플란트용으로 바꿨더니, 마진이 깨끗하게 나왔다. 이게 5번을 정확히 지킨 유일한 실험이었고, 이틀 중 신뢰할 수 있는 신호는 이거 하나였다.
셋째 — 그리고 정직하게 — 그 “답”조차 아직 9번을 통과하지 못했다. 나는 MillBox 화면에서 태그를 토글해 깨끗해진 걸 봤을 뿐, 실제 우리 export→배포 경로로 확인하지 않았고, 배포한 뒤 shipping 구성에서 “고장→수정→고장”을 순환시켜 증명하지 않았다. Agans의 기준으로 이건 아직 “안 고쳐진 것”이다. 나는 여전히 근본 원인을 완전히 알지 못한다.
그래서, 게이트
1번이 토대라면, 내 프로세스를 고치는 방법은 “2~9번을 더 열심히 하기”가 아니라고 본다. 1번(그리고 그 사촌인 7번, 9번)을 건너뛰지 못하게 막는 관문을 세우는 것이다. 무너진 순서를 거꾸로 뒤집으면 관문이 된다.
- 게이트 0 (1번): 코드를 건드리기 전에, 글로 쓸 수 있어야 한다 — (a) 여기서 “올바른 결과”가 무엇인지, (b) 데이터가 끝에서 끝까지 어떻게 흐르는지(블랙박스가 입력을 받아들이는 방식까지). 못 쓰겠으면, 첫 작업은 수정이 아니라 “내가 모르는 것” 목록을 만드는 것이다.
- 게이트 1 (2번): 결정적 최소 재현 + 명시적 오라클(“이 출력이 버그다”)을 세우기 전엔 A/B 실험 금지.
- 게이트 2 (5·7번): 매 수정 전에 배관이 여전히 통과하는지 확인(전달됐나? 링크됐나? 캐시 아닌 새 처리인가?) + 내가 고정하고 있는 숨은 변수들을 적어둔다. 파이프가 열려 있으면 반드시 나타나야 할 대조 신호를 하나 넣는다.
- 게이트 3 (9번): 실제로 배포할 구성에서 “고장→수정→고장”을 순환시켜 증명하기 전엔 “해결”도, 머지도 없다.
마무리
이 이야기의 진짜 빌런은, 내가 너무 쉽게 내뱉는 한 문장이다. “이거 간단하네.” 그 문장이 1번을 건너뛰게 만들고, 1번을 건너뛴 순간부터 집은 모래 위에 올라가기 시작한다.
디버깅은 결국 측정이다. 그리고 1번은 모든 측정이 기준으로 삼는 고정된 기준점이다. 기준점을 잃으면, 아무리 정밀하게, 하루 종일 측정해도 — 아무것도 측정하지 않은 것이 된다.
여기 정리한 게이트들은 앞으로 내가 쓸 개인 디버깅 매뉴얼의 첫 초안이기도 하다.