잘 돌아가는 코드를 통째로 지워 보자고 하면 어떨까요? 이상한 사람 취급을 받을 겁니다. 그런데 지난주에 읽은 글 하나 때문에 진짜로 지워 봤고, 되살리고 나서 알게 된 게 좀 많았습니다.
발단 — "구현을 통째로 지운다고 상상해 보세요"
Charity Majors의 AI demands more engineering discipline. Not less를 읽었어요. 요지는 이렇습니다. 코딩 에이전트 덕에 코드 만드는 비용이 거의 0이 되면, 코드는 소중히 모셔 둘 자산이 아니라 이해의 캐시가 된다는 거예요. 그러면 엄밀함은 코드가 아니라 사양과 평가 쪽으로 옮겨가야 하고요.
글 중간에 Chad Fowler의 판별법이 인용됩니다.
구현을 통째로 지운다고 상상해 보라. "코드를 그냥 버릴 순 없다"고 말할 때 우리가 실제로 뜻하는 건 이겁니다. 어떤 동작이 필수인지 모른다, 어떤 불변식이 지켜져야 하는지 모른다, 새 버전이 맞는지 판별할 방법이 없다, 어떤 버그가 잊힌 엣지 케이스를 고친 것인지 모른다. 이건 코드 문제가 아니라 평가 문제입니다.
📎 Deletion Test — 구현을 지웠다가 되살릴 수 있는지로 그 코드에 대한 이해도를 재는 방법이에요. 되살리지 못한다면 그건 코드가 없어서가 아니라, 무엇이 맞는지 판별할 기준이 없어서라는 게 핵심입니다. (Chad Fowler, The Deletion Test)
그런데 읽고 고개만 끄덕이면 그냥 감상문이잖아요. 남의 주장을 요약하는 것보다 내 코드에 실제로 걸어 보는 게 맞는 말인지 확인하는 방법이겠다 싶었습니다.
대상 — 하필 이 모듈을 고른 이유
저희 팀 도구인 aitk에는 로컬 세션 기록을 읽어 토큰 사용량을 집계하는 모듈이 있어요. 팀 AX 대시보드에 쌓이는 숫자를 만드는 부분입니다. 코드를 열어 보니 이런 주석이 박혀 있었어요.
/** 이 파일의 시각 키는 `ts`가 아니라 `timestamp`다 */Chad Fowler가 말한 "잊힌 엣지 케이스를 고친 흔적"이 화석처럼 남아 있는 상태였습니다. 누군가 한 번 데였고, 그 기억이 주석 한 줄로만 남아 있는 거죠. 대상으로 삼기 딱 좋았어요.
지우기 전에, 아는 것부터 세어 두자
바로 지우면 나중에 "원래 뭐가 있었더라"가 됩니다. 그래서 삭제 전에 두 가지를 만들었어요.
먼저 지식 인벤토리입니다. 이 모듈이 알고 있는 엣지 케이스를 하나씩 세어 봤더니 20건이 나왔어요. 그리고 각각이 어디에 담겨 있는지 나눴습니다. 테스트가 값으로 잡고 있는 것과, 코드 주석에만 있는 것으로요. 결과는 테스트가 값으로 잡는 게 15건, 코드 주석에만 있는 게 4건, 나머지 1건은 테스트 데이터 모양에 암묵적으로만 들어 있었어요. 주석에만 있는 4건은 전부 한 파일에 몰려 있었습니다. 그 파일엔 직접 테스트가 하나도 없었고요.
다음은 골든입니다. 실제 제 로컬 세션 기록에 원본을 돌려서 집계 결과를 통째로 저장해 뒀어요.
📎 골든 / 캡처·리플레이 — "무엇이 맞아야 하는가"를 손으로 적는 대신, "지금 실제로 무슨 일이 일어나는가"를 그대로 박제해 두는 방식이에요. 나중에 바뀐 코드를 같은 입력에 돌려서 결과가 달라졌는지만 보면 됩니다. (Michael Feathers, Characterization Testing)
그리고 예측을 적어 뒀습니다. "사양만 준 쪽은 테스트도 상당수 깨질 거다. 주석에만 있던 4건은 어느 쪽이든 조용히 사라질 거다." 이걸 미리 적어 둔 게 나중에 도움이 됐어요. 틀린 걸 알 수 있었거든요.
448줄을 지우고, 둘에게 각각 맡겼다
구현 파일 3개, 448줄을 지웠습니다. 그리고 두 갈래로 나눠서 다시 만들게 했어요.
- 사양만 준 쪽 — 이 모듈이 뭘 해야 하는지 설명한 문서와, 데이터가 어떤 모양인지 요약한 것만
- 사양 + 테스트를 준 쪽 — 위에 더해서 기존 테스트 15개까지
실험이 실험이 되게 하려고 두 가지를 신경 썼습니다. 원본이 git 기록에 남아 있으면 명령어 한 줄로 되살릴 수 있으니 git이 아예 없는 복사본에서 돌렸고요. 제가 이미 원본을 다 읽은 상태라 제가 직접 다시 짜면 실험이 안 되니, 원본을 본 적 없는 별도 에이전트에게 맡겼습니다.
결과입니다.
첫 줄부터 예측이 깨졌어요. 테스트를 본 적도 없는 쪽이 테스트 47개를 전부 통과했습니다. 사양과 데이터 모양만 보고 규칙 대부분을 다시 유도해 낸 거예요. 심지어 주석에만 있던 4건 중 2건("진행 중인 파일은 마지막 줄이 깨져 있을 수 있다" 같은 것)도 알아서 되살렸습니다. 스트리밍 데이터를 다뤄 본 사람이면 아는 상식이었던 거죠.
(다만 이건 이 모듈이 비교적 정직한 편이라 가능했던 것 같아요. 도메인 규칙이 사람 머릿속에만 있는 코드였다면 결과가 달랐을 겁니다.)
테스트는 전부 초록불, 그런데 숫자는 32.5% 틀렸다
진짜 문제는 두 번째 줄이었어요. 사양만 준 쪽은 테스트가 전부 초록불인 상태로 출력 토큰을 32.5% 다르게 집계했습니다. 손으로 만든 테스트는 이 차이를 전혀 감지하지 못했고, 실제 데이터를 다시 돌려 본 골든만 잡아냈어요.
이유는 단순했습니다. 우리가 손으로 만드는 테스트 데이터는 우리가 상상한 세계만 담거든요. 실제 기록에는 우리가 상상하지 않은 모양이 들어 있었습니다.
추적해 보니, 틀린 건 원본이었다
어느 쪽이 맞는지 확인하려고 실제 기록을 파고들었어요. 그랬더니 이런 게 나왔습니다.
같은 응답 ID, 같은 파일 안에 두 줄
1번째 줄 출력 2 상태: 없음 ← 답변이 만들어지는 중
2번째 줄 출력 676 상태: 완료 ← 완성된 답변답변이 생성되는 동안 같은 ID로 여러 번 기록되고, 뒤로 갈수록 값이 커집니다. 그런데 원본은 같은 ID를 처음 본 줄만 채택하고 나머지를 버리고 있었어요. 즉 만들어지는 중인 미완성 값을 세고 있었던 겁니다.
확인해 보니 값이 갈리는 경우가 397건, 전부 같은 파일 안이었고 전부 뒤로 갈수록 증가했습니다. 첫 줄만 채택한 합계와 완성된 값을 채택한 합계의 차이가 재생성본이 더 세던 양과 정확히 일치했고요. 원인이 확정된 거죠.
지운 코드가 무엇을 알고 있었는지 확인하려던 실험에서, 지운 코드가 무엇을 잘못 알고 있었는지가 나왔습니다.
재미있는 건 기존 테스트에 중복 처리 케이스가 분명히 있었다는 거예요. 그런데 그 테스트 데이터는 완전히 똑같은 두 줄을 씁니다. 실제 중복은 값이 다른 줄이었고요. 규칙은 적어 뒀지만 현실의 모양은 적어 두지 못한 겁니다.
고쳤습니다
같은 ID마다 지금까지 반영한 값을 들고 있다가, 더 큰 값이 나오면 늘어난 만큼만 더하도록 바꿨어요. 이러면 복제된 중복은 0이 더해져서 부풀지 않고, 미완성 값으로 과소집계되지도 않습니다. 테스트도 실제 모양(값이 다른 중복 줄)을 쓰는 케이스로 보강했고요.
출력 토큰이 정확히 예측한 값으로 올라갔고, 입력·캐시·세션 수는 그대로입니다 (모델별 집계는 출력이 오른 만큼 같이 올라갑니다). 제 로컬 기록으로 재보면 그동안 Claude Code 출력 토큰이 실제보다 4분의 1쯤 적게 집계되고 있었어요. 같은 코드가 팀 전체의 숫자를 만들고 있으니, 비율은 사람마다 다르겠지만 방향은 같을 겁니다.
남은 것들
초록불이 다 켜졌는데도 아무도 못 잡은 변경이 몇 개 더 있었습니다. 집계 구간의 끝을 포함하느냐 마느냐가 조용히 바뀌었고, 소수점 처리 방식도 달라졌어요. 테스트도 골든도 이건 못 잡았습니다.
그리고 구간 끝 처리는 제가 20건을 세면서 빠뜨린 21번째 항목이었어요. "내가 아는 걸 다 셌다"는 것부터가 착각이었던 거죠.
하나 더. 문제의 그 파일에는 성능 최적화가 하나 있었고, 주석에 "전부 파싱하면 명령 하나가 분 단위로 늘어진다"고 적혀 있었습니다. 두 재생성본 모두 이 최적화를 버렸길래 실제로 얼마나 느려지는지 재 봤어요. 차이를 찾지 못했습니다. 셋의 차이가 실행할 때마다 생기는 오차 범위 안이었어요. 코드에만 남아 있던 "지식" 중 일부는 애초에 검증된 적 없는 추측이었던 겁니다.
그래서 무엇이 남았나
그 글은 "엄밀함이 코드에서 평가로 옮겨간다"고 했는데, 직접 해보니 그 평가가 무엇인지가 좀 더 구체적으로 잡혔어요.
손으로 만 든 테스트만으로는 부족합니다. 32.5% 오차가 초록불이었으니까요. 테스트는 우리가 상상한 세계를 지키고, 실제 데이터 리플레이는 우리가 상상하지 못한 세계를 지킵니다. 둘은 다른 일을 해요.
지우기 전에 예측을 적어 두면 실험이 됩니다. 그냥 지웠다 되살리면 "오 되네" 하고 끝나요. 미리 적어 둬야 뭘 잘못 알고 있었는지가 드러납니다. 제 예측은 절반쯤 틀렸는데, 그 틀린 절반이 제일 배울 게 많았어요.
그리고 진짜 수확은 버그였습니다. 제 사용량이 실제보다 4분의 1쯤 적게 집계되고 있었다는 걸, 코드를 아무리 들여다봤어도 못 찾았을 거예요. 지우고 다시 만들어서 다른 답이 나왔을 때 비로소 "어느 쪽이 맞지?"라는 질문이 생겼고, 그 질문이 버그를 찾아냈습니다.
멀쩡한 코드를 지우는 게 여전히 무섭다면, 실제로 지우지 않고 인벤토리와 골든만 만들어 보는 것도 방법이에요. "이걸 지금 지우면 되살릴 수 있나"를 항목별로 적어 보는 것만으로도, 내가 무엇을 모르고 있었는지가 꽤 드러납니다. 저는 20건을 세면서 하나를 빠뜨렸고, 그게 제일 재미있는 부분이었어요.