문서는 이미 막은 구멍을 아직 열려 있다고 적고 있었다
랜챗(랜덤 화상채팅) 기능을 마무리하면서 운영 문서를 새로 썼다. 구성요소, 실행 방법, 시그널링 이벤트 명세, 신원 모델, 매칭과 신고·차단, 마이그레이션 목록, 실제로 물렸던 함정들, 알려진 한계, 검증 방법, 배포까지 10개 절짜리 문서였다. 이후 이 기능을 다시 건드릴 일이 있으면 소스를 처음부터 다시 뒤지지 말고 이 문서를 기준으로 삼기로 했다.
그런데 문서를 다 쓰고 나서 별도로 붙인 사실 검증 리뷰에서, 내가 방금 쓴 내용 중 여러 곳이 실제 코드와 어긋나 있다는 게 드러났다.
왜 방금 쓴 문서도 틀릴 수 있는가
이상하게 들릴 수 있지만, 문서를 쓴 사람이 그 기능을 직접 설계하고 구현한 바로 그 사람이라는 사실은 정확성을 보장해주지 않는다. 문서를 쓸 때 사람은 코드를 한 줄씩 다시 읽으며 옮기지 않는다. 대체로 자기가 "이렇게 만들었다"고 기억하는 내용을 그대로 옮겨 적는다. 그런데 기억은 최종 구현과 정확히 일치하지 않을 수 있다. 구현 도중에 접근 방식을 바꿨는데 처음 세운 계획이 기억에 더 강하게 남아 있을 수도 있고, 여러 번 리뷰를 거치며 세부 동작이 조금씩 수정됐는데 그 변화 하나하나까지 기억이 따라가지 못했을 수도 있다. 그래서 "내가 만든 기능이니 내가 제일 잘 안다"는 전제는 문서 작성 단계에서는 특히 위험하다.
내가 쓴 문서가 틀렸다
사실 검증 리뷰에서 총 4건이 잘못된 서술로 확인됐다. 예를 들어 문서에는 “차단 테이블을 읽는 코드가 없다”고 적었는데, 실제로는 매칭 제외 판정 로직이 이미 차단 테이블을 읽고 있었다. 읽는 코드가 없는 쪽은 오히려 신고 테이블이었다. 방향이 반대로 적혀 있었던 셈이다. 차단 기능의 동작 방식도 부정확했다. 차단은 DB에 한 행만 쓰이는데, 실제 양방향 효과는 조회 쪽의 or 필터에서 나오는 구조였다. 이 흐름을 문서는 다르게 서술하고 있었다.
내가 며칠 전에 직접 설계하고 구현한 기능인데도 기억을 그대로 옮겨 적으니 틀린 부분이 나왔다. 코드를 다시 열어 대조하지 않고 썼다면 그대로 굳어졌을 내용이었다.
두 오류를 나눠보면 성격이 다르다. 방향이 반대로 적힌 쪽은 단순한 착오에 가깝다 — 어느 테이블이 어느 테이블을 참조하는지를 헷갈린 것이다. 반면 차단의 실제 동작 방식을 잘못 서술한 쪽은 더 위험하다. 한 행만 쓰는데 양방향 효과가 나는 이유가 조회 쪽 필터 때문이라는 사실은, 코드를 다시 열어보지 않으면 직관적으로 재구성하기 어려운 종류의 지식이다. 그런 지식일수록 기억에 의존해 문서로 옮기는 과정에서 정확도가 떨어지기 쉽다.
더 큰 문제: 문서가 코드를 따라가지 못했다
사실관계 오류보다 더 신경 쓰였던 건 따로 있었다. 문서가 코드보다 뒤처지는 일이 세 번 반복됐다. 코드는 이미 취약점을 닫아뒀는데, 문서는 여전히 “열려 있다”고 기록된 채로 남아 있었던 것이다.
최종 리뷰 단계에서 옛 보안 모델을 서술한 부분을 잡아냈다. 그런데 그걸 고치고 난 다음 리뷰에서, 이미 해소된 fail-open(실패 시 통과) 동작을 여전히 서술하고 있는 다른 절이 또 발견됐다. 한 군데를 고치면 다른 군데가 남아 있는 식으로, 코드의 변경 속도를 문서가 계속 못 따라잡고 있었다.
이게 반복된 이유를 생각해보면, 문서 하나에 같은 내용이 여러 절에 걸쳐 서로 다른 각도로 서술돼 있었기 때문이다. 보안 모델을 설명하는 절과, 함정을 나열하는 절과, 한계를 정리하는 절이 각각 같은 사실을 조금씩 다른 표현으로 담고 있었다. 코드를 고치고 나서 한 절만 고치면 검토자 눈에는 문서가 최신 상태로 보인다. 하지만 같은 사실을 담고 있는 다른 절은 그대로 남아 있으니, 문서 전체로 보면 여전히 모순된 상태다. 정보가 여러 곳에 흩어져 있을수록 갱신을 빠뜨릴 자리도 늘어난다는 뜻이다.
세션 인수인계 문서도 같은 병을 앓고 있었다
다음 세션을 위해 진행 상태를 적어두는 인수인계 문서에서도 같은 문제가 나왔다. 이 문서가 닷새쯤 갱신되지 않은 채로 방치돼 있었는데, 그 사이 이미 사라진 전제 두 개가 여전히 사실인 것처럼 적혀 있었다. “익명 신원은 클라이언트가 만든다”와 “차단 조회는 실패 시 통과시킨다”는 서술이었는데, 둘 다 그 사이에 이미 바뀐 내용이었다.
만약 이 상태 그대로 다음 작업을 시작했다면 두 가지 중 하나였을 것이다. 이미 고쳐진 문제를 모르고 또 고치려 들거나, 반대로 이미 바뀐 계약 위에 옛 전제를 깔고 새 코드를 얹었을 것이다. 둘 다 시간을 버리거나 버그를 새로 만드는 결과로 이어진다.
운영 문서와 인수인계 문서는 갱신되지 않는 이유가 서로 다르다. 운영 문서는 한 번 공들여 쓰고 나면 다시 열어볼 일이 뜸해서 방치되기 쉽다. 인수인계 문서는 반대로 세션이 끝날 때마다 매번 새로 갱신해야 하는데, 그 습관이 하루라도 끊기면 바로 며칠 치 간극이 생긴다. 성격은 다르지만 결과는 같다. 코드는 계속 바뀌는데 문서는 어느 시점에 멈춰 있고, 그 간극은 문서를 읽는 사람 눈에는 보이지 않는다는 점이다.
규칙으로 못박기
그래서 규칙을 하나 정했다. 코드의 계약을 바꾸면 해당 문서의 그 절을 같은 커밋에서 함께 고친다. “나중에 정리하자”는 선택지에서 뺐다. 문서 수정을 뒤로 미루는 순간, 그 문서는 다음에 읽는 사람에게 틀린 지도를 쥐여주는 셈이기 때문이다.
교훈
- 문서는 코드와 같은 커밋에서만 진실을 유지한다. “나중에 정리하자”는 다음 사람, 대개는 미래의 나에게 틀린 지도를 쥐여주는 일이다.
- 특히 보안 관련 문서는 틀리면 없느니만 못하다. “이미 막혀 있다”를 “아직 열려 있다”로 잘못 읽으면 이미 끝난 일을 또 하는 중복 작업으로 끝나지만, 반대로 “아직 열려 있다”를 “이미 막혀 있다”로 읽으면 취약점을 그대로 방치하게 된다. 후자가 훨씬 위험하다.
- 새로 쓴 문서에는 사실 검증 단계를 별도로 붙인다. 방금 내가 직접 만든 시스템을 설명하는 문서라도, 기억을 그대로 옮겨 적으면 코드보다 부정확해질 수 있다. 실제로 이번에 4건이 틀려 있었다.
- 같은 사실이 문서 여러 절에 흩어져 있으면 갱신도 여러 번 해야 한다. 한 곳을 고쳤다고 문서 전체가 최신 상태가 됐다고 믿으면 안 된다.
- 운영 문서와 인수인계 문서는 방치되는 이유가 다르지만 결과는 같다. 둘 다 정기적으로 다시 열어 코드와 대조하는 시점을 따로 마련해두지 않으면, 간극은 소리 없이 쌓인다.