AI 코드 리뷰를 만들 때 주의할 점 — Claude Code 팀의 일곱 겹 리뷰를 PR 208건에 대 보며

정기창·

Anthropic 의 Claude Code 팀이, Claude 가 코드의 약 90% 를 쓰는 환경에서 AI 코드 리뷰와 사람의 승인을 어떻게 나눠 맡기는지 정리한 글을 2026년 10월 15일에 공개했습니다. 사람이 줄 단위로 읽던 리뷰를 무엇으로 바꿨는지를 일곱 겹의 층으로 나눠 설명한 글입니다. 저도 이 블로그의 모노레포에서 AI 에이전트가 PR 을 리뷰하고, 지적을 판정하고, 고친 뒤 머지까지 하는 리뷰 루프를 몇 달째 돌리고 있습니다. 그래서 원문의 일곱 겹을 하나씩, 리뷰 루프가 PR 마다 남겨 온 원장에 대 봤습니다.

2026년 8월 5일부터 10월 29일까지 쌓인 PR 208건의 원장에 대 보니, 같은 문제를 훨씬 작은 규모에서 풀고 있었는데 어긋나는 모양은 달랐습니다. 원문이 적은 실패는 끝나지 않는 리뷰였고, 우리 리뷰 루프는 끝나기는 했지만 뒤 라운드가 가져오는 것이 적었습니다. 판정 방식에 따라 지적이 그대로 인정되지 않은 비율이 다섯 배쯤 차이 났지만, 그 원인은 아직 가를 수 없었습니다.

같은 기간 머지 게이트가 막은 11번 가운데 8번은 리뷰하고 시험한 코드와 머지하려는 코드가 같은지를 묻는 자리에서 났습니다. 그리고 원문을 읽고 나서야, 바뀐 경로에 따라 사람을 부르는 위험 등급과 지난 사고로 리뷰어를 다시 채점하는 장치가 우리 쪽에 없다는 것이 보였습니다. 이 글은 원문을 요약하고, 그 일곱 겹을 우리 숫자에 대 본 결과와 거기서 얻은 «AI 에이전트로 코드 리뷰를 만들 때 주의할 점», 그리고 남은 빈틈을 적은 기록입니다. 주의할 점은 끝나는 조건과 뒤 라운드가 볼 것, 지적을 만드는 쪽과 판정하는 쪽의 분리, 거르는 시점과 스레드 상태, 리뷰한 코드와 머지할 코드의 일치, 검증하지 못한 것을 통과로 세지 않기, 지표의 단위 여섯 가지입니다.

먼저 밝혀 둘 것이 있습니다. 아래의 숫자는 모두 이 블로그의 모노레포에서 리뷰 루프가 돈 PR 에서 나왔고, 이 저장소에서 사람은 저 한 명입니다. 원장은 리뷰 루프가 PR 마다 스스로 남긴 기록이라 독립된 감사가 아닙니다. 지적이 맞았는지에 대한 판정도 사람의 반응이 아니라 에이전트의 판정이어서, 노이즈를 사람의 반응으로 재는 원문과는 재는 방법부터 다릅니다. 8월 5일부터 센 것은, 그 전까지는 늦게 끝난 첫 라운드 리뷰 축 하나가 별도의 라운드로 세어져 라운드 수가 부풀어 있었고 그날 그것을 고쳤기 때문입니다. 이 글의 비교는 통제된 실험이 아니어서 숫자로 원인을 단정하지 않으려 했고, 원문의 수치는 원문에 적힌 그대로 옮겼습니다.

원문이 바꾼 질문 — «사람이 읽었나»에서 «무엇이 잘못될 수 있나»로

원문은 Addy Osmani, Morgan Westlee Lunt, Qing Wang 이 claude.dev 블로그에 쓴 «How the Claude Code team does code review»입니다. 출발점은 규모입니다. 원문에 따르면 6월에서 9월 사이 Claude Code 저장소에 머지된 PR 은 두 배 이상 늘었는데 되돌린(revert) 비율은 40% 넘게 줄었고, Anthropic 모노레포에 하루 반영되는 변경은 5월 약 2,500건에서 9월 30,000건 이상이 됐습니다. 이 규모에서 줄 단위의 수동 리뷰는 사실상 불가능하다는 것이 글의 전제입니다.

원문은 리뷰가 하는 일을 둘로 나눕니다. 출고 전에 문제를 찾아 고치는 일, 그리고 팀이 코드에 대한 이해를 나누는 일입니다. 글은 주로 앞의 것을 다루고, 거기서 질문 자체를 바꿉니다.

«사람이 코드를 읽었나?»는 «이 변경에서 무엇이 잘못될 수 있고, 그것을 잡을 검사는 자동이든 사람이든 무엇인가?»의 부정확한 대리 지표다.

이 문장을 받아들이면 리뷰는 끝에 놓인 관문이 아니라, 쓰는 동안 계속 도는 맞춤법 검사에 가까워집니다. 대부분의 변경은 사람에게 보이기 전에 Claude 가 최소 한 번 리뷰하고 고친 상태입니다. 원문이 아끼려는 희소 자원은 사람의 시간과 주의이고, 여기에는 리뷰하는 시간과 나쁜 코드가 깬 것을 고치는 시간이 둘 다 들어갑니다.

목표는 대부분의 PR 이 사람 최대 한 명(작성자일 수도 있습니다)으로 머지되면서 revert 와 사고는 늘지 않는 것입니다. 그래서 PR 당 사람 승인 수의 중앙값과 p95, 사람이 단 코멘트, 사람을 기다린 시간을 재고, revert 와 사고의 비율을 가드레일로 둡니다. 가드레일이 오르면 시간을 아낀 것이 아니라 옮긴 것이라고 원문은 적습니다.

일곱 겹(7개 레이어)의 코드 리뷰 — 검사 여섯 층과 그 아래의 롤백

저는 이 글을 «7개의 레이어»로 기억하고 있었는데, 다시 읽어 보니 원문 그림의 설명은 «검사 여섯 층, 그 아래 롤백»이었습니다. 그 일곱 겹은 원문의 «Layer by layer» 절 안에 번호가 붙은 일곱 개의 소절로 나와 있습니다. 위의 여섯이 출고 전에 문제를 거르는 검사이고, 마지막 일곱 번째는 그래도 빠져나간 것을 빨리 알아채고 되돌리는 자리입니다. 어느 한 층도 혼자 완전하다고 가정하지 않는다는 점에서, 보안에서 심층 방어(defense in depth)라고 부르는 구조와 같은 모양입니다.

  1. 남이 보기 전에 검증합니다. 리뷰의 대부분은 PR 이전에 일어납니다. 테스트·빌드·디자인 대조 스크린샷처럼 Claude 가 직접 돌릴 수 있는 검증 수단을 주고, «됐습니다» 대신 출력을 보이게 합니다. 새 맥락의 서브에이전트가 diff 를 리뷰하고, 같은 지적이 반복되면 스킬·린트·CLAUDE.md 같은 근원에서 고칩니다. 코드 오너가 쓰는 REVIEW.md 에는 그 저장소에서 실패가 어떤 모습인지, nit 의 상한, 이름으로 추측하지 말고 파일과 줄로 근거를 댈 것, 요약은 개수로 시작하고 고칠 것이 없으면 그렇다고 말할 것을 적습니다.
  2. 모든 PR 에 AI 코드 리뷰를 겁니다. 리뷰어는 diff 를 저장소 전체와 PR 이 밝힌 의도에 대 보고, 버그 수정이면 근본 원인까지 봅니다. 연구 미리보기 동안 실질적인 코멘트를 받은 내부 PR 의 비율은 16% 에서 54% 가 됐고, 1,000줄이 넘는 PR 은 대부분 지적을 받은 반면 작은 PR 은 대부분 깨끗했습니다. 여러 에이전트가 동시에 버그를 찾고, 별도의 그룹이 지적마다 투표해 충분히 동의한 것만 게시합니다. 스타일·진단·정리뿐인 지적은 버리고, Important·Nit·Pre-existing 태그를 붙여 원래 있던 버그가 작성자 탓처럼 보이지 않게 합니다. 모든 지적에는 고치거나, 사유와 함께 스레드를 해결하는 식으로 답해야 합니다.
  3. 보안 리뷰가 머지를 막습니다. 버그 리뷰나 보안 리뷰의 지적이 열려 있으면 머지할 수 없습니다. 오탐은 짧은 사유와 함께 스레드를 닫아 풀고, 뒤의 커밋이 고치면 자동으로 풀립니다.
  4. 자동 검사가 머지를 막습니다. 린트·타입·전 플랫폼 테스트·빌드 같은 검사가 수십 개 있습니다. PR 템플릿은 «어떻게 시험했나»와 «어떻게 되돌리나»를 묻고, Claude 가 채워도 서명은 요청한 사람이 합니다.
  5. 위험도에 비례해 사람이 승인합니다. 문서·문구·내부 기능·플래그 뒤의 추가처럼 위험이 낮으면 자동 리뷰와 보안 리뷰가 초록일 때 승인 0명, 일반 제품 변경이나 캐시·자동 업데이터·시스템 프롬프트 같은 민감한 영역은 1명, 위험이 높으면 2명입니다. 등급은 작성자가 아니라 변경이 정합니다. 분류기가 등급을 고르고, 두 번째 분류기가 «등급상 필요 없어도 이 변경에 사람이 더 필요한가»를 봅니다. 오너가 민감하다고 적은 경로와 모든 CI 워크플로 파일은 언제나 사람에게 가고, 가벼운 경로는 신뢰 목록의 작성자만 탈 수 있습니다. 에이전트의 승인은 사람 승인으로 치지 않고, 에이전트에게 일을 시킨 사람이 필수 리뷰어 한 명으로 계산됩니다. 그리고 승인은 특정 커밋 하나에만 유효해서, 에이전트의 push 나 손으로 푼 merge 처럼 시스템이 보증할 수 없는 커밋이 들어오면 다시 승인받아야 합니다.
  6. 머지 큐로 조합을 다시 시험하고, 점진적으로 내보냅니다. 머지 큐는 main 과 변경을 합친 상태로 모든 검사를 다시 돌립니다. 따로 통과한 두 PR 이 합쳐져 main 을 깨지 않게 하려는 것입니다. 새 동작은 플래그 뒤에 두고 사내에 먼저 내보내며, 공개는 latest 채널을 거쳐 stable 로 갑니다.
  7. 빨리 알아채고 빨리 되돌립니다. 배포 없이 끌 수 있는 플래그와 주간 온콜이 있고, 사고 리뷰는 «어느 층이 이것을 잡았어야 했나»를 묻습니다. revert 나 사고 수치가 움직일 때 승인을 조이는 회로 차단기는 아직 없는데, 사람 0명 머지를 늘리기 전에 갖추고 싶다고 적었습니다. 원문의 문장을 옮기면 이렇습니다. «속도를 늦추면 오류율은 줄지만, 대개는 모든 일을 덜 하기 때문이다. 우리는 알아채고 고치는 속도를 높이고 싶다.»

층 밖에서 리뷰어를 고치는 고리

일곱 겹 바깥에는 리뷰어 자체를 고치는 고리가 있습니다. 먼저 사고를 eval 로 삼습니다. 사고를 낸 과거 PR 을 fix-forward 와 revert 까지 모아 그 코드를 아는 엔지니어가 버그와 위치를 확인해 두고, 리뷰어를 매일 그 세트에 돌리며, 리뷰어를 바꿀 때마다 전체를 다시 채점합니다. 노이즈는 사람의 반응으로 잽니다. 지적을 받은 사람이 고쳤는지, 틀렸다고 했는지, 의도된 동작이라고 했는지, 상관없다고 했는지를 세고, 깨끗하다고 아는 PR 에 리뷰어를 돌려 해로운 변경을 유도할 지적을 얼마나 내는지도 셉니다.

원문은 작성자가 에이전트일 때 노이즈가 더 비싸다고 짚습니다. 사람은 nit 하나를 몇 초 만에 넘기지만, 에이전트는 그것을 고치고, 커밋이 하나 늘고, 리뷰가 한 라운드 더 돕니다. 그래서 리뷰는 수렴해야 합니다. 에이전트 작성자와 에이전트 리뷰어가 서로를 바쁘게 만드는 사이, Claude Code CLI 의 PR 하나는 밤새 50라운드 넘게 돌았습니다.

리뷰어가 계속 지적한 코드를 git blame 으로 따라가 보니 약 59% 가 첫 패스 때부터 있던 코드였습니다. 새 버그가 아니라 원래 있던 버그를 라운드마다 몇 개씩 더 찾고 있었던 것이고, 원문은 기본으로 여러 라운드를 돌던 자기들의 습관을 두고 «우리가 더 나쁘게 만들었다»고 적습니다. 지금은 recall 과 함께 rounds-to-pass(통과까지 걸린 라운드 수)를 추적합니다. 세 라운드가 지나면 리뷰어가 접근 자체를 다시 생각해 보라고 요청하고, 처음 몇 번이 지나면 diff 만 다시 보고, 버그의 «종류»를 지적해 같은 종류를 전부 고치게 하며, 이미 단 지적은 반복하지 않습니다.

사람이 남는 자리도 적혀 있습니다. 만들지 말지(방향·취향·범위), 과금(billing)·자동 업데이터·CI 워크플로 같은 위험한 코드, 남들이 따라 할 패턴이 되는 아키텍처, 증거를 읽는 일, 그리고 결과에 대한 책임입니다.

일곱 겹을 우리 리뷰 루프의 원장에 대 보기

이 블로그의 모노레포에서는 PR 이 열리면 리뷰 루프가 돕니다. 품질·보안 같은 여러 축의 리뷰어가 첫 라운드를 리뷰해 GitHub 에 코멘트를 남기면, 판정자가 지적마다 판정하고, 수정 에이전트가 고친 뒤 다시 리뷰를 받습니다. 이 과정은 루프를 지휘하는 메인 에이전트가 조율합니다. critical 과 high 가 0 이 되면 머지 게이트가 GitHub 의 상태를 직접 조회해 확인하고, 2026년 5월부터는 게이트를 모두 통과하면 squash 머지까지 자동으로 합니다.

Claude Code 위에 에이전트와 스킬을 엮은 하네스의 일부이고, 이 사슬을 이은 과정은 사람 손 없이 PR→머지→배포에 적었습니다. 원문의 층에 대 보면 이렇습니다.

원문의 층우리 쪽비어 있거나 다른 곳
1. 남이 보기 전 검증빌드·타입·테스트·UI 스크린샷이 PR 전 완료 조건, 수정 에이전트의 결함 주입 확인PR 전 리뷰는 상시가 아니라 10건짜리 파일럿
2. 모든 PR 에 AI 코드 리뷰첫 라운드 다축 리뷰 → 세 갈래 판정원문은 게시 전에 투표, 우리는 게시 후에 판정
3. 막는 보안 리뷰보안은 첫 라운드 축의 하나, 열린 critical·high 스레드는 게이트가 막음medium 이하 열린 스레드는 게이트가 막지 않음(규칙상 고치거나 제 승인으로만 건너뜀)
4. 막는 자동 검사자체 러너의 CI 가 초록이어야 게이트 통과—
5-1. 위험도 비례 사람 승인사람은 한 명, critical·high 0 에 게이트 통과면 자동 머지경로 기준의 위험 등급이 없음
5-2. 승인은 커밋 하나에리뷰한 커밋 이후의 변경 검사, 패치가 같은 리베이스 판정«Claude 공동 작성» 표시를 수정 커밋의 증거로 봄
6-1. 머지 큐뒤처진 커밋 수 직접 계산, 머지 직전 재확인큐가 아니라 «뒤처지면 막고 리베이스 후 재시험»
6-2. 플래그·점진 출시일부 기능은 환경 변수 플래그로 끄고 켬사내 우선·채널 단계 출시 없음
7. 빠른 탐지·롤백배포 뒤 운영 실물 읽기 전용 확인, 배포 실패 복구 루프«어느 층이 잡았어야 했나»를 정례로 묻지 않음
리뷰어를 고치는 고리라운드 경제 규칙, 판정 사유 보존사고 eval 세트 없음, 노이즈를 에이전트 판정으로 잼

표로 놓고 보면 대부분의 층에 대응하는 무언가가 있기는 합니다. 다만 그 무언가가 원문의 층과 같은 일을 하고 있는지는 표로 알 수 없어서, 원장의 숫자로 봐야 했습니다.

라운드 — 끝나기는 했지만, 뒤 라운드가 가져온 것은 적었습니다

원문이 적은 실패는 끝나지 않는 리뷰였습니다. 우리 원장의 PR 208건 가운데 라운드 기록이 있는 207건(2026년 8월 5일~10월 29일)을 세어 보면, 한 라운드로 끝난 PR 이 114건(55.1%), 두 라운드가 78건(37.7%), 세 라운드가 14건(6.8%), 네 라운드가 1건이었습니다. 규칙상 상한은 다섯 라운드인데 거기까지 간 PR 은 없었고, 끝없이 도는 루프도 없었습니다.

대신 다른 모양이 보였습니다. 같은 207건에서 나온 지적 454건(critical 부터 nit 까지) 가운데 346건(76.2%)이 첫 라운드에서 나왔고, 두 번째 라운드가 90건, 세 번째가 18건, 네 번째가 0건이었습니다. 중대한 것으로 좁히면 더 뚜렷합니다. critical 과 high 47건 중 43건(91.5%)이 첫 라운드였고, 뒤 라운드에서 나온 것은 high 4건, critical 0건이었습니다. 뒤 라운드 109번 가운데 51번(46.8%)은 지적이 하나도 없었고, 첫 라운드부터 지적이 없었던 PR 은 65건(31.4%)입니다.

원문의 실패가 «끝나지 않음»이었다면, 우리 쪽은 «끝나기는 하는데 뒤 라운드의 값이 낮음»으로 어긋나 있었습니다. 중대한 지적은 거의 첫 라운드에서 나오고, 뒤 라운드는 절반 가까이가 빈손이었습니다. 끝나는 조건을 둔 덕분에 50라운드는 겪지 않았지만, 끝나기 전까지 도는 라운드가 무엇을 보는지는 따로 물어야 했습니다.

판정 — 방식에 따라 다섯 배쯤 차이가 났고, 원인은 아직 가를 수 없습니다

판정자는 지적을 세 갈래로 판정합니다. 인정(CONFIRMED), 부정확(IMPRECISE — 결론은 맞지만 위치나 세부가 틀린 지적으로, 교정문을 붙여 살립니다), 기각(REFUTED — 실질적인 오류일 때만 버립니다)입니다. 따로, 교정문을 따라 원래 코멘트가 가리킨 곳과 다른 곳을 고친 건수도 셉니다.

같은 기간 첫 라운드 지적 346건 가운데 판정 기록이 남은 253건(PR 109건)은 인정 224건(88.5%), 부정확 24건(9.5%), 기각 5건(2.0%)이었고, 다른 곳을 고친 것이 18건이었습니다. 판정 방식별로 나누면 차이가 큽니다. 별도 에이전트인 판정자가 판정한 쪽(PR 39건, 지적 121건)은 인정 80.2%, 부정확 16.5%, 기각 3.3% 였고, 16건(13.2%)은 원래 코멘트가 가리킨 곳이 아닌 데를 고쳤습니다.

메인 에이전트가 지적을 하나하나 직접 재현해 확인하고 판정자를 띄우지 않은 쪽(PR 67건, 지적 127건)은 인정 96.1%, 부정확 3.1%, 기각 0.8% 였고, 다른 곳을 고친 것은 2건이었습니다. 그대로 인정되지 않은 비율로 보면 19.8% 대 3.9%, 다섯 배쯤 됩니다. 두 방식 어느 쪽에도 묶이지 않는 PR 3건(지적 5건)은 이 비교에서 뺐습니다.

이 차이를 판정자의 독립성으로 설명하고 싶어지지만, 그렇게 단정할 수는 없습니다. 같은 PR 을 두 방식으로 판정한 실험이 아니기 때문입니다. 판정을 건너뛰는 것은 규칙상 지적이 10건 안팎이고, 메인 에이전트가 하나하나 명령으로 재현해 그 출력을 저에게 보였을 때만 허용됩니다. 그러니 작고 재현하기 쉬운 PR 이 그쪽으로 몰립니다. 실제로 판정자 쪽 PR 에는 high 가 19건 있었고, 메인 쪽에는 2건이었습니다.

비교하는 두 집단이 처음부터 다르게 골라진, 선택 편향의 모양입니다. 차이는 이만큼 났고, 그것이 판정 방식 때문인지 PR 의 성격 때문인지는 아직 가를 수 없습니다. 규칙 원문도 «애매하면 건너뛰지 말 것 — 건너뛸지 정하는 주체가 건너뛰는 쪽이라 유인이 한쪽으로 기울어 있다»고 스스로 경고합니다. 건너뛰는 조건을 좁게 묶어 둔 것도 그래서입니다.

기각이 2% 안팎으로 낮다는 것도 같은 조심이 필요합니다. 원문은 노이즈를 사람의 반응으로 재지만, 우리 숫자는 에이전트의 판정입니다. 그래서 이 숫자는 리뷰어가 정확하다는 증거가 아니라, 판정자가 기각한 비율일 뿐입니다.

머지 게이트 — 막은 11번 중 8번은 «같은 코드인가»였습니다

같은 기간 머지 게이트는 231번 돌아 220번 머지하고 11번(4.8%) 막았습니다. 막은 게이트별로는 리뷰한 커밋 이후의 변경을 검사하는 게이트에서 6번, UI 평가가 빠졌는지 보는 게이트에서 3번, main 이 앞서 나가 그 조합이 시험되지 않았는지 보는 게이트와 머지 직전에 main 이 움직였는지 다시 확인하는 게이트에서 1번씩이었습니다. 11번 중 8번이 리뷰하고 시험한 코드와 머지하려는 코드가 같은지를 묻는 게이트에서 났습니다. 원문의 5층이 «승인은 특정 커밋 하나에만 유효하다»로, 6층이 머지 큐로 지키는 바로 그 자리입니다. 사례는 아래 주의점에서 이어서 적었습니다.

AI 에이전트로 코드 리뷰를 만들 때 주의할 점

원문과 우리 숫자를 나란히 놓고 나니, 코드 리뷰 자동화를 처음 만들 때 알았으면 좋았을 것들이 여섯 가지로 모였습니다. 원문의 처방과 겹치는 것도 있고, 원문에는 없는 우리 쪽 사고에서 나온 것도 있습니다.

끝나는 조건과, 뒤 라운드가 볼 것을 먼저 정합니다

우리 루프는 critical·high 가 0 이 되면 끝나고, 다섯 라운드를 넘지 못하게 되어 있습니다. 앞의 숫자처럼 상한에 닿은 PR 은 없었습니다. 그런데 끝난다는 것만으로는 충분하지 않았습니다. 뒤 라운드의 절반 가까이가 빈손이었고, 중대한 지적은 거의 첫 라운드에서 나왔습니다. 원문이 짚은 대로, 작성자가 에이전트이면 지적 하나가 커밋 하나와 라운드 하나로 번집니다. 2026년 9월 1일 주간 감사에서 나온 라운드 경제 규칙은 그래서 «몇 번 돌릴까»보다 «언제 멈추고, 뒤 라운드는 무엇을 볼까»를 정합니다.

  • 직전 라운드에 critical·high 가 없고 수정 diff 가 좁으면, 리뷰어를 다시 띄우지 않고 메인 에이전트가 수정 diff 만 읽고 끝냅니다. 처음 몇 번이 지나면 diff 만 다시 본다는 원문의 처방과 같은 방향입니다.
  • «주석이나 문서가 낡았다»는 지적은 high 로 올리지 않고 medium 이하로 둡니다. high 가 부풀면 종료 조건이 뒤로 밀려 라운드가 연명합니다.
  • 수정 에이전트는 재리뷰 전에, 자기가 넣은 검사나 가드에 결함을 일부러 넣어 정말 빨개지는지 한 번 확인합니다. 수정이 새 결함을 만들고 다음 라운드가 그것을 잡는 자기유발 회귀를 라운드 밖에서 끊으려는 것입니다. 변이 테스트(mutation testing)를 손으로 한 번 해 보는 셈입니다.
  • 문서·설계 PR 은 첫 라운드부터 «이 문서대로 구현하면 실제로 돌아가나»(상태 전이, 제약 충돌, 실패와 재시도 경로)를 보는 렌즈로 봅니다. «문장끼리 맞는가» 한 축만 여러 번 돌리지 않습니다. 한 축이 못 보는 결함은 그 축을 몇 번 더 돌려도 그대로 남기 때문입니다.

같은 감사는 «적대적 검증은 낭비다»라는 가설도 따져 봤는데, 대체로 기각됐습니다. 검증 루프는 실제 결함을 잡았고, 비용을 지배한 것은 리뷰가 아니라 세션마다 다시 읽는 상주 컨텍스트였습니다. 다시 읽는 컨텍스트가 비용을 얼마나 차지하는지는 Claude Code 멀티 세션의 토큰 비용에 따로 적었습니다. 줄일 것은 검증 자체가 아니라, 값이 낮아진 뒤에도 같은 방식으로 도는 라운드였다는 생각이 들었습니다.

지적을 만드는 쪽과 판정하는 쪽을 가릅니다

판정자를 리뷰어와 따로 둔 것은 2026년 8월 5일입니다. 두 역할의 지시가 정면으로 부딪혔기 때문입니다. 코드 품질 리뷰어의 정의는 함수 길이나 any 같은 일반 품질 문제를 «반드시 플래그하라»고 의무화하는데, 반증의 기준은 정확히 그런 지적부터 의심하라고 합니다. 한 에이전트가 두 지시를 동시에 따를 수는 없습니다.

당시 에이전트 정의 63개 어디에도 반증의 어휘가 없었고, 대신 반증에 성공한 코멘트를 수정 입력에서 조용히 빼는 규칙이 있었습니다. 무엇을 왜 뺐는지가 어디에도 남지 않는 누락이었습니다. 지금 판정자는 같은 라운드의 수정 에이전트로 다시 쓰지 않습니다. 자기 판정을 자기가 집행하게 되기 때문입니다. 회계나 보안에서 직무 분리(separation of duties)라고 부르는 원칙을 리뷰 루프 안에 들인 셈입니다.

판정을 두 갈래에서 세 갈래로 늘리며 생긴 «부정확» 칸이 제 몫을 한 곳도 여기였습니다. 앞의 원장에서 판정자가 본 121건 가운데 16건(13.2%)은 결론은 맞지만 자리가 틀린 지적이어서, 교정문을 따라 코멘트가 가리킨 곳이 아닌 데를 고쳤습니다. 맞다와 틀리다 두 칸뿐이었다면 그대로 인정되어 엉뚱한 자리를 고치게 하거나, 기각되어 진짜 문제와 함께 버려졌을 지적들입니다.

거를 거면 게시하기 전에, 게시했다면 스레드 상태까지 닫습니다

원문은 지적을 게시하기 전에 거릅니다. 여러 에이전트가 찾은 지적에 별도의 그룹이 투표해, 충분히 동의한 것만 PR 에 올립니다. 우리 루프는 반대로, 리뷰어가 먼저 GitHub 에 게시하고 판정자가 그 뒤에 판정합니다. 이 순서의 차이가 생각보다 비싸다는 것을, 판정을 두 갈래에서 세 갈래로 바꿀 때 알게 됐습니다.

그때의 수정은 판정의 어휘만 바꾸고, 판정 결과를 GitHub 스레드의 상태로 되돌려 쓰는 배선은 빠뜨렸습니다. 그대로 두었다면 부정확으로 판정되어 다른 파일을 고친 지적의 원래 스레드가 열린 채로 남고, 열린 critical·high 스레드를 막는 머지 게이트가 그런 PR 을 영영 막았을 것입니다. 새 실패 모드를 하나 만든 셈이었고, 이 구멍은 그 수정을 리뷰한 리뷰 루프가 잡았습니다. 어휘를 바꾸면 그 결과가 흐르는 경로를 전부 다시 걸어야 한다는 것을 그때 배웠습니다. 지금은 이렇게 묶어 둡니다.

  • 종료 조건은 기각된 지적을 뺀 뒤에 셉니다.
  • 기각하면 그 사유를 스레드 답글로 남기고 스레드를 닫습니다.
  • 교정문을 따라 다른 곳을 고쳤으면, 원래 스레드에 «어디에서 처리됐다»는 답글을 남기고 닫습니다.
  • 판정은 라운드를 넘어 유효합니다. 그렇지 않으면 첫 라운드에서 거른 오탐이, 수정 에이전트가 코멘트를 다시 읽을 때 되살아납니다.
  • 부정확의 교정문은 GitHub 코멘트에는 없고 판정 결과에만 있으므로, 수정 에이전트의 프롬프트에 직접 실어야 합니다. 빼면 수정 에이전트는 원래 코멘트에 적힌 틀린 파일과 줄을 고칩니다.

게시한 뒤에 판정하는 구조는, 판정 결과를 스레드 상태로 되돌려 쓰는 일까지 떠안습니다. 거를 거라면 게시하기 전에 거르는 편이 쌉니다.

다만 무엇을 기본값으로 둘지는 원문과 반대로 정했습니다. 우리 검증 규율은 다수결을 기각 판정에만 쓰고, «불확실하면 기각하라»는 지시를 금지합니다. 불확실하면 부정확으로 두고 확신도를 낮춥니다. 2026년 7월 9일에는 «불확실하면 기각»을 쓴 두 갈래 판정으로 여러 에이전트를 한꺼번에 띄운 검증이, 판정 대상 40건 가운데 20건을 잘못 버린 일이 있었습니다. 에이전트 126개가 떴던 날도 그날이었습니다.

원문의 «충분히 동의해야 게시»는 오탐의 비용을 줄이는 쪽이고, 우리 규칙은 놓침의 비용을 줄이는 쪽입니다. 어느 쪽이 맞는지는 작성자가 사람인지 에이전트인지, 그리고 놓친 버그가 얼마나 비싼지에 달려 있다는 생각이 들었습니다.

리뷰한 코드와 머지할 코드가 같은지 확인합니다

머지 게이트가 막은 11번 중 8번이 이 자리였습니다. 검사한 시점의 대상과 실제로 쓰이는 시점의 대상이 달라지는 틈, TOCTOU(time-of-check to time-of-use)라고 부르는 모양입니다. 원문이 승인을 커밋 하나에 묶고, 머지 큐로 main 과의 조합을 다시 시험하는 것도 이 틈을 막는 일입니다. 우리 쪽에서 실제로 걸린 경우는 리뷰가 끝난 뒤 main 이 같은 파일을 건드린 경우, 리베이스하는 동안 패치 24개 중 2개가 달라진 경우, 리뷰 뒤에 생긴 수정 커밋이 리베이스와 섞여 리뷰한 패치 묶음과 달라진 경우였습니다.

오차단도 한 번 있었습니다. 리뷰 루프 자신의 자동 리베이스가 리뷰한 커밋의 해시를 새로 써서, 게이트가 «리뷰한 커밋이 사라졌다»며 막은 것입니다. PR 의 패치는 그대로였고 해시만 바뀌었습니다. 그 뒤로는 git range-diff 로 «패치가 같은 리베이스»인지 판정해 통과시킵니다.

이때 리뷰 뒤의 커밋이 전부 리뷰 루프의 수정 커밋인지, 패치가 같은지, 리뷰 뒤에 main 이 바꾼 파일과 겹치는 것이 하나도 없는지 같은 조건을 함께 봅니다. 해시로 묶은 승인은 리베이스 같은 기계적인 작업에도 깨집니다. 원문의 «시스템이 보증할 수 없는 커밋»이라는 말을 쓰려면, 무엇을 보증된 변경으로 칠지부터 정의해야 했습니다.

main 이 앞서 나간 경우는 처음에 GitHub 가 걸러 줄 것으로 보고 있었습니다. 머지 게이트를 설계할 때 두 번째 게이트에서 mergeStateStatus 가 CLEAN 이나 UNSTABLE 인지를 보고, 뒤처짐(BEHIND)이면 막도록 적었습니다. 그런데 무료 플랜의 비공개 저장소에서는 GitHub 의 «머지 전에 브랜치를 최신으로 유지하라»는 브랜치 보호 설정을 쓸 수 없고, 그래서 BEHIND 가 아예 계산되지 않았습니다. 텍스트 충돌만 없으면 main 보다 한참 뒤처진 브랜치도 CLEAN 으로 보였고, 실제로 151커밋 뒤처진 브랜치가 있었습니다. 보고가 아니라 GitHub 를 믿는다는 원칙은 맞았지만, GitHub 가 계산하지 않는 값까지 믿을 수는 없었습니다.

그래서 지금은 compare API 로 뒤처진 커밋 수를 직접 세어 막고, 머지하기 직전에 한 번 더 확인합니다. 원문처럼 머지 큐를 두는 대신 «뒤처지면 막고, 리베이스한 뒤 다시 시험한다»는 쪽입니다. 시험을 묶어 돌리는 방식도 따져 봤지만, 실패가 어느 변경의 몫인지 가릴 수 없게 되어 쓰지 않았습니다. 두 확인은 대략 이런 모양입니다.

# PR 브랜치가 main 보다 몇 커밋 뒤처졌는지 직접 센다 (BEHIND 가 계산되지 않는 저장소에서도 동작)
gh api "repos/OWNER/REPO/compare/main...feature-branch" --jq '.behind_by'

# 리뷰한 커밋과 리베이스한 뒤의 커밋이 같은 패치인지 본다 (= 같음, ! 다름)
git range-diff origin/main REVIEWED_SHA HEAD

리뷰의 기준도 같은 생각으로 스냅샷에 묶습니다. 리뷰 규칙은 base SHA 에서, 코드는 head SHA 에서 읽어 PR 이 자기가 심사받을 규칙을 고쳐 쓰지 못하게 하고, 이전 SHA 의 리뷰 결과를 새 SHA 로 승계하지 않습니다.

검증하지 못한 것과 보지 않은 것을 통과로 세지 않습니다

검증이 조용히 실패하면, 그동안의 PR 은 검증을 받은 것처럼 보입니다. 4월에 다른 모델로 돌리던 리뷰어가 두 번 조용히 실패했고, 그사이 최근 PR 30건 가운데 29건이 작성한 쪽의 자기 리뷰만 받고 머지됐습니다. 이미 머지된 PR 두 건을 다시 리뷰하자 한 건에서 critical 1건과 high 4건, 다른 한 건에서 critical 1건과 high 2건이 나왔습니다.

PR 을 만들기 전에 로컬 diff 를 리뷰하는 파일럿에서는, 판정 에이전트가 전부 죽어도 결과가 PASS 로 집계되는 결함이 있었습니다. «검증을 못 했다»와 «검증했고 문제없다»가 같은 출력이었던 것입니다. 검증하지 못한 상태가 통과 쪽으로 흘러가는, 흔히 fail-open 이라고 부르는 실패입니다. 리뷰어의 출력 형식과 게이트가 읽는 형식이 어긋나면 같은 일이 생깁니다. high 가 있어도 게이트가 0건으로 읽게 됩니다. 도구에 들어 있는 리뷰 명령을 통째로 가져다 쓰지 않은 이유이기도 합니다.

원문이 REVIEW.md 에 적으라고 한 «요약은 개수로 시작하고, 고칠 것이 없으면 그렇다고 말하라»는 문장을, 저는 이 맥락에서 읽었습니다. 0 이 «세어 보니 0»인지 «세지 못해서 0»인지를 출력에서 가를 수 있어야 합니다.

보지 않은 것을 결과에 적게 하는 것도 같은 문제의 다른 면입니다. 서버 쪽 리뷰어가 자기 자신을 고치는 PR 을 두 번 리뷰했는데, 두 번 다 입력 예산 때문에 변경 파일 16개 중 13개만 봤고, 빠진 셋이 하필 핵심 처리 코드와 리뷰어 자신의 런타임 파일, 그 테스트였습니다. 다행히 결과에 «검토 누락»으로 적혀 있어서, 그 결과를 «16개 파일을 다 본 리뷰»가 아니라 «13개 파일 범위의 리뷰»로 읽을 수 있었습니다. 그 이야기는 Flue 로 에이전트 두 개를 만든 글에 적었습니다.

입력이 리뷰가 볼 수 있는 범위를 정한다는 것은 다른 데서도 보였습니다. PR 전 리뷰와 PR 시점 리뷰를 비교하는 파일럿(지금까지 10건, 아직 결론은 없습니다)에서, PR 시점 리뷰가 PR 본문의 사실 오류를 잡았습니다. 로컬 diff 만 받는 리뷰는 원리적으로 볼 수 없는 것으로, diff 를 PR 이 밝힌 의도에 대 보라는 원문 2층의 문장이 가리키는 자리입니다.

검사 자체가 못 보는 것도 있습니다. 코드 리뷰와 E2E 23케이스를 통과한 UI 변경이, 실제 렌더를 측정했을 때에야 결함을 드러낸 일이 있었습니다. 배포 헬스체크(루트 경로 GET 이 200~399 인지)는 빌드 인자가 번들에 들어갔는지, 본문이 맞는지, OAuth redirect_uri 가 맞는지를 보지 못합니다. 그래서 배포 뒤에 운영 실물을 읽기 전용으로 확인하는 단계를 따로 두었습니다. 원문 7층의 빠른 탐지에 해당하는 자리입니다.

주석 정리 규칙을 레포 전체에 적용하던 중에도 비슷한 일이 있었습니다. 주석만 지운 파일 543개짜리 PR 에서, 리뷰 루프는 UI 평가를 «해당 없음»으로 건너뛰었습니다. 그런데 Tailwind 는 쓰이는 클래스를 찾을 때 주석의 글자까지 훑기 때문에, 앱 6개의 생성 CSS 에서 유틸리티 30개가 빠져 있었습니다. 머지 게이트가 «해당 없음이라는 근거가 생성 CSS 의 차이를 보지 못했다»며 막았습니다. AST 로 «주석만 바뀌었다»를 확인하는 검사는 원리적으로 이것을 보지 못합니다. 실제 운영 영향은 없었는데, 빠진 유틸리티를 쓰는 곳이 없었기 때문입니다.

원문은 «검사가 무엇을 놓칠지 말할 수 없다면, 빠른 승인 대신 진짜 리뷰를 요청하라»고 적었습니다. 사람이 한 명뿐인 쪽에서는 이 문장이 «검사가 무엇을 못 봤는지를 결과에 적게 하라»로 읽혔습니다. 리뷰 완료나 지적 0건이 병합 승인이 아니라는 것도 규칙에 적어 두었고, 리뷰 댓글에 적힌 제안 명령은 그대로 실행하지 않으며, 리뷰를 조율하는 코드의 테스트가 통과한 것을 모델이 정확하다는 증거로 치지 않습니다. 원문이 자기들의 Code Review 제품은 승인이 아니라 지적을 게시한다고 한 것과 같은 선입니다.

지표는 세는 단위부터 정합니다

원문은 recall 과 함께 rounds-to-pass 를 추적합니다. 이 숫자는 라운드를 어떻게 세느냐에 먼저 달려 있었습니다. 리뷰 비용을 보려고 만든 대시보드의 «재리뷰 지적 0건 비율»은 GitHub 의 리뷰 객체 하나를 한 라운드로 셉니다. 그런데 스레드에 답글을 달면 본문이 빈 리뷰 객체가 몇 초 간격으로 생기는 것으로 보입니다. 원장에 한 라운드로 기록된 PR 하나에 리뷰 객체가 6개 있었고, 그중 4개는 본문이 비어 있었습니다. 나머지 둘은 첫 라운드 리뷰와, 수정 뒤 메인 에이전트가 남긴 확인 리뷰였습니다.

그래서 최근 머지된 PR 60건을 대시보드로 세면 92% 였고, 같은 시기 원장의 PR 60건으로 세면 60%(뒤 라운드 30번 중 18번)였습니다. 첫 라운드 지적도 대시보드는 54건, 원장은 101건이었는데, 대시보드가 리뷰 본문의 요약 표 행만 세기 때문으로 보입니다. 8월 5일부터의 전체 207건으로 세면 앞에서 본 46.8% 입니다. 부풀어 있던 것은 대시보드였고, 숫자를 견주기 전에 무엇을 한 번으로 세는지부터 맞춰야 했습니다.

원문을 읽고 드러난 우리 리뷰 루프의 빈틈

표의 오른쪽 칸을 다시 보면, 원문을 읽기 전에는 빈칸이라고 생각하지 못한 것들이 있습니다.

경로 기준의 위험 등급이 없습니다. 원문은 등급을 작성자가 아니라 변경이 정하게 하고, 오너가 민감하다고 적은 경로와 모든 CI 워크플로 파일은 언제나 사람에게 보냅니다. 우리 루프는 critical·high 가 0 이고 게이트를 모두 통과하면, CI 워크플로나 인증 경로를 바꾼 PR 도 똑같이 자동으로 머지합니다. 사람이 한 명이라 승인 층이 얇은 것은 어쩔 수 없지만, 그 한 명을 어느 변경에 부를지는 경로로 정할 수 있습니다. CI 워크플로와 인증, 배포 경로가 먼저 떠오릅니다.

여섯 번째 게이트가 표시를 믿습니다. 리뷰한 커밋 뒤에 들어온 커밋 가운데, 커밋 메시지에 «Claude 공동 작성»(Co-Authored-By) 표시가 붙은 것은 리뷰 루프의 수정 커밋으로 보고 통과시킵니다. 그런데 이 표시는 같은 브랜치에 커밋을 올린 다른 에이전트 세션의 커밋에도 붙습니다. 원문이 «에이전트의 push»를 시스템이 보증할 수 없는 커밋의 예로 든 바로 그 경우입니다. 표시가 아니라 출처로 판정하도록 바꿔야 합니다.

사고 eval 세트가 없습니다. 리뷰어의 프롬프트나 모델을 바꿔도, 과거에 사고를 낸 PR 로 다시 채점하지 않습니다. 그래서 리뷰어를 바꾼 뒤에 무엇을 놓치게 됐는지를 같은 잣대로 잴 수 없습니다.

노이즈를 사람의 반응이 아니라 에이전트의 판정으로 잽니다. 앞에서 적었듯 기각률 2% 는 판정자가 기각한 비율이지, 리뷰어가 정확하다는 뜻이 아닙니다. 원문은 지적을 받은 쪽이 고쳤는지, 틀렸다고 했는지, 의도된 동작이라고 했는지를 사람의 반응으로 셉니다.

그 밖에도 사고가 났을 때 «어느 층이 잡았어야 했나»를 정례로 묻지 않고, PR 전 리뷰는 아직 10건짜리 파일럿이라 결론이 없습니다. 이 가운데 위험 등급과 여섯 번째 게이트의 신뢰 기준, 사고 eval 세트는 아직 손대지 않았습니다.

돌이켜 생각해 보면, 원문과 우리 쪽의 가장 큰 차이는 층의 수가 아니라 사람의 수였습니다. 원문의 사람 승인 층은 위험도에 따라 0명에서 2명까지 움직이지만, 1인 개발에서 그 층은 얇을 수밖에 없습니다. 머지 게이트가 막은 11번 중 8번은 «리뷰한 코드가 머지할 코드인가»를 묻는 자리였고, 조용히 실패한 리뷰어와 Tailwind 의 일은 «보지 못한 것»이 «문제없음»으로 읽혔거나 읽힐 뻔한 자리였습니다. 그렇다면 남은 층이 무엇을 놓치는지를 더 정확히 알아야 한다는 생각이 들었습니다. 위의 빈틈을 고치는 일도 거기서 시작하려 합니다.

AI 에이전트 코드 리뷰코드 리뷰 자동화Claude CodeAI 에이전트리뷰 루프머지 게이트1인 개발