설계까지 다 거치고도, 실행해봐야 보였다 4편에서
카탈로그를 /ingest에서 뺐다. 다음엔 뭘 고칠지 물었더니 백로그가
네 개 있었다 — exporter 저장 경로를 바꾸는 것, 디버그 로그에 설정값이
안 남는 것, /refactor의 죽어있는 기능, 그리고 create_file이
경로가 겹치면 조용히 기존 파일에 이어붙이는 것.
우선순위를 정한 이유 — create_file 경로 충돌 시 조용한 이어붙이기
넷 중 무엇부터 할지 고민하다가 마지막 것을 골랐다. 이유는
단순했다 — 나머지 셋은 안 고쳐도 당장 뭔가 망가지진 않는데, 이건
조용한 데이터 오염이었다. 병합 판단을 아예 없앤 작업(#33, /ingest가
기존 위키 문서로 병합하는 경로를 빼고 그 판단을 /refactor에
전담시킨 변경) 문서에는 “create_file은 항상 새 노트만 만든다”고
적어뒀는데, 실제 코드는 경로가 기존 파일과 겹치면 지금도 그 파일
뒤에 새 내용을 이어붙이고 있었다 — 설계까지 다 거치고도, 실행해봐야 보였다 4편에서
카탈로그 문제를 조사하다가 우연히 발견한 것이었다.
카탈로그를 없앴으니 AI가 기존 제목을 볼 일 자체가 줄어서 마주칠
확률은 낮아졌지만, 메커니즘은 그대로 살아있었다. 반복되는 주제가
많은 vault일수록 우연한 제목 충돌로 이미 다듬어둔 노트가 아무 경고
없이 오염될 수 있는 상태였다. 이런 종류(사용자가 모르는 사이 일어나는
손상)는 새 기능이 없는 것보다 우선순위가 높아야 한다고 판단했다.
이어붙이기 대신 고유 경로
고치는 방법은 두 갈래였다 — 이어붙이기를 의도적인 동작으로 인정하고
그냥 두거나, 항상 고유한 새 경로를 강제하거나. 후자를 골랐다. 이
저장소엔 이미 같은 문제(파일명이 겹치면 어떻게 할까)를 풀어둔 코드가
있었다 — 채팅에서 노트를 내보낼 때 쓰는 NoteExporter가 파일명이
겹치면 숫자를 붙여(“제목 2”, “제목 3”…) 고유한 이름을 찾을 때까지
반복하는 패턴이다. 이걸 그대로 재사용하면 데이터 손실 없이(기존
파일은 전혀 안 건드림) #33이 원래 적어뒀던 의도를 코드로도 진짜
성립시킬 수 있었다.
이 과정에서 트레이드오프 하나가 나왔다. 같은 응답(배치) 안에서 여러 문서가 서로를 링크할 때, 실행 전에 미리 각 문서의 경로를 예측해두는 로직이 있는데, 이 예측은 “경로가 겹치면 이름이 바뀔 수 있다”는 걸 모르고 계산된다. 그러니 아주 드문 경우 — 같은 배치의 다른 문서가 원래 예측된 이름으로 이 문서를 링크했는데, 정작 이 문서가 충돌 때문에 다른 이름으로 만들어지면 — 그 링크가 깨진 채로 남을 수 있었다. 다만 이건 기존의 “조용한 이어붙이기”보다는 나은 방향이라고 판단했다 — 데이터 오염이 아주 드문 깨진 링크로 바뀌는 정도의 개선이니까. 완전히 없애려면 예측 로직 자체를 다시 설계해야 해서 스코프가 커지므로, 지금 스코프에서는 감수하고 별도 이슈로 남겨뒀다.
덤으로 나온 죽은 주석
이 코드를 읽다가 바로 옆 주석 하나가 눈에 걸렸다. “배치가 끝나면 아래 어떤 함수에서 실제 vault 상태로 한 번 더 검증한다”고 적혀 있었는데, 그 함수 이름으로 코드 전체를 찾아봐도 나오지 않았다. 존재하지 않는 함수를 있다고 말하는 주석이었다. “코드와 주석이 다른 것만큼 안 좋은 경우가 없다”는 이유로 이것도 같이 정리하기로 했다 — 그 함수 이름 언급만 지우면 되는, 사소한 덤 작업처럼 보였다.
반전 — 그 지시 자체가 틀렸다
여기가 이번 편의 핵심이다. 설계 문서에 “존재하지 않는 함수 이름만 지우라”고 적어서 구현을 했는데, 그 지시가 새로 쓴 문장 자체가 틀려 있었다. 원래 죽은 주석을 고치면서 “배치 종료 후 실제 vault 상태 기준의 2차 재검증은 없음”이라고 한 걸음 더 나가서 단정해버린 것이다.
리뷰가 이걸 확인하다가 잡아냈다. 그 2차 재검증은 실제로 존재하고 있었다 — 다만 죽은 주석이 말하던 이름의 함수가 아니라, 같은 함수 안에서 80줄쯤 아래에 있는 별도의 반복문으로 구현돼 있었을 뿐이었다. 정리하면 이렇다. 원래 죽은 주석은 “이름은 틀렸지만 내용(2차 검증이 있다)은 맞은” 주석이었다. 그런데 그걸 고치겠다고 새로 쓴 주석은 “이름은 안 틀렸지만 내용(2차 검증이 없다)이 틀린” 주석이 되어버렸다 — 같은 자리에서 정반대 방향으로 또 틀린 셈이다. 리뷰 결과를 받고 나서, 진짜 존재하는 그 반복문을 정확히 가리키도록 다시 고쳤다 — “2차 검증이 없다”가 아니라 “2차 검증이 있는데, 그 이름이 이거다”로.
재검증까지 거치고 나서
수정한 뒤 다시 리뷰를 돌렸다. 이번엔 새로 쓴 주석이 정말 코드와 일치하는지, 그리고 이 수정이 순수하게 주석 몇 줄만 바꾼 건지(다른 로직을 건드리지 않았는지)를 확인받은 뒤에야 최종적으로 마무리됐다.
남는 생각
“죽은 주석을 지우자”는 이번 작업에서 제일 사소해 보이는 부분이었다. 근데 그 사소한 부분에서조차, 검증 없이 그냥 넘어갔으면 또 다른 틀린 주석을 코드에 남겼을 거다. 코드 리뷰가 봐야 하는 건 새로 짠 로직만이 아니라, 그 로직을 설명하는 말까지도 포함된다는 걸 이번에 다시 확인했다.
그리고 이게 처음 있는 일도 아니었다. 웨이브 병렬화를 구현할 때(설계까지 다 거치고도, 실행해봐야 보였다 3편)도, 계획서에 명시한 취소 처리 로직 자체에 버그가 있었고 구현하던 중에 그걸 잡아냈다. 이번엔 그보다 훨씬 작은 규모(주석 한 줄)였지만 패턴은 같았다 — 뭔가를 고치라고 내린 지시 자체가 틀릴 수 있고, 그걸 잡아내는 건 결국 또 다른 검증 단계였다. 지시를 내리는 사람도, 지시를 실행하는 사람도, 둘 다 근거 없이 서로를 믿어서는 안 된다는 걸 아주 작은 사례로 한 번 더 확인한 셈이다.