나는 내 코드를 리뷰하는 것을 그만두었다. 그러기 위해 먼저 충족되어야 했던 조건들
요약
코딩 에이전트의 생산성 향상으로 인해 발생하는 과도한 코드 변경 사항을 관리하기 위해, 코드 리뷰 대신 자동화된 정적 분석과 엄격한 린트 규칙을 활용하는 전략을 제안합니다.
핵심 포인트
- 에이전트가 생성하는 방대한 diff를 사람이 모두 리뷰하는 것은 불가능함
- 기계적인 규칙(함수 길이, 복잡도 등)은 리뷰가 아닌 에러로 처리해야 함
- 에이전트는 인간과 달리 엄격한 규칙에도 불평하지 않으므로 더 높은 임계값 설정 가능
- 예외 사항은 인라인 주석이 아닌 설정 파일(config)을 통해 중앙 관리해야 함
요즘 대부분의 날들에, 나는 diff (차이점)를 읽지 않고 pull request (PR)를 머지 (merge)한다.
이 문장은 예전 같았으면 내가 채용하지 않았을 누군가를 묘사하는 말이었다. 그러니 무엇이 변했는지 정확히 말해두겠다. 그것은 자신감이 생겼기 때문도 아니고, 무모해졌기 때문도 아니다. 그것은 리뷰가 잡아내던 것들을 더 일찍 잡아낼 수 있는 곳으로 옮겼기 때문이다.
그 과정이 어떻게 일어났는지에 대한 솔직한 버전이다.
문제는 철학이 아니라 산술이었다
나는 여러 개의 코딩 에이전트 (coding agents)를 병렬로 실행한다. 그 결과는 내가 하루에 읽을 수 있는 것보다 더 많은 diff를 생성한다. 단순히 "읽고 싶은 것보다 많다"는 뜻이 아니라, 진심으로 업무 시간 내에 다 읽을 수 없는 양이다.
그런 상황이 발생하면 정확히 두 가지 선택지가 있다:
- 읽을 수 있는 양에 맞춰 생성량을 줄인다.
- 읽지 않아도 안전하게 만든다.
나는 두 번째를 선택했다. 내가 용감해서가 아니라, 1번 옵션은 내가 이 시스템을 구축한 이유 자체를 버리는 것을 의미하기 때문이다.
불편한 사실은, 2번 옵션은 마음가짐의 문제가 아니라는 점이다. 그것은 반드시 충족되어야 하는 구체적인 조건들의 목록이다. 나의 목록은 다음과 같다.
1. 규칙은 리뷰 코멘트가 아니라 파일 안에 존재해야 한다
내가 지금까지 했던 모든 코드 리뷰에서, 내 코멘트의 대다수는 기계적인 것이었다. 이 함수는 너무 길다. 이 중첩 (nesting)은 너무 깊다. 왜 여기에 any를 썼는가?
기계는 이 모든 것을 말할 수 있다. 그래서 나는 기계가 이를 **에러 (errors)**로서 말하도록 만들었다:
"max-lines-per-function": ["error", { max: 60, skipBlankLines: true }],
complexity: ["error", 20],
"max-depth": ["error", 4],
...
여기에 더해 cognitive-complexity를 에러로 처리하는 eslint-plugin-sonarjs와, any 사용을 금지하고 non-null assertion을 금지하는 @typescript-eslint의 strict preset을 추가했다.
여기서 새로운 것은 없다. 다른 점은 다음 부분이다.
2. 규칙은 인간 팀이 허용할 수 있는 수준보다 더 엄격해야 한다
이 부분이 내가 진정으로 흥미롭다고 느끼는 지점이다.
만약 이러한 임계값 (thresholds)을 인간 팀에게 적용한다면, 일주일 이내에 이를 완화해달라는 PR이 올라올 것이다. 엔지니어들이 게을러서가 아니다. "이 함수는 63줄이고, 이걸 나누면 오히려 더 나빠진다"라는 말이 때로는 사실이기 때문이며, 매번 그것을 두고 논쟁하는 것은 진을 빼놓는 일이기 때문이다.
Lint(린트)의 엄격함은 언제나 **기계적 정확성 (machine correctness)**과 인간의 인내심 (human patience) 사이의 절충(trade-off) 문제였다. 그리고 규칙을 완화하는 것은 결코 기술적인 결정이 아니었다. 그것은 사회적인 결정이었다.
에이전트(Agent)는 짜증을 내지 않는다. 에이전트는 규칙을 읽고, 함수를 분리하고, 다음으로 넘어간다. 내일 다시 그 일을 하라고 명령받는 것에 대해 아무런 의견도 갖지 않는다.
따라서 사회적 비용은 0이 되었고, 일단 그런 상황이 되면 절충의 저울은 한쪽 방향으로만 기울게 된다. 나는 고통스러울 정도로 모든 규칙을 강화했고, 아무도 불평하지 않았다. 불평할 사람이 아무도 없었기 때문이다.
3. 예외 사항은 이유와 함께 설정 파일(config)에 존재해야 하며, 절대 인라인(inline)으로 작성하지 않는다
규칙이 엄격해지는 순간, 실제 예외 사항들이 나타난다. 만약 // eslint-disable-next-line을 허용한다면, 규칙은 한 달 안에 무용지물이 된다. 그 주석은 코드 리뷰 시에는 보이지 않고, 실제로는 영구적으로 남기 때문이다.
따라서 예외 사항은 설정 파일에 넣되, 이유당 한 줄씩 작성한다:
files: [
"src/components/Sidebar.vue", // @keyframes — "생각 중"을 나타내는 스피너 링
"src/components/GuiPanel.vue", // `.frame + .frame` 인접 형제 결합자(sibling-combinator) 간격 문제
...
모든 항목은 _이유_를 명시한다. 그리고 블록 위의 주석에는 다음과 같이 적어둔다: 해당 이유가 사라지면 이 항목을 삭제할 것.
차이점은 가시성(visibility)이다. 인라인으로 비활성화하는 것은 보이지 않는다. 하지만 설정 파일에서 점점 늘어나는 허용 목록(allowlist)은 직접 눈으로 확인하고 부끄러움을 느낄 수 있는 대상이 된다.
4. 규칙 그 자체는 순수 함수(pure functions)이며, 테스트를 거친다
이 부분은 대부분의 설정에서 생략되는 단계다.
만약 규칙이 어떤 결정(어떤 파일 확장자가 어디로 갈지, 경로가 어떻게 정규화될지, 무엇을 유효한 세션 ID로 간주할지 등)을 내린다면, 그 결정은 _코드_이다. 그리고 UI를 통해서만 실행되는 코드는 아무도 테스트하지 않는 코드다.
따라서 결정 사항들을 공유 모듈 내의 순수 함수(pure functions)로 추출하고, 해당 함수들을 까다로운 케이스들(빈 값, null, 경계값, 잘못된 대소문자, 잘못된 플랫폼 등)에 대해 테스트한다. 현재 그 프로젝트에는 수천 개의 테스트 케이스가 있으며, 그 대다수는 흐름(flow)보다는 작은 순수 함수들을 테스트하는 데 집중되어 있다.
핵심은 개수가 아닙니다. 핵심은 다음과 같습니다: 규칙이 순수 함수(pure function)라면 그 규칙을 테스트할 수 있습니다. 하지만 그것이 컴포넌트 내에 임베딩(embedded)되어 있다면, 오직 리뷰만 할 수 있을 뿐입니다. 그리고 저는 리뷰하는 것을 멈췄습니다.
5. 사용자가 사용하는 OS에서 CI를 실행하고, 내가 가진 OS가 아닌 곳에서 실행하기
저희 팀 전체는 macOS를 사용합니다. 하지만 저희 사용자들은 그렇지 않습니다.
그래서 PR(Pull Request)마다 Linux와 macOS에서 CI를 실행하고, Windows는 야간 스케줄로 실행합니다 (느리지만, 매일 하는 것만으로도 변화를 감지하기에 충분합니다).
이것은 제가 예상했던 방식과는 다르게 즉시 비용을 회수해 주었습니다. 제가 Windows 버그 리포트를 재현할 수 있는 유일한 환경이기 때문입니다. 이전에는 Windows 관련 이슈가 발생하면 보고자에게 제 추측을 테스트해 달라고 요청해야 했습니다. 이제 저는 브랜치를 푸시합니다.
또한, 수정 사항이 다른 것들을 조용히 망가뜨리지 않도록 경로 구분자(path separators), realpathSync 동작 방식, fs.watch 차이점 등 Windows 전용 테스트 케이스를 의도적으로 작성합니다.
6. 코드를 읽는 무언가가 존재하며, 그것은 코드를 작성한 주체가 아니다
Claude Code가 작성하고. 그리고 Codex가 리뷰하고, CodeRabbit이 리뷰하는 식으로 반복되며, 그들이 이의를 제기하기를 멈출 때까지 루프가 돌아갑니다.
여기서 중요한 메커니즘은 'AI 리뷰가 좋다'는 것이 아닙니다. 핵심은 작성자와 독자가 다른 모델이라는 점입니다. 자신의 결과물을 검토하는 모델은 자신만의 사각지대를 공유합니다. 하지만 두 개의 다른 모델은 그렇지 않습니다, 대부분의 경우.
이것이 제가 멈춘 활동을 대체할 수 있는 가장 근접한 것입니다. 신중한 인간 리뷰어만큼 좋지는 않습니다. 하지만 하루에 네 번째 PR에서 밤 11시에 지친 인간 리뷰어보다는 훨씬 좋습니다.
내가 여전히 확인하는 것들
'저는 아무것도 리뷰하지 않는다'고 말하는 것은 거짓말이 될 것이기 때문에, 경계에 대해 솔직하게 말씀드리고 싶습니다:
- UI 변경 사항. 위에 나열된 어떤 것도 레이아웃이 보기 싫거나 흐름이 혼란스러운지 알려줄 수 없습니다.
- CI가 재현하지 못하는 조건 하에서의 동작과 관련된 모든 것. 만약 변경 사항이 CI가 재현할 수 없는 조건에서의 동작에 관한 것이라면, 제가 직접 확인합니다.
- 인증(auth), 권한(permissions) 또는 데이터 손실을 건드리는 모든 것. 이 경우의 파급 범위는 자동화하기에는 너무 큽니다.
그 외의 모든 것은 녹색 신호가 뜨면 통과시킵니다.
솔직한 비용
만약 제가 회의적인 시각으로 이 글을 읽는다면, 다음 세 가지를 알고 싶을 것입니다.
많은 작업이 초기에 집중됩니다. 위에서 언급한 6가지 항목 중 그 어떤 것도 공짜가 아닙니다. 만약 그중 두 가지만 설정하고 멈춘다면, 안전망 없는 엄격함만 남게 되며, 이는 아무것도 하지 않는 것보다 더 나쁩니다.
스택이 균일할 때만 작동합니다. 제 저장소(repo)별 설정 파일이 거의 비어 있는 이유는 모든 프로젝트가 동일한 언어, 동일한 테스트 러너(test runner), 동일한 CI 형태를 사용하기 때문입니다. 만약 여러분의 저장소들이 서로 일치하지 않는다면, 똑같은 규칙을 반복해서 작성해야 할 것입니다. 그것부터 먼저 해결하십시오. 그게 더 저렴하게 먹힙니다.
속도가 곧 정확성은 아닙니다. 작업물은 빠르게 배포되고 빠르게 수정됩니다. 이 설정이 보장하는 것은 "버그가 없음"이 아니라, 리뷰를 했더라도 어차피 잡아내지 못했을 버그들만이 살아남는다는 것입니다.
내가 실제로 얻은 교훈
저는 시간을 아끼기 위해 이 모든 것을 설정했고, 실제로 효과가 있었습니다. 하지만 그것이 흥미로운 부분은 아닙니다.
흥미로운 부분은 린트(lint)의 엄격함, 테스트 커버리지(test coverage), CI의 범위 — 즉, "해야 한다는 것은 알지만 하지 않는 엔지니어링 규율 (engineering discipline)"이라는 카테고리 전체가 결코 더 나은 방법을 아는 것 때문에 막혀 있었던 것이 아니라는 점입니다. 그것은 인간 팀이 협상을 시작하기 전까지 얼마나 많은 마찰(friction)을 감수할 수 있느냐에 의해 막혀 있었습니다.
그 제약 조건이 방금 제거되었습니다. 점진적으로 제거된 것이 아니라, 완전히 사라졌습니다.
우리 대부분은 아직 이에 맞춰 업데이트하지 못했다고 생각합니다.
위의 모든 내용은 공개되어 있습니다: 나의 글로벌 설정 (my global config) 및 이 설정이 실행되는 프로젝트 (the project it runs on), 둘 다 MIT 라이선스입니다. 유용한 것은 무엇이든 가져가세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기