DHH는 날짜를 짚었고 저는 AI에 손코딩을 넘긴 날을 모릅니다

이미지
DHH가 올해 Rails World에서 한 발표를 요약한 영상을 몇 편 봤습니다. 원본 발표를 처음부터 끝까지 본 건 아니고, 한국어로 정리해 준 유튜브 영상 네 편을 이어서 봤습니다. 발표를 남이 정리해 준 걸로 보고 대충 알았다고 넘어가는 것도, 생각해 보면 요즘 제가 코드를 대하는 방식이랑 비슷합니다. 원본을 다 읽지 않고 정리된 결과를 보고, 이상한 데가 있으면 그때 찾아봅니다. 내용이 많았는데 제일 오래 남은 건 날짜 하나였습니다. 2025년 11월 24일. 영상에 따르면 DHH는 이날을 "우리 시대의 코닥 브라우니"라고 불렀다고 합니다. Claude Opus 4.5가 나온 날입니다. 1900년에 1달러짜리 카메라가 나오면서 초상화를 그리던 화가들이 일감을 잃고 방향을 틀었던 것처럼, 그날부터 프로그래머의 일이 바뀌었다는 얘기였습니다. 그리고 기술은 비탈길처럼 매끄럽게 오르는 게 아니라 한참 평평하다가 어느 날 한 칸 뛰는 계단처럼 온다고 했습니다. 영상을 보면서 저도 모르게 제 날짜를 찾고 있었습니다. 저는 언제였지. 생각이 안 났습니다. 마지막으로 직접 친 코드 어림잡을 수 있는 건 있습니다. 2025년 9월쯤부터는 제가 코드를 직접 쓴 기억이 없습니다. DHH가 짚은 날보다 두 달쯤 앞입니다. 이걸 날짜라고 하기는 좀 어렵습니다. 9월 몇 일에 무슨 일이 있어서 그날부터 안 쓴 게 아니라, 거꾸로 짚어 올라가다 보니 그쯤부터는 기억이 비어 있다는 정도입니다. 어느 날 키보드에서 손을 뗀 게 아니라 손으로 친 마지막 줄이 언제였는지 생각해 보니 안 떠오르는 겁니다. 두 달 앞이라고 해서 제가 남들보다 빨랐다는 뜻도 아닙니다. 그때 무슨 모델을 쓰고 있었는지도 정확히 기억이 안 납니다. DHH는 특정 모델이 나온 날을 짚었는데 저는 어떤 모델 때문에 넘어왔는지를 말할 수가 없습니다. 모델이 바뀐 날은 발표가 있으니 찾아보면 나오겠지만, 제가 바뀐 날은 아무 데도 적혀 있지 않습니다. 커밋 기록을 뒤져 보면 뭔가 나올...

Claude Code plan 모드로 plan.md 를 쓸 때 놓치는 것 - 승인한 설계가 틀렸던 사례

밝은 책상 위에 손으로 쓴 설계 노트가 펼쳐져 있고 한 항목에만 크게 가위표가 쳐진 클로즈업

넉 달째 개인 프로젝트를 에이전트로 돌리고 있고, 회사 코드도 상당 부분 AI와 같이 짭니다. 그래서 「계획 없이 코딩하면 토큰 낭비하는 이유」라는 글이 조회 2만을 넘긴 걸 보고 한참 들여다봤습니다. 제가 매일 하는 일 얘기니까요.

요지는 이렇습니다. research.md로 코드베이스를 먼저 분석시키고, 그걸 바탕으로 plan.md를 쓰고, 주석을 달아가며 계획을 몇 번 다듬은 다음, "plan.md대로 모두 구현해"라고 시킵니다. 그러면 "AI는 새로 판단할 필요 없이 계획 충실하게 코드를 쓰죠"라고 돼 있습니다.

읽고 나서 든 생각은 하나였습니다. 저는 정확히 반대로 일하고 있는데, 그럼 제가 토큰을 버리고 있는 건가.

그래서 지난주 커밋 로그를 열어봤습니다. 7월 29일 저녁, 회사에서 맡고 있는 데스크톱 오디오 앱 버그를 다섯 개 고친 날입니다. 첫 커밋이 저녁 6시 5분, 마지막이 밤 9시 52분이었으니 네 시간이 안 걸렸습니다. 다섯 개는 이런 것들이었습니다. 자동으로 다음 곡으로 넘어가면 앨범 이미지가 이전 곡 것으로 남는 것, 음소거를 바꿔도 반영이 안 되는 것, 음질을 바꿔도 첫 전환에서는 이전 음질로 나오는 것, 볼륨이 이전 값으로 돌아가는 것, 출력 장치를 바꿔도 다음 곡에 반영이 안 되는 것.

지금 나란히 놓고 보면 하나의 문제입니다. 재생 중에 바꾼 설정이, 자동으로 넘어간 다음 곡에 안 실립니다. 미리 준비해둔 다음 곡 재생기가 설정 변경 대상에서 빠져 있었고, 그게 올라오는 순간 준비할 때의 옛 상태가 되살아났습니다. 원인은 하나인데 증상이 다섯 가지로 나온 겁니다.

그런데 이걸 plan.md에 미리 적을 수 있었을까 생각해보면, 못 적었을 것 같습니다. 다섯 개를 꿰는 문장은 네 번째 커밋을 쓰면서 겨우 나왔습니다. 그 전까지는 그냥 앨범 이미지 버그였고, 그다음엔 음소거 버그였습니다. 마지막 커밋 메시지에 제가 직접 이렇게 써놨습니다.

"…가 Not-Fixed로 남긴 '같은 결함의 네 번째 반복' 항목이다"

네 번째에서야 패턴에 이름이 붙었습니다. 그 이름을 계획할 때 알았으면 버그 다섯 개가 아니라 리팩터링 하나였을 겁니다. 알 방법이 없어서 다섯 번 걸렸습니다.

저 레포에서 AI로 코드를 짜는 건 예외가 아닙니다. 커밋 메시지에 AI-Use: yes와 기여도를 남기는 관례가 있는데, 그 표시가 달린 커밋이 칠백 개가 넘습니다. 그날 세 번째 커밋도 AI-Contribution: 100%로 찍혀 있습니다.

"낭비"라는 단어

저 글이 말하는 순서 자체는 나쁘지 않습니다. research.md, plan.md, 주석 반복, 구현, 피드백. 특히 개발자가 계획을 직접 보고 승인한 다음에야 AI가 코드를 쓴다는 대목은 저도 대체로 그렇게 생각합니다.

걸린 건 제목에 들어간 단어였습니다. 낭비.

이 단어는 대화에 쓰는 토큰이 손실이라는 뜻입니다. 계획을 세우고 그대로 실행하는 게 효율이고, 왔다 갔다 하면서 정하는 건 새는 거라는 뜻이죠. 정말 그런지 궁금해서 그냥 세어봤습니다. 이 블로그를 돌리는 레포에 세션 로그가 다섯 개, 400턴 넘게 남아 있었습니다. 들어간 입력 토큰의 97.5%가 캐시에서 다시 읽힌 거였습니다. 새로 캐시에 쓴 게 2.5%쯤이고 캐시를 아예 안 탄 건 거의 없었습니다.

Anthropic의 프롬프트 캐싱은 캐시에서 읽을 때 기본 입력 값의 10분의 1쯤을 받습니다. 처음 캐시에 쌓을 때는 조금 더 내고, 그 뒤로는 10분의 1로 계속 읽는 겁니다.

그러니까 대화가 길어져서 쌓인 맥락은, 길어졌기 때문에 오히려 싸졌습니다. 같은 세션 안에서 앞의 맥락을 반복해서 읽는 값은 정가가 아니라 10분의 1이고, 제 경우엔 거의 전부가 그 값으로 처리됐습니다.

비싼 건 오히려 새 세션을 열고 plan.md를 처음부터 읽히는 쪽입니다. 캐시가 없으니 새로 쌓거나 정가를 냅니다. 세션을 끊고 문서로 넘겨받는 방식은, 토큰 청구서만 놓고 봐도 대화를 이어가는 쪽보다 나을 게 없어 보였습니다. 제 숫자가 다른 사람한테도 그대로 나올지는 모르겠습니다. 캐시가 얼마나 잘 걸리는지는 프롬프트를 어떻게 짜느냐에 따라 꽤 다를 테니까요. 그래도 대화하는 데 쓴 토큰이 그냥 버려지는 게 아니라는 건 제 로그에서는 분명했습니다.

낭비라는 말이 좀 묘하게 들리는 건, 그 단어가 돈 얘기처럼 들리면서 실제로는 돈을 안 세고 하는 말이라서인 것 같습니다. 대화가 길면 왠지 많이 쓴 것 같고, 계획서 한 장 쓰고 한 번에 끝내면 왠지 아낀 것 같습니다. 그 느낌이 청구서와 꼭 같지는 않았습니다.

다섯 번째는 네 번째가 있어서 고쳤습니다

다섯 개가 사실 하나였다고 했는데, 하나로 정리되는 과정도 순서대로였습니다.

네 번째, 볼륨 건을 고칠 때 원칙이 하나 나왔습니다. 준비만 돼 있고 아직 소리를 안 내는 재생기도 볼륨 변경을 받아야 하는데, 그렇다고 그 재생기가 실제로 들리는 크기까지 건드리면 안 됩니다. 그래서 "목표 볼륨"과 "실제로 들리는 볼륨"을 나눴습니다. 목표값은 전체에 보내고, 실제 적용은 지금 재생 중인 쪽만. 그런데 하나가 더 있었습니다. 준비 중인 재생기가 자기 볼륨을 위로 거꾸로 보고하면서 저장된 설정을 낡은 값으로 덮어쓰고 있었습니다. 그래서 그 보고도 목표값을 읽게 바꿨습니다.

한 시간쯤 뒤에 다섯 번째, 출력 장치 건에 붙었습니다. 증상은 같은 모양이었습니다. 출력 장치를 바꿔도 준비 중인 재생기는 자기가 만들어질 때의 장치에 붙어 있고, 올라오면 그 곡 한 곡만 이전 장치로 나갑니다. 앞에서처럼 전체에 보내면 될 것 같았습니다.

그러면 새 문제가 생깁니다. 준비 중인 재생기의 출력 장치 설정이 실패하면 그 실패가 "기본 장치로 돌아가라"는 통지를 쏘고, 그 통지가 기준값을 기본 장치로 되돌리고, 그게 다시 퍼지면서 멀쩡히 재생 중인 재생기까지 기본 장치로 끌어내립니다. 안 들리는 재생기 하나가 실패했다고 들리는 재생기를 끌고 내려가는 겁니다.

그래서 통지할 수 있는 건 재생 중인 쪽만으로 했습니다. 준비 중인 재생기는 장치를 바꾸되, 실패해도 통지하지 않습니다. 커밋 메시지에 이렇게 적었습니다.

"들리지 않는 엔진은 라우팅 기준값을 덮을 자격이 없다. 볼륨 역보고를 목표값만 읽게 한 것과 같은 논지다."

"같은 논지다." 다섯 번째 버그의 해법은 네 번째 버그를 고치다 나온 원칙을 한 번 더 쓴 거였습니다. 안 들리는 재생기는 전체 상태를 덮을 자격이 없다. 볼륨에서 한 번, 출력 장치에서 또 한 번. 그런데 이 원칙은 네 번째를 고치기 전에는 없었습니다. 그날 저녁 8시 40분 커밋이 만든 겁니다.

앨범 이미지를 고칠 때는 아무 패턴도 안 보였습니다. 음소거를 고치면서 승인된 설계가 틀렸다는 걸 알았고, 음질을 고치면서 재현이 잘 안 되는 이유를 알았고, 볼륨을 고치면서 원칙이 생겼고, 출력 장치에서 그 원칙 덕분에 새 문제를 미리 피했습니다. 저녁 6시에 쓴 plan.md에 마지막 해법이 들어 있을 수 있었을까요. 그 해법은 네 번째의 부산물이고, 네 번째는 세 번째를 고치면서 코드를 읽다가 나왔습니다. 계획을 아무리 꼼꼼히 세워도 이 사슬은 안 나올 것 같습니다. 계획하는 시점에는 사슬의 첫 칸도 안 보이니까요.

발견이 일의 대부분인 작업은 이렇게 생긴 것 같습니다. 앞의 결과가 뒤의 문제를 다시 정의합니다. 그런 일에서 계획을 앞에 몰아두면, 제일 모를 때 내린 결정이 제일 많이 알 때의 판단을 이기게 됩니다.

그날 저녁을 돌아보면 제가 제일 똑똑했던 건 밤 10시쯤이었습니다. 다섯 개를 다 고치고 나서야 이 코드가 어떻게 생겼는지 제대로 알았습니다. 저녁 6시의 저는 그보다 훨씬 몰랐습니다. 계획서 방식은 6시의 저한테 제일 큰 권한을 주고 10시의 저한테는 그대로 따르라고 하는 셈인데, 거꾸로 된 것 같습니다.

승인받은 설계가 틀렸던 커밋

그날 두 번째 커밋, 음소거 건의 메시지에는 이런 소제목이 달려 있습니다.

승인받은 설계와 다르게 구현한 부분 (이 커밋에서 가장 봐야 할 곳)

제가 붙인 소제목입니다. 승인된 설계는 다음 곡을 올리는 시점에 마지막 음소거 값을 다시 적용하는 거였습니다. 합리적으로 들립니다. 그런데 그대로 하면 문제가 생겼습니다. 그 "마지막 음소거 값"은 처음 값이 무조건 false였고, 음소거의 진짜 기준값은 프런트엔드가 아니라 Rust 쪽 설정 저장소에 있었습니다. 승인된 대로 짜면 처음 값이 false인 변수를 기준으로 삼고 진짜 기준값은 무시하게 됩니다. 버그가 그대로 나갑니다. 그래서 준비 중인 재생기까지 포함해서 전체 목록에 적용하는 쪽으로 일부러 다르게 짰습니다.

뽐뿌 글의 그 문장이 다시 떠올랐습니다.

"이때 AI는 새로 판단할 필요 없이 계획 충실하게 코드를 쓰죠."

이 커밋에서는 계획에 충실했으면 버그가 나갔습니다. 계획이 틀렸다는 걸 안 건 계획할 때가 아니라 구현하러 들어가서 실제 코드를 읽었을 때였습니다. 그 변수의 처음 값이 뭔지, 진짜 기준값이 어디 있는지는 파일을 열어봐야 아는 거고, 계획서를 쓸 때는 아무도 몰랐습니다.

저는 이게 사고라기보다 원래 그런 거라고 봅니다. 계획은 모르는 게 많을 때 세우고 구현은 아는 게 늘었을 때 합니다. 그 사이에 알게 된 게 있으면 계획을 고치는 게 맞고, 계획을 지켜야 할 이유는 별로 없는 것 같습니다.

열 번에 두세 번만 나오는 버그

세 번째 커밋, 음질 건에는 이런 줄이 있습니다.

"미리 받는 시점이 곡마다 달라(캐시 곡은 즉시, 스트리밍 곡은 종료 4초 전) 10회 중 2~3회만 재현됐다."

이런 버그에 research.md를 쓸 수 있을까요. 코드베이스를 아무리 가만히 분석해도 "다음 곡을 미리 받는 시점이 캐시 여부에 따라 달라서 특정 타이밍에만 걸린다"는 문장은 안 나올 것 같습니다. 열 번 돌려보고, 두세 번 걸리는 걸 보고, 왜 나머지 일곱 번은 안 걸리지를 물어봐야 나옵니다.

실제로 한 것도 그랬습니다. 돌려보고, 안 걸리고, 다시 돌려보고, 걸리고, 뭐가 달랐지, 아까는 캐시된 곡이었고 지금은 스트리밍 곡이네, 그럼 미리 받는 타이밍이 다른 거 아닌가, 코드를 보니 맞았습니다. 이게 다 대화였습니다. 이 대화가 없었으면 그 버그는 "가끔 그런다"로 남았을 겁니다. 이런 버그는 원인을 짐작하는 것보다 안 걸리는 일곱 번을 보는 게 더 중요한 것 같습니다. 걸리는 세 번만 보면 뭐가 문제인지 모르는데, 안 걸리는 일곱 번이랑 나란히 놓으면 차이가 보입니다. 그 나란히 놓는 걸 문서로 미리 할 수는 없습니다.

"아직 구현하지 말고"

원문에서 제일 걸린 문장은 따로 있었습니다.

그리고 "주석 반영해서 업데이트해. 아직 구현하지 말고"라고 지시하죠.

왜 이렇게 시키는지는 압니다. 계획이 덜 여물었는데 코드가 쏟아지면 되돌리기 번거로우니까요. 그 걱정은 맞습니다.

그런데 이 문장이 실제로 하는 일은 읽는 것과 쓰는 것을 억지로 떼어놓는 겁니다. 계획을 다듬는 동안엔 코드를 건드리지 말고, 다 정해지면 그때 한꺼번에 써라.

그런데 계획이 맞는지 확인하는 방법은 대개 코드를 읽는 것뿐입니다. 음소거 건이 그랬습니다. 승인된 설계가 틀렸다는 걸 알려면 변수의 처음 값과 진짜 기준값이 어디 있는지를 알아야 했고, 둘 다 파일을 열어야 아는 거였습니다. "아직 구현하지 말고"를 엄격하게 지키면 계획이 틀렸다는 걸 계획 단계에서는 끝까지 모릅니다. 읽기는 해도 되고 쓰지만 말라는 뜻이라면 좀 낫긴 한데, 그러면 코드를 읽어가며 계획을 고치는 것 자체가 이미 대화라서 굳이 파일로 나눠서 라운드를 돌릴 이유가 뭔지 잘 모르겠습니다.

계획 단계와 구현 단계 사이에 벽을 세우면 벽 너머에 있는 정보가 계획 쪽으로 못 넘어옵니다. 그날 되돌린 건 거의 없었습니다. git revert도 안 썼습니다. 계획이 틀린 걸 구현하다가 알았고, 알자마자 고쳤고, 왜 고쳤는지를 커밋 메시지에 남겼습니다. 되돌릴 일이 안 생긴 건 계획이 좋아서가 아니라 틀린 계획을 오래 붙들고 있지 않아서였던 것 같습니다.

프롬프트 엔지니어링이 남긴 습관

research.md를 쓰고, plan.md를 쓰고, "아직 구현하지 말고"라고 하고, "plan.md대로 모두 구현해"라고 합니다. 이 절차가 왜 생겼을까 생각해보면 모델을 중간에 못 고친다는 전제가 깔려 있는 것 같습니다.

한 번 시켜서 최대한 정확한 결과를 뽑아야 하니까, 시키기 전에 완벽하게 조립합니다. 맥락을 미리 다 채우고, 조건을 빠짐없이 늘어놓고, 예시를 붙이고, 출력 형식을 못 박습니다. 그게 프롬프트 엔지니어링이었습니다.

그때는 그게 기술이었습니다. 역할을 주고("당신은 시니어 개발자입니다"), 단계별로 생각하라고 하고, 예시를 몇 개 붙이고, 출력을 JSON으로 못 박고, 중요한 건 대문자로 반복하고. 이런 게 노하우로 돌았습니다. 두 번째 기회가 없었기 때문입니다. 대화가 길어지면 앞을 잊었고, 중간에 방향을 틀면 엉뚱한 데로 갔고, 컨텍스트 창이 좁아서 왔다 갔다 할 여유도 없었습니다. 결과가 틀리면 프롬프트를 고쳐서 처음부터 다시 돌리는 수밖에 없었습니다. 그러니 처음에 다 넣는 게 맞았습니다.

이 흐름은 한 번 따라가본 적이 있습니다. 프롬프트에서 컨텍스트로, 컨텍스트에서 하네스로, 그리고 하네스 위에 한 층 더 얹히는 아웃터 루프까지. 그때 제 레포를 트리거, 토폴로지, verifier, stop rule 네 갈래로 나눠봤는데, 계획서가 들어갈 만한 칸은 트리거였습니다. 무슨 일을 맡길지 정하는 쪽이니까요. 그런데 plan.md대로 구현하라고 시키는 순간 이 문서가 verifier 칸까지 차지합니다. 다음 걸음을 정하려고 쓴 문서가 결과를 판정하는 기준까지 겸하게 됩니다.

plan.md는 그 습관이 이어진 것 같습니다. 프롬프트를 정교하게 조립하던 걸 계획서를 정교하게 조립하는 걸로 바꿨을 뿐이고, 모양만 파일이 됐지 생각하는 방식은 같습니다. 한 방에 다 넣어야 한다는 거요.

그 전제가 이제는 잘 안 맞습니다. 지금 모델은 대화 중간에 고쳐집니다. "아니 그거 말고" 한마디로 방향이 바뀌고, 세 턴 전에 제가 잘못 말한 걸 짚으면 알아듣습니다. 컨텍스트도 넉넉합니다. 그리고 그렇게 쌓인 맥락은 캐시에서 10분의 1 값으로 다시 읽힙니다.

아직도 아이디어 정리까지 프롬프트로 하려는 사람이 많은 것도 이 습관 때문인 것 같습니다. 생각을 다 정리해서 프롬프트에 담으려고 합니다. 저는 지금은 순서가 반대라고 봅니다. 정리가 안 된 채로 대화를 시작해도 되고, 정리는 대화 안에서 됩니다. 이 도구가 예전 도구와 다른 건 그 점인 것 같습니다.

원래 사람도 그렇게 일합니다. 회사에서 새 기능을 만들 때 기획자가 완성된 명세서를 던지고 개발자가 그대로 구현하는 회사는 없습니다. 회의를 하고, 애매한 걸 짚고, "그럼 이 경우는요?"를 서른 번 묻고, 그러다 요구사항 자체가 바뀝니다. 그 회의를 낭비라고 부르진 않습니다. 그게 일입니다. 명세서는 회의에 들어가는 게 아니라 회의에서 나오는 거라고 봅니다.

계획을 잘 세웠으면 미리 잡을 수 있었던 것 아니냐는 생각도 해봤습니다. 진짜 좋은 research.md였다면 준비 중인 재생기가 설정 변경 대상에서 빠져 있다는 걸 미리 잡았어야 하는 게 아닌가. 그런데 그걸 잡으려면 재생 슬롯이 몇 개인지, 어디서 올라오는지, 설정마다 어느 계층에 저장되는지를 다 알아야 하는데, 그건 "코드베이스 분석해서 보고서 써"로 나오는 깊이가 아니었습니다. 다섯 번 걸려가면서 알게 된 깊이였습니다. 그걸 계획 단계에서 다 하라는 건 구현 단계에서 하는 일을 앞으로 당겨오는 것뿐이고, 일이 없어지는 건 아닌 것 같습니다.

세션이 길어지면 모델이 앞을 잊는 건 실제로 있는 문제입니다. 요즘 모델은 컨텍스트가 차면 앞부분을 요약해서 접습니다. 저도 긴 세션에서는 "지금까지 정한 것 정리해봐"를 가끔 시킵니다. 계획서를 만드는 게 아니라, 접히기 전에 중요한 걸 다시 최근 쪽으로 끌어오는 거에 가깝습니다. 대화 안에서 정리하면 그 정리본도 다음 턴부터 캐시에 얹혀 싸게 읽힙니다. 파일로 빼서 새 세션에서 다시 읽히면 그 이득이 없어집니다.

남는 문서와 버려지는 문서

문서를 안 쓰는 건 아닙니다.

그 오디오 앱 레포는 커밋이 2천 개가 넘고, docs/ 아래에 문서가 40개 넘게 있습니다. 아키텍처, 테스트 전략, IPC 계약, 상태 관리, 코딩 컨벤션, 그리고 이 시스템이 항상 지켜야 하는 불변식을 적은 foundation/invariants.md. 루트에 CLAUDE.md와 AGENTS.md가 있고, .claude/ 아래에 스킬과 훅과 워크플로우가 있습니다.

그런데 plan.md는 없습니다. research.md도 없습니다.

저는 이게 우연은 아니라고 봅니다. 저 40여 개는 다 남는 문서입니다. 다음 사람이 읽을 거고, 반년 뒤에도 맞을 겁니다. plan.md는 반대입니다. 이번 일이 끝나면 버려지고, 안 버리면 썩습니다. 이 문서를 6개월 뒤에 누가 읽을 일이 있나. 있으면 쓰고, 없으면 대화에서 하면 될 얘기를 파일로 옮겨 적은 것뿐인 것 같습니다.

그날 첫 번째 커밋이 코드를 고치면서 아키텍처 결정 기록 하나도 같이 고쳤다는 게 재밌었습니다. 그 기록에 적힌 함수 동작이 지금 코드와 달랐습니다. 버그를 고치다가 문서가 낡았다는 걸 알고 같이 손봤습니다. 구현이 문서를 갱신한 거지, 문서가 구현을 지시한 게 아니었습니다.

저 레포에는 Not-Fixed라는 관례도 있습니다. 커밋 메시지에 이번에 일부러 안 고친 걸 적어두는 칸입니다. 알고는 있는데 이 커밋 범위가 아니거나, 지금 손대면 위험하거나, 다음에 하는 게 맞다고 본 것들입니다.

볼륨 커밋 메시지에 이런 줄이 있습니다.

"…의 Not-Fixed 항목이 요구한 '목표값 전용 setter + 페이드 소유권 조정'이 각각 …에 해당한다."

그리고 마지막 출력 장치 커밋에 앞에서 본 "같은 결함의 네 번째 반복"이 있습니다.

커밋끼리 대화하고 있는 겁니다. 앞 커밋이 여기까지 했고 이건 남겼다고 적으면, 뒤 커밋이 그걸 받아서 그 남긴 걸 처리했다고 답합니다. 계획을 앞에 몰아두는 대신, 매번 지금 아는 만큼의 남은 일을 적어두고 다음이 그걸 이어받습니다.

계획서와 다른 게 두 가지 있습니다. 하나는 쓰는 때입니다. 계획서는 제일 모를 때 쓰고, Not-Fixed는 그 일을 막 끝내서 제일 잘 알 때 씁니다. 다른 하나는 틀릴 일이 거의 없다는 겁니다. 계획서는 "이렇게 하겠다"는 예측이라 틀릴 수 있고, 틀린 채로 남으면 썩습니다. Not-Fixed는 "이건 안 했다"는 사실이라 잘 안 썩습니다. 아직은 관례라기보다 습관에 가깝지만, 계획을 앞에 쌓는 대신 남은 걸 뒤에 적어두는 쪽으로 가고 있는 건 맞는 것 같습니다.

얼어붙은 문서가 썩는 속도

문서가 썩는 얘기가 나와서 제 쪽 일도 하나 적어둡니다. 제 자동화 레포에는 세션 사이에 상태를 넘기는 active.md가 있습니다. 다음 세션에서 이어서 할 일을 적어두는 파일입니다. 거기에 이런 항목이 오래 박혀 있었습니다.

손대지 말 것 (고유 정체성): robot/ 10편 · game1/ 01~20

지난주에 이 지침을 대놓고 폐기했습니다. 실제로 뽑아보니 손대지 말라고 적어둔 그 30편이 문제의 원인이었습니다. 짧고, 이미지도 없고, 내부 링크도 없었습니다. 전부 내렸습니다. 지금 active.md에는 "이 지침은 폐기됐다. 실수로 되살리지 말 것"이라고 적혀 있습니다.

같은 파일에 6월 중순에 적어둔 판단이 하나 더 있었는데 그것도 틀렸습니다. 틀린 기록이 6주 동안 그대로 남아 있었고, 그동안 세션들이 전부 그 틀린 전제 위에서 판단했습니다.

문서는 쓴 순간부터 낡기 시작하는 것 같습니다. 코드는 컴파일러와 테스트가 잡아주는데 문서는 아무도 안 잡아줍니다. 계획서를 정답처럼 대접하면 확인할 방법이 없는 문장을 기준 삼아 계속 일하게 됩니다. 대화는 이게 덜합니다. 방금 나온 도구 결과가 문장을 바로 반박하니까요. 파일을 읽었는데 계획서와 다르면 바로 드러납니다. 문서에는 그런 마찰이 없습니다.

제 하네스에는 원래 배포 게이트가 있었습니다. 글을 발행하기 전에 문체 검사를 돌리고 결과를 파일로 남기게 했고, 발행 스크립트가 그 파일에서 판정: 합격을 찾아서 없으면 발행을 막았습니다. 기계로 강제하는 품질 게이트라 꽤 그럴듯했습니다.

지난주에 통째로 들어냈습니다. 그 판정: 합격을 검사받는 에이전트가 직접 썼기 때문입니다. 검문소와 통행증 발급처가 같았습니다. 게이트가 확인하던 건 글이 아니라 합격이라고 적혀 있느냐였고, 그 문자열은 통과하고 싶은 쪽이 쓰는 거였습니다. 항목별 점수를 더하는 계산도 있었는데 그 점수도 자기가 매긴 거였습니다. 숫자가 붙어 있으니 객관적으로 보였을 뿐입니다.

그래서 기준을 바꿨습니다. 코드로 강제할 수 있는 건 모델이 뭘 내놓든 상관없는 사실뿐입니다. 커밋 훅의 시크릿 스캔이 그렇습니다. 파일에 AWS 키 패턴이 있느냐는 모델이 뭐라고 하든 안 바뀝니다. 이 글이 사람이 쓴 것처럼 읽히느냐는 그런 사실이 아닙니다. 그래서 게이트를 없애고 체크리스트로 바꿨고, 합격 대신 몇 번째 줄의 어떤 표현을 뭘로 바꿨는지를 남깁니다.

plan.md를 정답처럼 대접하는 것도 같은 실수 같습니다. 그 문서는 그 계획대로 구현할 쪽이 썼습니다. 계획서가 자기를 검증할 수는 없습니다. 검증하는 건 실행 결과이고, 그 결과를 계획에 되먹이는 게 대화입니다.

문서에만 이런 일이 생기는 것도 아니었습니다. 이 글을 쓰고 일주일쯤 뒤에 벤치마크 1위와 2위 차이를 재다가 모델 대신 내 하네스 쪽을 세어본 적이 있는데, 거기서도 같은 모양이 나왔습니다. 게이트가 파일로 있다는 것과 그 게이트가 뭔가를 확인하고 있다는 건 별개였습니다.

계획을 세우지 말자는 건 아닙니다. 계획한테 감당 못 할 권위를 주지 말자는 쪽입니다. 계획을 문서로 만들면 그 문서가 갑자기 무거워집니다. 누가 승인했고, 그 승인을 받으려고 시간을 썼고, 그러니 틀렸다고 말하기가 어색해집니다. 대화 안에 있는 계획은 그런 무게가 없어서 "아 이거 아니네" 한마디로 버릴 수 있습니다. 계획은 다음 한 걸음을 정하는 데 쓰는 거지, 다섯 걸음 뒤에 알게 될 걸 미리 이기라고 있는 게 아닌 것 같습니다.

제가 하는 방식

계획은 대화로 세웁니다. 문서부터 쓰라고 하지 않고, 뭘 하려는지 말하고, 되묻게 하고, 애매한 걸 같이 좁힙니다. 이때 제가 잘못 알고 있던 게 자주 드러납니다.

어느 정도 모이면 "지금까지 정한 것 정리해봐"라고 합니다. 이게 사실상 plan.md 노릇을 하는데, 파일이 아니라 대화 안에 둡니다. 그래야 다음 턴에 반박당할 수 있고, 캐시에 얹혀 싸게 다시 읽힙니다. 정리해보라고 하면 모델이 제가 말한 걸 자기 말로 다시 적는데, 그걸 읽다 보면 제가 애매하게 말한 데가 같이 드러납니다. 제가 직접 계획서를 쓸 때는 그게 잘 안 보입니다. 제 머릿속에서는 다 이어져 있으니까요.

그다음 그걸 바탕으로 작업을 시키는데, 여기서 뽐뿌 글과 달라집니다. "그대로 구현해"라고 하지 않습니다. 하다가 정리한 게 틀린 게 나오면 멈추고 말해달라고 합니다. 음소거 커밋이 딱 그랬습니다. 그리고 결과를 보고 다시 대화로 고칩니다. 돌려보고, 이상한 걸 짚고, 왜 그런지 묻습니다. "열 번에 두세 번"도 "네 번째 반복"도 거기서 나왔습니다. 마지막으로 남을 것만 파일로 뺍니다. 불변식, 아키텍처 결정, 함정 같은 것들이요. 그래서 저 레포에 invariants.md는 있고 plan.md는 없습니다.

그날 두 번째 버그를 어떻게 잡았는지 그대로 적어보면 이렇습니다. QA가 "최초 1회만 소리가 안 난다"고 올렸습니다. 계획서로 만들지 않고, 재현 조건을 좁히고, 로그를 떴습니다. 처음 올라간 재생 요소가 한동안 음소거 상태였고 그 뒤 전환은 전부 정상이었습니다. "최초 1회만"이라는 QA 표현과 맞았습니다. 그럼 준비 단계에서 음소거 상태를 못 받는 거 아닌가 싶어서 코드를 봤더니 맞았습니다. 그럼 전환할 때 마지막 값을 다시 적용하면 되겠다, 여기서 승인했습니다. 그러고 구현하러 들어갔더니 그 "마지막 값"의 처음 값이 false였습니다. 설계를 바꿔서 구현했고, 왜 바꿨는지를 커밋에 남겼습니다.

승인이 중간에 있습니다. 없앤 게 아닙니다. 다만 승인한 게 최종 설계가 아니라 다음 한 걸음이었고, 그래서 그 한 걸음이 틀렸다는 게 드러났을 때 되돌릴 게 없었습니다. 뽐뿌 글과 제 방식의 차이는 문서를 쓰느냐 마느냐보다 승인을 어디에 두느냐인 것 같습니다. 계획 전체를 승인하면 그 계획이 틀렸을 때 계획을 지키려고 하게 되고, 한 걸음씩 승인하면 틀린 걸 버리는 값이 한 걸음어치입니다.

저 절차가 맞는 일도 있을 겁니다. 명세가 밖에서 고정돼서 내려오고 구현이 거의 기계적인 번역인 일, 예를 들면 API 스펙을 받아서 클라이언트 코드를 만드는 일은 발견할 게 별로 없으니 계획을 앞에 몰아두는 게 나을 것 같습니다. 여러 사람이 나눠서 하는 일이면 대화는 한 사람 머릿속에만 남으니 어딘가에 적어야 하고, 그때 계획서는 계획서라기보다 서로 지켜야 할 약속에 가깝습니다. 그건 남는 문서입니다. 마이그레이션이나 삭제, 배포처럼 실수하면 되돌리기 어려운 일은 미리 적고 사람이 한 줄씩 승인하는 게 맞습니다. 이렇게 보면 발견할 게 적거나, 발견을 남한테 넘겨야 하거나, 되돌릴 수 없는 일입니다. 반대로 디버깅이나 리팩터링, 없던 걸 새로 만드는 일처럼 발견이 대부분인 일에서는 계획을 앞에 몰아두는 만큼 손해를 보는 것 같습니다.

그날 다섯 개를 고치고 나서 만약 plan.md부터 썼으면 어땠을까 생각해봤습니다. 아마 "자동 곡 전환 후 앨범 이미지가 갱신되지 않는 버그를 수정한다. 상태 보관소의 썸네일 설정 함수를 확인하고, 승격 지점에서 갱신되도록 한다" 정도로 적었을 겁니다. 그리고 그대로 구현했을 거고, 한 시간이면 끝났을 겁니다. 나머지 네 개는 계획서에 없었으니 그대로 남았을 겁니다.

그날 하루만 보면 토큰을 아꼈을 겁니다. 대신 음소거 버그는 다음 주에, 볼륨 버그는 그다음 주에 처음부터 다시 파고들었을 겁니다. 매번 새 세션에서, 캐시 없이, 맥락을 처음부터 다시 쌓아가면서요.

댓글

이 블로그의 인기 게시물

봉제인형 사진 한 장, ChatGPT와 Gemini의 다른 대답

구글은 서울에서 모델이 아니라 에이전트를 판다

AI 부업으로 3개월에 2달러 벌고 50달러를 썼다는 증언을 봤습니다. 그 위에 광고가 네 겹입니다