열 번의 수선, 한 글자
아홉 명의 알리바이를 확인하고서야 진범이 보였다
스택튜브에는 한 달치 노트를 한 권의 전자책으로 묶어주는 기능이 있다. 월말이 되면 그달에 분석된 영상 노트들이 챕터가 되고, 표지가 입혀지고, 이메일과 킨들로 배달된다. 월간 무크지라고 부른다. 이 기능을 만들던 주말의 이야기다.
빌드는 성공했다. 파일도 생성됐다. 용량도 그럴듯했다. 그런데 책을 열면, 백지였다.
목차는 있는데 본문이 없는 책. 에러 메시지는 없었다. 시스템의 어디를 봐도 "성공"이라고 적혀 있었다. 성공했다는 백지 책. 여기서부터 수사가 시작됐다.
에러가 나면 에러 메시지를 AI에게 보내면 된다. 그건 이제 무섭지 않다. 그런데 이번엔 보낼 것이 없었다. "책이 비어 있어"라는 말은 단서가 아니라 증상이다. 병원에 가서 "아파요"라고만 말하는 환자가 된 기분이었다. 어디가 어떻게 아픈지를 모르면, 의사도 짐작으로 시작할 수밖에 없다.
그래서 짐작으로 시작했다. 폰트 패키지가 빠졌나 — 고쳤다. 백지. 스타일시트가 연결이 안 됐나 — 고쳤다. 백지. 배포 설정에서 파일이 누락됐나 — 고쳤다. 백지. 변환기 옵션인가 — 고쳤다. 백지. 한 번 고칠 때마다 "이번엔 됐다"는 확신이 있었고, 매번 책은 비어 있었다.
여기서 이상한 점이 있었다. 고치는 것마다 실제로 문제이긴 했다. 폰트도, 스타일시트도, 배포 설정도 전부 손봐야 할 곳이었다. 하나하나가 진짜 결함이었다. 그래서 더 헷갈렸다. 고칠 때마다 "이게 원인이었겠지"라고 믿을 근거가 있었으니까. 진짜 범인이 따로 있는데, 잡범들이 계속 잡히는 상황이었다.
이게 이런 종류의 버그가 가진 함정이다. 용의자가 한 명이면 수사가 쉽다. 그를 조사하면 끝이다. 하지만 용의자가 열 명이고, 그중 아홉 명이 각자 다른 사소한 잘못을 저질렀다면 — 진범을 가려내는 일이 어려워진다. 잘못을 저지른 사람이 많을수록, 누가 이 사건의 진짜 원인인지가 흐려진다.
일곱 번째 수선에서 AI가 범인을 찾았다. 원인은 코드 한 줄. 정확히는, 문자열을 바이트로 바꿔주는 변환 하나가 빠져 있었다.
설명을 듣고도 한참 이해하지 못했다. 요약하면 이렇다. 책의 각 페이지를 만드는 라이브러리가 있고, 그 라이브러리에 글을 넘길 때는 특정한 형식이어야 했다. 형식이 맞지 않으면 라이브러리는 그 페이지를 만들지 못한다. 그런데 그 실패가 겉으로 드러나지 않고, 빈 페이지로 대체되어 있었다. 그래서 모든 페이지가 조용히 비어 있었던 것이다.
수정은 .encode("utf-8") — 한 줄이다. 이 한 줄을 넣자 712,841바이트짜리 책이 나왔다. 본문이 있는 책이었다.
그날 밤에 생각했다. 열 번의 수선 중 아홉 번이 헛수고였나?
아니었다. 그게 이 사건에서 내가 얻은 가장 큰 교훈이다. 아홉 번의 수선은 헛수고가 아니라, 진범을 드러나게 만든 과정이었다. 폰트를, 스타일시트를, 배포 설정을 다 고쳐서 용의선상에서 지우고 나니, 남은 자리에 진짜 원인이 홀로 서 있었다. 용의자가 열 명일 때는 추리가 안 된다. 아홉 명의 알리바이를 확인하는 일은 지루하지만, 그게 수사다. 그 지루한 소거 없이는 마지막 한 명을 지목할 수 없다.
비개발자에게 이건 특히 중요한 감각이었다. 나는 코드를 읽지 못하니까, "이 부분이 원인일 것 같다"는 직관이 없다. 개발자라면 코드를 훑어보고 진범을 바로 좁힐 수도 있을 것이다. 하지만 나에게는 그런 지름길이 없다. 대신 나에게는 소거법이 있었다. 하나씩 고치고, 하나씩 지우고, 남는 것을 본다. 느리지만, 결국 도착한다.
그리고 또 하나. 나는 이제 "성공"이라는 말을 조금 덜 믿게 됐다. 시스템이 "빌드 성공"이라고 말하는 것과 결과물이 멀쩡한 것은 다른 문제였다. 빌드가 성공했다는 건 "파일을 만드는 과정에 오류가 없었다"는 뜻이지, "그 파일의 내용이 옳다"는 뜻이 아니었다. 그 둘을 같은 것으로 착각하는 동안, 나는 백지 책을 일곱 번 만들었다.
한 글자. 정확히는 한 줄. 하지만 그 한 줄을 찾기까지 아홉 번의 소거가 필요했고, 그 아홉 번은 낭비가 아니었다.
🔧 이 에피소드의 기술 용어 해설
EPUB 전자책 표준 파일 형식. 내부적으로는 여러 개의 웹 페이지(XHTML)를 압축해 묶은 것이다. 킨들, 애플 북스 등에서 읽을 수 있다.
문자열과 바이트 (String / Bytes)
사람이 읽는 글자(문자열)와 기계가 저장하는 0과 1(바이트)은 다른 형식이다. 둘 사이를 오갈 때는 변환(인코딩)이 필요하다. .encode("utf-8")이 그 변환이다.
소거법 (Elimination) 가능한 원인을 하나씩 배제해가며 진짜 원인에 도달하는 방법. 코드를 읽지 못하는 사람이 디버깅에서 쓸 수 있는 가장 강력한 도구. 직관으로 원인을 좁힐 수 없다면, 하나씩 지워서 남기면 된다.
빌드 성공 ≠ 결과 정상 빌드가 성공했다는 것은 "만드는 과정에 치명적 오류가 없었다"는 뜻이지, "만들어진 결과물의 내용이 옳다"는 보장이 아니다. 이 둘을 구분하는 것이 이 에피소드의 핵심 교훈이다.
핫픽스 (Hotfix) 운영 중인 서비스의 긴급 수정. 이 사건에는 핫픽스가 열 번 있었고, 일곱 번째가 진범이었다.