계획서대로 짰으면 그 버그는 그대로 나갔다

계획서대로 짰으면 그 버그는 그대로 나갔다
넉 달째 개인 프로젝트를 에이전트로 굴리고 있고, 회사 코드도 상당 부분 AI와 짠다. 그래서 「계획 없이 코딩하면 토큰 낭비하는 이유」라는 글이 조회 2만 3천에 추천 42를 받고 있는 걸 보고 한참 들여다봤다. 내가 매일 하는 일에 대한 얘기니까.
요지는 이렇다. research.md로 코드베이스를 먼저 분석시키고, 그걸 바탕으로 plan.md를 쓰고, 주석 달아가며 계획을 몇 번 다듬은 다음, "plan.md대로 모두 구현해"라고 지시한다. 그러면 "AI는 새로 판단할 필요 없이 계획 충실하게 코드를 쓰죠"라고 되어 있다.
읽고 나서 든 생각은 하나였다. 나는 정확히 반대로 일하고 있는데, 그럼 내가 토큰을 버리고 있는 건가.
그래서 지난주 내 커밋 로그를 열어봤다. 7월 29일 저녁, 회사에서 맡고 있는 데스크톱 오디오 앱의 버그를 다섯 개 고친 날이다. 커밋 시각이 18:05, 18:06, 19:06, 20:40, 21:52다. 네 시간이 안 걸렸다. 다섯 개는 이런 것들이었다.
- 자동 곡 전환 후 앨범 이미지가 이전 곡 것으로 남는다
- 자동 전환 시 바꾼 음소거 상태가 반영 안 된다
- 음질을 바꿔도 첫 전환에서는 이전 음질이 재생된다
- 자동 전환 후 볼륨이 이전 값으로 돌아간다
- 출력 장치를 바꿔도 다음 곡에 반영이 안 된다
지금 나열해놓고 보면 하나의 문제다. 재생 중에 바꾼 설정이, 자동으로 넘어간 다음 곡에 안 실린다. 미리 준비해둔 다음 곡 재생기가 설정 변경 대상에서 빠져 있었고, 그게 승격되는 순간 준비 시점의 옛 상태가 되살아난다. 원인은 같고, 증상만 다섯 가지로 갈라져 나온 것이다.
그런데 이걸 plan.md에 미리 적을 수 있었을까. 못 적는다. 저 다섯 개를 관통하는 문장은 네 번째 커밋을 쓰면서 겨우 나왔다. 그 전까지는 그냥 앨범 이미지 버그였고, 그다음엔 음소거 버그였다. 마지막 커밋 메시지에 내가 직접 이렇게 써놨다.
"…가 Not-Fixed로 남긴 '같은 결함의 네 번째 반복' 항목이다"
패턴에 이름이 붙은 게 네 번째다. 그 이름을 계획 단계에서 미리 알고 있었다면 애초에 버그 다섯 개가 아니라 리팩터링 하나였을 것이다. 알 방법이 없었으니까 다섯 번 걸린 거다.
참고로 저 레포는 AI로 코드를 짜는 게 예외가 아니다. 커밋 메시지에 AI-Use: yes와 기여도를 남기는 관례가 있는데, 그 표기가 달린 커밋이 746개다. 세 번째 커밋은 AI-Contribution: 100%로 찍혀 있다. AI를 안 써봐서 하는 얘기가 아니라는 뜻이다.
"낭비"라는 단어를 청구서에서 검증해봤다
반박하기 전에 원문의 주장을 정확히 세워두자. 저 글이 나쁜 글이 아니다. research.md → plan.md → 주석 반복 → 구현 → 피드백. 나쁜 순서가 아니고, 특히 "개발자가 계획 직접 보고 승인한 다음에야 AI가 코드를 쓰게 됩니다"라는 대목은 나도 대체로 동의한다.
문제는 제목에 박혀 있는 단어다. 낭비.
이 단어는 대화에 쓰는 토큰이 손실이라는 뜻이다. 계획을 세우고 그 계획대로 실행하는 게 효율이고, 왔다 갔다 하면서 정하는 건 새는 거라는 뜻이다. 나는 이게 사실인지 궁금해서 그냥 세어봤다. 이 블로그를 굴리는 레포의 세션 로그 다섯 개, 총 473턴이 남아 있다.
| 토큰 | 비중 | |
|---|---|---|
| 캐시 재사용 | 58,444,764 | 97.5% |
| 캐시 기록(신규) | 1,478,205 | 2.5% |
| 캐시 미적용 | 5,183 | 0.0% |
| 출력 | 580,433 | — |
총 입력 5,992만 토큰 중 5,844만이 캐시에서 다시 읽혔다.
Anthropic의 프롬프트 캐싱 과금은 캐시 읽기가 기본 입력 토큰가의 약 10분의 1이다. 캐시에 새로 기록할 때는 1.25배(5분 TTL) 또는 2배(1시간 TTL)를 낸다. 즉 처음 한 번 쌓아둘 때 조금 더 내고, 그 뒤로는 십분의 일로 계속 읽는 구조다.
그래서 저 표가 말하는 건 이거다. 대화가 길어져서 쌓인 컨텍스트는, 길어졌기 때문에 싸졌다. 같은 세션 안에서 앞의 맥락을 반복해서 읽는 비용은 정가가 아니라 1/10이다. 97.5%가 그 가격으로 처리됐다.
거꾸로 비싼 건 뭔가. 새 세션을 열고 plan.md를 처음부터 읽히는 쪽이다. 캐시가 없으니 캐시 기록이거나 정가다. 세션을 끊고 문서로 넘겨받는 워크플로우는, 순수하게 토큰 청구서만 놓고 봐도 대화를 이어가는 쪽보다 유리하지 않다.
수치가 나 하나의 사례라는 건 안다. 캐시 히트율은 프롬프트 구조와 도구 목록이 얼마나 안정적이냐에 따라 달라지고, 시스템 프롬프트에 타임스탬프 하나만 끼워 넣어도 그 뒤가 전부 무효화된다. 그래서 이 97.5%가 모든 사람의 숫자는 아니다. 다만 "대화는 낭비"라는 명제를 반증하는 데는 사례 하나로 충분하다. 낭비가 아닌 경우가 실재한다는 게 이 표다.
다섯 번째는 네 번째가 없었으면 못 고쳤다
앞에서 다섯 개가 사실 하나였다고 했는데, 정확히는 그것보다 더 나쁘다. 하나로 정리되는 과정 자체가 순차적이었다.
네 번째(20:40, 볼륨 건)를 고칠 때 원칙 하나가 나왔다. 준비만 되어 있고 아직 소리를 내지 않는 재생기도 볼륨 변경을 받아야 하는데, 그렇다고 그 재생기가 실제 소리 크기까지 건드리면 안 된다. 그래서 "목표 볼륨"과 "실제로 들리는 볼륨"을 분리했다. 목표값은 전체에 전파하고, 실제 적용은 활성 슬롯만. 그리고 하나 더 있었다. 그 준비 중인 재생기가 자기 볼륨을 상위로 역보고하면서 영속 설정을 낡은 값으로 덮어쓰고 있었다. 그래서 역보고도 목표값을 읽도록 바꿨다.
한 시간 뒤 다섯 번째(21:52, 출력 장치 건)에 붙었다. 증상은 같은 모양이었다. 출력 장치를 바꿔도 준비 중인 재생기는 자기가 세워질 때의 장치에 고착돼 있고, 승격되면 그 곡 한 곡만 이전 장치로 나간다. 그러니 앞에서 한 것처럼 전체 슬롯에 전파하면 될 것 같았다.
그러면 새 회귀가 생긴다. 준비 중인 재생기의 출력 장치 설정이 실패하면, 그 실패가 "라우팅 폴백" 통지를 쏘고, 그 통지가 라우팅 기준값을 기본 장치로 되돌리고, 그게 다시 방송되면서 정상 재생 중인 활성 재생기까지 기본 장치로 끌어내린다. 안 들리는 재생기 하나가 실패했다고 들리는 재생기를 끌고 내려가는 것이다.
그래서 통지 권한을 활성 슬롯에만 줬다. 준비 중인 재생기는 장치를 바꾸되 실패해도 통지하지 않는다. 커밋 메시지에 이렇게 적었다.
"들리지 않는 엔진은 라우팅 기준값을 덮을 자격이 없다. 볼륨 역보고를 목표값만 읽게 한 것과 같은 논지다."
"같은 논지다." 이 문장이 이 글의 핵심이다.
다섯 번째 버그의 올바른 해법은, 네 번째 버그를 고치다 나온 원칙의 재적용이다. "안 들리는 재생기는 전역 상태를 덮을 자격이 없다." 볼륨에서 한 번, 출력 장치에서 또 한 번. 그런데 이 원칙은 네 번째를 고치기 전에는 존재하지 않았다. 20:40의 커밋이 만들어낸 것이다.
그러니까 순서는 이렇다.
- 앨범 이미지를 고친다 → 아직 아무 패턴도 안 보인다
- 음소거를 고친다 → 승인된 설계가 틀렸다는 걸 알게 된다
- 음질을 고친다 → 재현이 잘 안 되는 이유를 알게 된다
- 볼륨을 고친다 → "안 들리는 건 덮을 자격이 없다"는 원칙이 생긴다
- 출력 장치를 고친다 → 4에서 생긴 원칙을 적용해서 회귀를 미리 피한다
18:05에 쓴 plan.md에 5번의 해법이 들어 있을 수 있나. 없다. 5번의 해법은 4번의 부산물이고, 4번은 3번을 고치는 동안 코드를 읽다가 나왔다. 계획을 아무리 정교하게 세워도 이 사슬은 안 나온다. 계획 시점에는 사슬의 첫 칸조차 안 보이니까.
이게 발견이 본체인 작업의 모양이다. 앞의 결과가 뒤의 문제 정의를 바꾼다. 그런 작업에서 계획을 앞에 몰아두면, 가장 정보가 없는 시점에 내린 결정이 가장 정보가 많은 시점의 판단을 이기게 된다.
승인받은 설계가 틀렸던 커밋
그날 두 번째 커밋(18:06, 음소거 건)의 메시지에는 이런 소제목이 달려 있다.
승인받은 설계와 다르게 구현한 부분 (이 커밋에서 가장 봐야 할 곳)
내가 붙인 소제목이다. 내용은 이렇다. 승인된 설계는 "다음 곡으로 올리는 시점에 마지막 음소거 값을 다시 적용"이었다. 합리적으로 들린다. 그런데 그대로 하면 다른 문제가 생긴다. 근거를 네 개 적어놨는데, 앞의 두 개만 옮기면 이렇다.
- 그 "마지막 음소거 값"의 초기값이 무조건
false다. - 음소거의 진짜 기준값은 프런트엔드가 아니라 Rust 쪽 설정 저장소에 있다.
즉 승인된 설계대로 구현하면, 초기값이 false인 변수를 기준으로 삼게 되고, 진짜 기준값을 무시한다. 버그가 그대로 나간다. 그래서 준비 중인 재생기까지 포함해 전체 목록에 적용하는 방식으로 일부러 다르게 짰다.
뽐뿌 글의 문장을 다시 보자.
"이때 AI는 새로 판단할 필요 없이 계획 충실하게 코드를 쓰죠."
이 커밋은 그 문장의 반례다. 계획에 충실했으면 버그가 나갔다. 계획이 틀렸다는 걸 알아차린 건 계획 단계가 아니라 구현하러 들어가서 실제 코드를 읽었을 때다. lastMuted의 초기값이 뭔지, 진짜 기준값이 어디 있는지는 파일을 열어봐야 아는 사실이고, 계획서를 쓰던 시점에는 아무도 몰랐다.
그리고 이건 사고가 아니라 정상 동작이다. 계획은 정보가 부족한 시점에 세우고, 구현은 정보가 늘어난 시점에 한다. 그 사이에 알게 된 게 있으면 계획을 고쳐야지, 계획을 지켜야 할 이유가 없다.
10회 중 2~3회만 재현되는 버그
세 번째 커밋(19:06, 음질 건)에는 이런 줄이 있다.
"미리 받는 시점이 곡마다 달라(캐시 곡은 즉시, 스트리밍 곡은 종료 4초 전) 10회 중 2~3회만 재현됐다."
재현율 20~30%다. 이런 버그에 research.md를 쓸 수 있나. 코드베이스를 아무리 정적으로 분석해도, "다음 곡을 미리 받는 시점이 곡의 캐시 여부에 따라 달라서 특정 타이밍에만 걸린다"는 문장은 안 나온다. 그건 열 번 돌려보고, 두세 번 걸리는 걸 보고, 왜 나머지 일곱 번은 안 걸리지를 물어봐야 나오는 문장이다.
내가 실제로 한 건 이거다. 돌려본다 → 안 걸린다 → 다시 돌려본다 → 걸린다 → 뭐가 달랐지 → 아까는 캐시된 곡이었고 지금은 스트리밍 곡이다 → 그럼 미리 받는 타이밍이 다른 거 아닌가 → 코드 확인 → 맞다.
이 과정 전체가 대화다. 그리고 이 대화가 없었으면 그 버그는 "가끔 그런다"로 남았다.
"아직 구현하지 말고"
원문에서 내가 제일 걸린 문장은 따로 있다.
그리고 "주석 반영해서 업데이트해. 아직 구현하지 말고"라고 지시하죠.
이 지시를 왜 하는지는 안다. 계획이 덜 여물었는데 코드가 쏟아지면 되돌리기 번거로우니까. 그 걱정 자체는 타당하다.
그런데 이 문장이 하는 일을 뜯어보면, 읽는 것과 쓰는 것을 강제로 분리하는 것이다. 계획을 다듬는 동안에는 코드를 건드리지 마라. 다 정해지면 그때 한꺼번에 써라.
그런데 계획을 검증할 수 있는 방법은 대개 코드를 읽는 것뿐이다. 앞의 음소거 건이 그랬다. 승인된 설계가 틀렸다는 걸 알려면 두 가지 사실이 필요했다. 그 변수의 초기값이 무조건 false라는 것, 그리고 진짜 기준값이 다른 계층에 있다는 것. 둘 다 파일을 열어야 아는 사실이다. "아직 구현하지 말고"를 엄격하게 지키면, 계획이 틀렸다는 걸 계획 단계에서는 영원히 알 수 없다.
물론 "읽기는 해도 되고 쓰지만 말라"는 뜻으로 해석할 수도 있다. 그러면 절충은 되는데, 그 순간 계획 단계와 구현 단계의 경계가 흐려진다. 코드를 읽어가며 계획을 고치는 과정이 이미 대화니까. 그러면 굳이 파일로 분리해서 라운드를 나눌 이유가 뭔가.
실제로 내가 그날 한 것 중 되돌린 건 거의 없다. git revert도 안 썼다. 계획이 틀린 걸 구현 중에 알았고, 알자마자 고쳤고, 왜 고쳤는지를 커밋 메시지에 남겼다. 되돌릴 일이 안 생긴 이유는 계획이 좋아서가 아니라, 틀린 계획을 오래 붙들고 있지 않아서다.
이건 프롬프트 엔지니어링의 잔상이다
여기까지가 사례고, 이제 내가 진짜 하고 싶은 얘기다.
research.md → plan.md → "주석 반영해서 업데이트해. 아직 구현하지 말고" → "plan.md대로 모두 구현해". 이 절차가 왜 생겼는지 생각해보면 답이 하나밖에 없다. 모델을 중간에 못 고친다는 전제 위에서만 말이 되는 절차다.
한 번의 지시로 최대한 정확한 결과를 뽑아야 하니까, 지시를 넣기 전에 완벽하게 조립한다. 컨텍스트를 미리 다 채워 넣고, 조건을 빠짐없이 열거하고, 예시를 붙이고, 출력 형식을 못 박는다. 그게 프롬프트 엔지니어링이었다.
그때는 그게 기술이었다. 역할을 부여하고("당신은 시니어 개발자입니다"), 단계별로 생각하라고 시키고, 예시를 몇 개 붙이고, 출력 형식을 JSON으로 못 박고, 중요한 지시는 대문자로 반복하고. 이런 게 노하우로 유통됐다. 왜냐면 두 번째 기회가 없었기 때문이다. 대화가 길어지면 앞을 잊어버렸고, 중간에 방향을 틀면 엉뚱한 데로 갔고, 컨텍스트 창이 좁아서 왔다 갔다 할 여유 자체가 없었다. 결과가 틀리면 프롬프트를 고쳐서 처음부터 다시 돌리는 수밖에 없었다. 그러니 처음에 다 넣는 게 합리적이었다.
이 계보는 한 번 따라가본 적이 있다. 프롬프트에서 컨텍스트로, 컨텍스트에서 하네스로, 그리고 하네스 위에 한 층 더 얹히는 아웃터 루프까지. 그때 내 레포를 트리거·토폴로지·verifier·stop rule 네 자리로 갈라봤는데, 계획서가 앉을 만한 칸은 트리거다. 무슨 일감을 물릴지 정하는 자리. 그런데 plan.md를 그대로 구현하라고 시키는 순간 이 문서가 verifier 칸까지 넘본다. 다음 걸음을 정하려고 쓴 문서가 결과를 판정하는 기준을 겸하게 된다.
plan.md는 그 습관의 연장이다. 프롬프트를 정교하게 조립하는 대신, 계획서를 정교하게 조립하는 것뿐이다. 형태만 파일로 바뀌었지 사고 구조는 똑같다. 한 방에 넣어야 한다.
그 전제가 이미 유효하지 않다. 지금 모델은 대화 중간에 고쳐진다. "아니 그거 말고" 한 마디로 방향이 바뀌고, 세 턴 전에 내가 잘못 말한 걸 지적하면 알아듣는다. 컨텍스트도 남아돈다. 무엇보다, 앞에서 본 것처럼 그렇게 쌓인 맥락은 캐시에서 십분의 일 값으로 다시 읽힌다.
사람들이 아직도 아이디어 정리조차 프롬프트로 하려고 하는 게 이 잔상 때문이라고 본다. 생각을 정리해서 프롬프트에 담아 넣으려고 한다. 그런데 이제는 순서가 반대다. 정리가 안 된 채로 대화를 시작해도 되고, 정리는 대화 안에서 일어난다. 그게 이 도구가 예전 도구와 다른 지점이다.
그리고 이건 원래 사람이 일하는 방식이기도 하다. 회사에서 새 기능을 만들 때, 기획자가 완성된 명세서를 던지고 개발자가 그대로 구현하는 회사는 없다. 회의를 하고, 애매한 부분을 짚고, "그럼 이 경우는요?"를 서른 번 묻고, 그러다 요구사항 자체가 바뀐다. 그 회의가 낭비인가. 그게 일이다. 명세서는 회의의 입력이 아니라 출력이다.
예상되는 반박 두 가지
"그건 그냥 계획을 못 세운 거 아니냐."
제일 그럴듯한 반박이다. 진짜 좋은 research.md였다면 "미리 준비해둔 재생기가 설정 변경 대상에서 빠져 있다"는 구조적 결함을 사전에 잡아냈어야 하지 않나.
그게 가능하려면 계획 단계에서 코드베이스 전체를 정확하게 읽어야 한다. 그런데 그건 이미 구현 단계에서 하는 일이다. 문제를 밀어낸 것뿐이지 없앤 게 아니다. 그리고 실제로 그 결함을 잡으려면 재생 슬롯이 몇 개인지, 승격이 어디서 일어나는지, 각 설정이 어느 계층에 저장되는지를 다 알아야 하는데, 이건 "코드베이스를 분석해서 보고서를 써라"로 나오는 깊이가 아니다. 다섯 번 걸려가면서 알게 된 깊이다.
더 근본적으로는, 이 반박이 성립하려면 완벽한 계획이 가능하다는 전제가 필요하다. 그 전제가 참이면 계획서 방식이 옳다. 나는 발견이 본체인 작업에서 그 전제가 거짓이라고 보는 것이고, 앞의 사슬이 그 근거다.
"세션이 길어지면 모델이 앞을 까먹지 않나."
이건 실제로 있는 문제다. 다만 해법이 plan.md가 아니다. 요즘 모델은 컨텍스트가 차오르면 서버 쪽에서 앞부분을 요약해 접는다. 나도 긴 세션에서 "지금까지 정한 것 정리해봐"를 주기적으로 시키는데, 이건 계획서를 만드는 게 아니라 접히기 전에 중요한 걸 다시 최근 위치로 끌어오는 동작에 가깝다.
그리고 앞서 본 캐시 구조가 여기서도 작동한다. 대화 안에서 정리하면 그 정리본이 다음 턴부터 캐시된 접두부의 일부가 된다. 파일로 빼서 새 세션에서 다시 읽히면 그 이득이 사라진다.
그럼 문서를 안 쓰냐
여기서 오해를 막아야 한다. 나는 문서를 안 쓴다고 말하는 게 아니다.
방금 그 오디오 앱 레포는 2,075 커밋짜리다. docs/ 아래에 40개 넘는 문서가 있다. 아키텍처, 테스트 전략, IPC 계약, 상태 관리, 코딩 컨벤션, 그리고 foundation/invariants.md. 이 시스템이 항상 지켜야 하는 불변식을 적어둔 파일이다. 루트에 CLAUDE.md와 AGENTS.md가 있고, .claude/ 아래에 스킬 14개와 훅과 워크플로우가 있다. 문서가 없는 레포가 전혀 아니다.
그런데 plan.md가 없다. research.md도 없다.
이게 우연이 아니라고 생각한다. 저 40여 개는 전부 남는 문서다. 다음 사람이 읽을 것들이고, 반년 뒤에도 유효할 것들이다. plan.md는 그 반대다. 이번 작업이 끝나면 버려지고, 안 버리면 썩는다.
쓸 가치가 있는 문서와 없는 문서를 가르는 기준이 그거다. 이 문서를 6개월 뒤에 누가 읽을 일이 있나. 있으면 쓴다. 없으면 그건 대화에서 하면 될 얘기를 파일로 옮겨 적은 것뿐이다.
그리고 재밌는 건, 그날 첫 번째 커밋(18:05)이 코드를 고치면서 아키텍처 결정 기록(ADR) 하나도 같이 정정했다는 점이다. 그 ADR에 적힌 함수 동작 설명이 지금 코드와 달랐다. 버그를 고치는 과정에서 문서가 낡았다는 걸 발견하고 같이 손봤다.
이 순서가 중요하다. 구현이 문서를 갱신했다. 문서가 구현을 지시한 게 아니라.
Not-Fixed — 계획서 대신 우리가 쓰는 것
저 레포에는 Not-Fixed라는 관례가 있다. 커밋 메시지에 이번에 일부러 안 고친 것을 적어두는 칸이다. 알고는 있는데 이 커밋의 범위가 아니거나, 지금 손대면 위험하거나, 다음에 하는 게 맞다고 판단한 것들.
앞의 볼륨 커밋 메시지에 이런 줄이 있다.
"…의 Not-Fixed 항목이 요구한 '목표값 전용 setter + 페이드 소유권 조정'이 각각 …에 해당한다."
그리고 마지막 출력 장치 커밋에는 "…가 Not-Fixed로 남긴 '같은 결함의 네 번째 반복' 항목이다"가 있다.
이게 뭐냐면, 커밋끼리 대화하고 있는 것이다. 앞 커밋이 "여기까지 했고, 이건 남겼다"고 적으면, 뒤 커밋이 그걸 받아서 "그 남긴 것 처리했다"고 답한다. 계획을 앞에 몰아두는 대신, 매 단계에서 지금 아는 만큼의 미결을 기록해두고 다음 단계가 그걸 이어받는다.
계획서와 다른 점이 두 군데다.
하나는 쓰는 시점이다. 계획서는 가장 모르는 시점에 쓴다. Not-Fixed는 그 작업을 막 끝낸, 가장 잘 아는 시점에 쓴다.
다른 하나는 틀릴 수가 없다는 것이다. 계획서는 "이렇게 할 것이다"라는 예측이라 틀릴 수 있고, 틀린 채로 남으면 앞의 active.md 사례처럼 썩는다. Not-Fixed는 "이건 안 했다"는 사실 기술이다. 사실은 안 썩는다.
이게 두 커밋에만 붙어 있다는 건 인정한다. 아직 관례라기보다 습관에 가깝다. 그래도 방향은 분명하다. 계획을 앞에 쌓는 대신 미결을 뒤에 남기는 쪽.
얼어붙은 문서가 썩는 속도
문서가 썩는 얘기가 나온 김에, 내 쪽 사례도 하나 꺼내겠다. 내 자동화 레포에는 세션 간에 상태를 넘기는 active.md가 있다. 다음 세션에서 이어서 할 일을 적어두는 파일이다. 거기에 이런 항목이 오래 박혀 있었다.
손대지 말 것 (고유 정체성): robot/ 10편 · game1/ 01~20
지난주에 이 지침을 명시적으로 폐기했다. 실제 데이터를 뽑아보니 손대지 말라고 적어둔 그 30건이 정확히 문제의 원인이었다. 길이 중앙값 3,600자, 이미지 0장, 내부링크 0개. 전부 내렸다. 지금 active.md에는 이렇게 적혀 있다. "이 지침은 폐기됐다. 실수로 되살리지 말 것."
같은 파일에 6월 14일자로 적어둔 판단이 하나 더 있었는데, 그것도 틀렸다. 틀린 기록이 6주 동안 그대로 남아서, 그 사이 세션들이 전부 틀린 전제 위에서 판단했다.
문서는 쓴 순간부터 낡기 시작한다. 코드는 컴파일러가 잡아주고 테스트가 잡아주는데, 문서는 아무도 안 잡아준다. 계획서를 ground truth로 대접하면 이렇게 된다. 검증할 방법이 없는 문장을 기준 삼아 계속 일하게 된다.
대화는 이 문제가 덜하다. 방금 나온 도구 결과가 문장을 즉시 반박하기 때문이다. 파일을 읽었는데 계획서에 적힌 것과 다르면 그 자리에서 드러난다. 문서에는 그런 마찰이 없다.
검문소와 통행증 발급처가 같으면
같은 얘기를 다른 각도에서 한 번 더 하겠다. 내 하네스에 원래 배포 게이트가 있었다.
글을 발행하기 전에 문체 검사를 돌리고, 그 결과를 -ai-check.md 파일에 남기게 했다. 그리고 발행 스크립트가 그 파일에서 판정: 합격 문자열을 grep 해서, 없으면 발행을 막았다. 기계적으로 강제되는 품질 게이트. 꽤 그럴듯했다.
지난주에 이걸 통째로 들어냈다. 이유는 하나다. 그 판정: 합격이라는 문자열을, 검사받는 에이전트가 직접 썼기 때문이다.
검문소와 통행증 발급처가 같았다. 게이트가 검증하고 있던 건 글의 품질이 아니라 "합격이라고 적혀 있느냐"였고, 그 문자열은 통과시키고 싶은 쪽이 쓰는 것이었다. 점수도 마찬가지였다. 항목별로 점수를 매겨 합산하는 산술이 있었는데, 그 점수 역시 자기가 매긴 거였다. 숫자가 붙어 있으니 객관적으로 보였을 뿐이다.
그래서 기준을 바꿨다. 코드로 강제할 수 있는 건 모델 출력과 무관한 사실뿐이다. 커밋 훅의 시크릿 스캔이 그 예다. 파일에 AWS 키 패턴이 있느냐 없느냐는 모델이 뭐라고 주장하든 안 바뀐다. 반면 "이 글이 사람이 쓴 것처럼 읽히느냐"는 그런 사실이 아니다. 그래서 게이트를 없애고 체크리스트로 바꿨다. 판정 라벨 대신 줄 단위 수정 내역을 남긴다. "합격"이 아니라 "37번째 줄의 이 표현을 이걸로 바꿨다"를 남기는 것이다.
plan.md를 ground truth로 대접하는 게 정확히 같은 실수다. 그 문서를 누가 썼나. 그 계획대로 구현할 쪽이 썼다. 계획서가 스스로를 검증할 수는 없다. 검증하는 건 실행 결과이고, 결과를 계획에 되먹이는 건 대화다.
이 병이 문서에만 붙는 것도 아니다. 이 글을 쓰고 일주일쯤 뒤에 벤치마크 1위와 2위의 격차를 재보다 모델 대신 내 하네스 쪽을 세어본 적이 있는데, 거기서도 같은 모양이 나왔다. 게이트가 파일로 존재한다는 사실과 그 게이트가 무언가를 검증하고 있다는 사실은 별개였다.
그래서 "계획을 세우지 말라"가 아니다. 계획에 그것이 감당할 수 없는 권위를 주지 말라는 것이다. 계획은 다음 한 걸음을 정하는 데 쓰는 것이지, 다섯 걸음 뒤에 알게 될 사실을 미리 이기라고 있는 게 아니다.
그럼 나는 어떻게 하나
내 방식은 이렇다.
계획은 대화로 세운다. 문서를 먼저 쓰라고 시키지 않는다. 뭘 하려는지 말하고, 되묻게 하고, 애매한 걸 같이 좁힌다. 이 단계에서 내가 잘못 알고 있던 게 자주 드러난다.
어느 정도 수렴하면 "지금까지 정한 것 정리해봐"라고 한다. 이게 사실상 plan.md 역할을 하는데, 파일이 아니라 대화 안에 남긴다. 그래야 다음 턴에 반박당할 수 있다. 그리고 캐시에 얹혀서 싸게 다시 읽힌다.
그다음 그걸 기반으로 작업시키는데, 여기서 뽐뿌 글과 갈라진다. 나는 "그대로 구현해"라고 하지 않는다. 구현하다가 정리한 내용이 틀린 게 나오면 멈추고 말해달라고 한다. 앞의 음소거 커밋이 정확히 그 경우였다.
그리고 결과를 보고 다시 대화로 정정한다. 돌려보고, 이상한 걸 짚고, 왜 그런지 묻는다. 여기서 나온 게 "10회 중 2~3회"고 "네 번째 반복"이다.
마지막으로 남을 것만 파일로 뺀다. 불변식, 아키텍처 결정, 함정. 이번 작업이 끝나도 유효한 것들만. 그래서 저 레포에 invariants.md는 있고 plan.md는 없다.
말로만 하면 추상적이니, 그날 두 번째 버그를 어떻게 잡았는지 그대로 적어보겠다.
QA가 "최초 1회만 소리가 안 난다"고 올렸다. 나는 이걸 계획서로 만들지 않았다. 대신 이렇게 갔다. 재현 조건을 좁힌다 → 로그를 뜬다 → 12시 3분 24초에 올라간 재생 요소가 81초 동안 음소거 상태였고 그 뒤 전환은 전부 정상이라는 걸 확인한다 → "최초 1회만"이라는 QA의 표현과 일치한다 → 그럼 준비 단계에서 음소거 상태를 못 받는 거 아닌가 → 코드 확인 → 맞다 → 그럼 전환 시점에 마지막 값을 다시 적용하면 되겠네 → 여기서 승인 → 구현하러 들어가서 그 "마지막 값"의 초기값이 false인 걸 발견 → 설계를 바꿔서 구현 → 왜 바꿨는지 커밋에 기록.
승인이 중간에 있다. 없앤 게 아니다. 다만 승인한 게 최종 설계가 아니라 다음 한 걸음이었고, 그래서 그 한 걸음이 틀렸다는 게 드러났을 때 되돌릴 게 없었다.
뽐뿌 글과 이 흐름의 차이는 문서를 쓰느냐 마느냐가 아니다. 승인을 어디에 놓느냐다. 계획 전체를 승인하면 그 계획이 틀렸을 때 계획을 방어하게 된다. 한 걸음씩 승인하면 틀린 걸 버리는 비용이 한 걸음어치다.
뽐뿌 글이 맞는 경우
공정하게 짚자. 저 절차가 맞는 상황도 있다.
작업 범위가 진짜로 미리 확정되는 경우. 명세가 외부에서 고정돼 내려오고, 구현이 기계적인 번역에 가까운 일. 예를 들어 API 스펙 문서를 받아서 클라이언트 코드를 생성하는 작업. 발견할 게 별로 없으니 계획을 앞에 몰아두는 게 낫다.
여러 명이 나눠서 하는 경우. 대화는 나 하나의 머릿속에만 남는다. 세 명이 나눠 붙는 작업이면 어딘가에 적어야 하고, 그때 계획서는 계획서가 아니라 인터페이스 계약서다. 그건 남는 문서다.
모델을 못 믿는 영역. 실수 비용이 큰 곳. 마이그레이션, 삭제, 배포 같은 것들이다. 여기서는 미리 적어두고 사람이 한 줄씩 승인하는 게 맞다.
공통점이 보인다. 셋 다 발견할 게 적거나, 발견을 남에게 전달해야 하거나, 되돌릴 수 없는 경우다. 반대로 말하면, 발견이 작업의 본체인 일에서는 계획을 앞에 몰아두는 만큼 손해다. 디버깅이 그렇고, 리팩터링이 그렇고, 없던 걸 새로 만드는 게 그렇다. 아직 모르는 상태에서 내린 결정이 나중에 알게 된 사실을 이기게 되니까.
마무리
그날 다섯 개를 고치고 나서 든 생각은, 만약 내가 plan.md부터 썼으면 어땠을까였다.
아마 이렇게 적었을 것이다. "자동 곡 전환 후 앨범 이미지가 갱신되지 않는 버그를 수정한다. 상태 보관소의 썸네일 설정 함수를 확인하고, 승격 지점에서 갱신되도록 한다." 그리고 그대로 구현했을 것이다. 한 시간이면 끝났을 것이다.
그리고 나머지 네 개는 그대로 남았을 것이다. 계획서에 없었으니까.
토큰을 아꼈을까? 그날 하루로 보면 아꼈다. 근데 음소거 버그는 다음 주에, 볼륨 버그는 그다음 주에 다시 처음부터 파고들었을 것이다. 매번 새 세션에서, 캐시 없이, 컨텍스트를 처음부터 쌓아가면서.
대화는 낭비가 아니다. 낭비인 건 같은 걸 여러 번 새로 발견하는 쪽이다.
댓글
댓글 쓰기