컨벤션과 코드가 다를 때, 다수결이 아니라 독립 수렴으로 갈랐습니다
컨벤션 문서와 실제 코드가 어긋나 있으면 보통 코드를 고칩니다. 문서가 기준이니 코드가 틀렸다고 보는 것입니다. 그런데 그 문서를 마지막으로 손댄 시점이 반년 전이라면 이야기가 달라집니다. 그 사이 현실이 움직였고 문서만 제자리에 있었을 수 있습니다.
컨벤션 문서 3종을 실제 코드와 대조하는 작업을 했습니다. 백엔드·프론트엔드·앱 세 갈래이고, 대상은 두 고객사의 저장소 9개입니다. 최근 60일 커밋이 각각 441건과 496건이라 둘 다 현역입니다. 멈춘 코드베이스를 기준으로 삼으면 대조 자체가 성립하지 않는데, 그 걱정은 없었습니다.
어긋난 자리마다 판단이 갈렸습니다
대조 결과 어긋난 지점이 여러 개 나왔습니다. 어떤 항목은 문서가 낡아 보였고 어떤 항목은 코드가 게을러 보였습니다. 그 인상만으로 하나씩 처리하면 판정이 그날의 기분을 따라갑니다. 기준이 먼저 필요했습니다.
가장 먼저 떠오르는 기준은 건수입니다. 어떤 규칙을 어긴 코드가 89건이면 그 규칙이 현실과 안 맞는다고 볼 만합니다. 이 기준의 약점은 89건이 89번의 판단이 아니라는 데 있습니다.
한 사람이 한 번 정한 방식이 89번 복제된 결과일 수 있습니다. 그러면 세고 있는 것은 현실의 무게가 아니라 복사와 붙여넣기의 횟수입니다. 반대편 기준인 문서 우선도 안전하지 않습니다. 앱 3개가 전부 문서와 다르게 가고 있다면 그 문서는 이미 안 읽히는 문서입니다.
기준 | 판정 방식 | 한계 |
|---|---|---|
다수결 | 위반 건수가 많은 쪽을 표준으로 삼는다 | 한 번의 잘못된 판단이 복제된 것과 구분되지 않습니다 |
컨벤션 우선 | 문서가 항상 옳다고 본다 | 현실이 전부 다른 방향이면 안 읽히는 문서가 됩니다 |
독립 수렴 | 서로 모르는 둘이 같은 선택을 했는지 본다 | 표본이 작으면 판정이 흔들립니다 |
서로 모르는 둘이 같은 답에 도달했는가
채택한 기준은 세 번째입니다. 서로 참조하지 않는 두 프로젝트가 독립적으로 같은 선택을 했으면 컨벤션이 현실을 못 따라간 것으로 보고 문서를 고칩니다. 한쪽에만 있으면 그 코드가 어긴 것으로 보고 코드를 고칩니다.
두 프로젝트는 서로의 코드를 보지 않습니다. 그런데도 같은 답에 도달했다면, 그 답으로 밀어낸 힘이 각자에게 따로 작용했다는 뜻입니다. 반대로 한쪽에만 있는 방식은 그 힘이 국소적이었다는 신호입니다. 세는 단위가 건수에서 독립된 결정의 수로 바뀌는 지점입니다.

문서를 고친 쪽 — 네 항목
양쪽이 같은 방향으로 가 있던 항목은 문서를 고쳤습니다. 네 개였습니다.
앱 폴더 구조 — 컨벤션은 레이어 우선인데 앱 3개가 전부 기능 우선이었습니다. 하위 폴더 분포를 3앱 합산으로 세어 표준 네 개를 확정했습니다.
API 호출 방식 — 수동 구현에서 코드 생성 방식으로 바꿨습니다. 3앱이 전부 그렇게 쓰고 있었습니다.
매퍼 파일 위치 — 최상위로 옮겼습니다. 두 프로젝트 다 최상위였고, 컨벤션에만 남아 있던 규칙이었습니다.
열거형 위치 — 구조별로 분리했습니다. 한쪽의 해당 폴더는 단순한 열거형 모음이 아니라 컨트롤러와 서비스를 가진 진짜 기능 패키지였습니다.
폴더 구조를 개정하면서 한 문장을 같이 못박았습니다. 기능이 커져 유틸이 필요하면 그 기능 폴더 안에 만들고 최상위로 올리지 않는다는 것입니다. 이 문장이 없으면 시간이 지나며 다시 레이어 우선으로 흘러갑니다.
규칙만 적으면 다음 사람이 또 뒤집습니다. 그래서 이번에는 이유를 같이 적었습니다. 매퍼를 기능 폴더에 섞으면 인터페이스와 설정 파일이 쌍으로 돌아다녀서, 어느 종류인지 알려면 파일을 열어봐야 합니다. 규칙 옆에 이 한 줄이 있으면 다음 사람이 뒤집기 전에 한 번 멈춥니다.
코드를 고친 쪽 — 한 저장소에만 있던 것
한쪽에만 있던 위반은 성격이 달랐습니다. 맵 반환 89건, 클래스 레벨 매핑 22건, 화면 쪽에서 데이터 계층을 직접 부르는 패턴. 전부 한 프로젝트에만 있었고 다른 쪽은 0건이었습니다.
0건이라는 숫자가 판정을 굳혔습니다. 다른 팀은 같은 상황에서 다른 답을 냈다는 뜻이라, 컨벤션을 바꿀 근거가 아니라 부채입니다. 핸드오프 문서를 만들어 넘겼고 그날 안에 전량 정리됐습니다.

전부 레거시라고 적었는데 아니었습니다
초판에는 이 위반들을 두고 전부 레거시이고 최근 60일 신규 유입이 0이라고 적었습니다. 변경 이력으로 확인해보니 96건 중 33건이 최근 60일에 생긴 신규 파일이었습니다. 다른 세션이 이 전제를 바로잡아 문서를 개정했습니다.
이 차이가 다음 행동을 바꿉니다. 유입이 멈춘 부채는 정리만 하면 되고, 유입 중인 부채는 정리보다 차단이 먼저입니다. 차단 없이 정리하면 정리한 만큼 다시 쌓입니다. 확인에 든 시간은 변경 이력을 세는 몇 분이었습니다.
전부 레거시라는 문장은 검증 없이 세우기 쉬운 전제입니다. 코드를 보면 오래돼 보이고, 오래돼 보이면 오래된 것 같습니다. 파일이 언제 생겼는지는 코드가 아니라 이력이 압니다.

문서가 스스로 약속한 것을 안 지키고 있었습니다
컨벤션 세 파일 머리에는 새 프로젝트 초기화 시에도 기준으로 사용한다고 적혀 있었습니다. 정작 초기화 절차는 어디에도 없었습니다. 흩어져 있던 초기화 명령들을 문서 안으로 흡수했습니다.
시작 전에 확인할 스펙 질문 목록
빈 프로젝트가 아닐 때는 덮어쓰지 않는다
생성한 프로젝트에 컨벤션 사본을 남긴다
오탐도 하나 나왔습니다. 특정 애노테이션 누락이 2건으로 집계됐는데, 표본을 열어보니 한 건은 상위 클래스가 다른 종류였고 그 문자열이 다른 애노테이션 안에 들어 있어 검색에 걸린 것이었습니다. 실제로는 1건이었습니다.
위반이 아닌 차이도 구분해 적었습니다. 두 프로젝트의 프레임워크 버전이 다르고, 한쪽의 검증 라이브러리는 메이저 버전이 달랐습니다. 컨벤션 예제가 한쪽에서는 그대로 안 맞을 수 있다는 사실을 참고로 남겼습니다.
이 기준이 항상 맞지는 않습니다
표본이 둘뿐입니다. 셋째 프로젝트가 다른 방향으로 가 있으면 이번 판정 중 몇 개는 흔들립니다. 두 프로젝트를 같은 사람이 만들었다면 독립도 아닙니다. 같은 습관이 두 번 나온 것과 구분되지 않습니다.
더 큰 한계는 따로 있습니다. 현실이 나쁜 방향으로 수렴했을 수도 있습니다. 그때는 문서가 버티는 쪽이 맞습니다.
둘 다 그렇게 한다는 사실은 근거이지 정당화가 아닙니다.
정리하며 남긴 점검 항목
문서와 코드가 다르면 몇 건인지 대신 몇 개의 독립된 결정인지 먼저 센다
컨벤션을 고칠 때 규칙만 적지 말고 그렇게 정한 이유를 같은 자리에 적는다
부채로 분류하기 전에 변경 이력으로 최근 신규 유입 건수를 확인한다
문서가 스스로 약속한 절차가 실제로 문서 안에 있는지 확인한다
정적 검색 결과는 표본을 열어보고 오탐을 걷어낸 뒤 숫자를 쓴다
문서가 낡았는지는 문서를 봐서 알 수 없습니다
결과는 세 가지입니다. 컨벤션 3종이 실제 코드와 맞춰졌고, 한쪽에만 있던 위반은 부채로 분류돼 그날 정리됐고, 초기화 절차가 컨벤션 안으로 들어왔습니다.
문서와 코드 중 어느 쪽이 낡았는지는 문서만 봐서는 알 수 없습니다. 서로 모르는 둘이 같은 답에 도달했는지를 보면 대체로 갈립니다. 표본이 둘뿐이라 셋째 프로젝트가 들어오는 시점이 이 기준을 다시 세울 자리입니다.