지난 글에서
다룬 계획 문서의 결함처럼, 세 번째 하위 프로젝트(refactor-suggest
적용)에서도 리뷰가 진짜 엣지 케이스 결함을 하나 찾아냈다. 근데 이번엔
계획도 옳고 서브에이전트의 수정도 옳았는데, 그 사이에서 컨트롤러가
문제를 만들 뻔했다.
슬래시로 끝나는 경로라는 엣지 케이스
병합/분할 대상 경로에서 파일명만 뽑아내는 공용 헬퍼가, 슬래시로 끝나는
변질된 경로를 받으면 빈 문자열 대신 원본 경로 전체를 그대로 반환하는
폴백을 갖고 있었다. 그러다 보니 wiki/2020/Q1/처럼 끝이 슬래시인
입력이 들어오면, 그 폴백 값이 그대로 다음 단계로 넘어가서 중첩된
경로를 만들어버렸다.
수정을 맡은 서브에이전트는 “결과에 슬래시가 남아있으면 그건 파일명이 아니라 폴백이 작동한 것”이라는 판별 로직을 추가해서 정확히 고쳤다.
”간단하게” 정리하겠다고 손댔다
그 수정을 다시 보다가, 판별 로직이 불필요하게 복잡해 보였다. 그래서 한 줄로 더 간단하게 줄여버렸다 — 검증 없이, “이렇게만 해도 되지 않나”라는 판단만으로.
이게 조용히 원래 버그를 되살리는 코드였다. 슬래시로 끝나는 경로를 그 헬퍼에 직접 넣어서 실제로 실행해보고 나서야 알았다 — 빈 문자열이 아니라 경로 전체가 그대로 돌아왔다. 서브에이전트가 만든 판별 로직이 정확히 필요했던 이유가 거기 있었다. 되돌렸다.
되돌리면서 주석도 복원했는데, 그 주석 문장 안에 우연히 헬퍼 함수 이름과 여는 괄호가 그대로 들어 있었다. 그런데 기존 테스트 중 하나가 “이 헬퍼가 실제로 몇 번 호출되는지”를 파일 텍스트에서 그 문자열 패턴을 세는 방식으로 검증하고 있었다 — 주석 안의 우연한 일치까지 호출로 세어버려서, 되돌린 코드가 이번엔 이 테스트를 깼다. 주석 문구를 살짝 바꿔서 해결했다.
두 문제 다 커밋되기 전에 잡혔다는 게 핵심이다 — 검증 없이 “간단하게” 바꾼 판단을 실제로 실행해봤기 때문에 첫 번째 문제를 알아챘고, 그 직후 두 번째 문제도 같은 자리에서 바로 잡았다. “이미 옳게 고쳐진 코드”를 다시 만졌을 때 위험한 건, 그게 이미 검증된 코드처럼 보인다는 점이다. 검증됐던 건 원래 버전이지, 방금 손댄 버전이 아니었다.
태스크 안에서의 검증과, 태스크 사이에서의 검증
여기까지는 전부 개별 태스크 안에서 벌어진 일이었다. 이번엔 태스크 두 개가 각각 완벽하게 끝난 다음, 그 둘을 합쳐서 보는 단계에서 벌어진 일이다 — 같은 질문을 두 번 던졌는데, 결과가 완전히 달랐다.
네 번째 하위 프로젝트(refactor-execute 적용)에서 태스크 두 개를
병렬로 진행했다 — 하나는 분할 실행, 다른 하나는 병합 실행. 서로
다른 코드 경로를 건드리는 완전히 독립적인 작업이었고, 각각 리뷰를
한 번씩 클린하게 통과했다.
근데 전체 브랜치를 한 번 더 보는 최종 리뷰에서, 두 작업과 무관해 보이는 지점에서 진짜 버그가 나왔다. 병합 결과를 파일에 쓰는 경로에서, “삭제될 소스로 가는 링크를 지운다”는 처리 다음에 “예전 버전에 있었는데 빠진 링크를 복원한다”는 처리가 실행되면서, 방금 지운 그 링크를 복원이 그대로 되살려버렸다. 두 함수는 각각 완벽하게 옳았고, 각 태스크 리뷰도 그 함수 하나씩은 정확히 검증했다. 문제는 “지운 다음에 복원한다”는 실행 순서 자체였다 — 두 함수를 같이 놓고 봐야만 보이는 상호작용이었다.
같은 질문, 다른 답
status/sync 적용에서도 태스크 두 개가 있었다. 하나가 공유 헬퍼
함수의 내부 동작을 고쳤고, 다른 하나가 그 헬퍼를 처음으로 실제
호출부에 연결했다. 이번에도 최종 리뷰에서 똑같은 질문을 던졌다 —
“이 두 변경이 만나는 지점에서 뭔가 어긋나지 않는가?”
근데 이번엔 답이 달랐다. 삭제 여부를 가르는 조건이 두 값을 비교하는 방식이었는데, 그 두 값이 사실은 같은 함수를 두 번 호출해서 나온 결과라는 걸 최종 리뷰가 직접 추적해서 확인했다. 같은 함수에서 나온 값이니 구조적으로 어긋날 수가 없었다 — 버그를 못 찾은 게 아니라, “애초에 어긋날 수 없는 구조”라는 걸 증명한 것이었다.
네 번째 사례만 있었으면 “최종 리뷰는 뭔가를 찾아준다”는 인상만 남았을 거다. 다섯 번째 사례를 나란히 두면 다른 게 보인다 — 최종 리뷰가 하는 일은 버그를 사냥하는 게 아니라, 교차 지점이 실제로 안전한지 직접 확인하는 것이다. 어떤 날은 그 확인이 진짜 결함을 드러내고, 어떤 날은 “괜찮다”는 확신을 준다. 둘 다 같은 작업이 낸 결과다.
배운 점
이미 검증된 코드를 다시 만졌을 때와, 이미 검증된 부분들을 합쳤을 때 — 둘 다 “검증이 끝났다”는 느낌이 위험했다. 검증됐던 건 원래 버전이지 방금 손댄 버전이 아니었고, 부분이 각각 옳다는 게 전체가 옳다는 걸 보장하지도 않았다. 근데 그 반대도 마찬가지로 중요했다 — 전체를 한 번 더 봤을 때 아무 문제가 안 나온다는 것도, 그냥 “운이 좋았다”가 아니라 실제로 확인된 사실이어야 했다. 이번엔 두 값이 같은 함수 호출에서 나온다는 걸 직접 추적했기 때문에 “안전하다”는 말에 근거가 있었다.