← 에세이 /Post · 01 of 22 · Embedded Dev

AI 코드 리뷰는 몇 번 돌려야 할까

같은 코드에 400회의 LLM 호출을 썼다. 리뷰 1회는 실제 결함의 34%만 본다. 그리고 가장 자주 지적된 항목 4개는 전부 오탐이었다.

·21 min read · · · #ai-code-review#llm-agents#harness#benchmark#code-review
AI 코드 리뷰는 몇 번 돌려야 할까
On this page

이 문서는 harness-bench가 만들어지기까지의 실험과 시행착오를 한 흐름으로 정리한 장문 기록이다. 숫자만 빠르게 보려면 FINDINGS.ko.md, 실무 절차만 보려면 PRESCRIPTION.ko.md, 재현 가능한 벤치마크 결과는 README.ko.md를 참고하면 된다. 이 숫자들로 주장할 수 없는 것LIMITS.ko.md에 따로 적어 뒀다.

이 연구 전체에서 실제로 발생한 LLM 호출 수는 약 400회다. 리뷰 호출만 센 것도, 실험 실행 수를 센 것도 아니다.


이 글의 용어

이 연구는 리뷰를 두 개의 역할로 나눠서 측정했다. 이 구분이 글 전체의 뼈대다.

리뷰어(reviewer)   코드를 읽고 문제를 보고한다. 코드는 건드리지 않는다
수정자(fixer)      그 보고를 받아 실제로 코드를 고친다. 거절할 수도 있다
용어
지적(finding)리뷰어가 한 건으로 보고한 항목. 진짜인지 아닌지는 별개다
오탐(false positive)결함이라고 보고됐지만 실제로는 결함이 아닌 것
고유 지적 / 원시 지적같은 문제를 가리키는 지적을 하나로 묶은 개수 / 묶기 전의 총 개수
(call)모델 호출 1회. 비용의 단위다
라운드 / 루프리뷰→수정 1회가 한 라운드, 그 라운드를 여러 번 반복한 것이 한 루프
계약(contract)그 코드가 지켜야 할 동작을 못박아 둔 명세와 그것을 검사하는 테스트
준수율그 계약을 지키고 있는 비율. 19/19, 23/23 은 조건과 테스트를 전부 통과했다는 뜻
공개 표면다른 코드가 의존하는 부분 — 함수 이름, 시그니처, 문서화된 동작
파일만 / 리포 접근리뷰어에게 대상 파일 하나만 준 조건 / 소스 트리를 읽기 전용으로 열어준 조건
정밀도 / 재현율보고한 것 중 진짜의 비율 / 실제 존재하는 결함 중 찾아낸 비율
churn한 라운드에서 추가·삭제된 줄 수. 얼마나 손댔는가이지 품질이 아니다
거절률수정자에게 넘긴 지적 중, 수정자가 근거를 밝히고 고치지 않기로 한 비율
fan-out한 리뷰어에게 전부 맡기는 대신 영역별 전문 리뷰어로 나눠 돌리는 방식

한 번만 나오는 용어(ARI · 뮤테이션 테스트 · 차등 테스트 · 귀무분포 · 카나리 등)는 처음 등장하는 자리에서 설명한다.


먼저 결론부터

이 연구는 “AI 코드 리뷰를 몇 번 돌리면 충분한가?”라는 질문에서 시작했다.

처음에는 단순하게 생각했다.

리뷰하고, 고치고, 다시 리뷰하면 점점 좋아지지 않을까?

실험 결과는 그렇게 단순하지 않았다.

가장 강하게 남은 결론은 다음과 같다.

결론근거본문
1리뷰 반복과 리뷰-수정 루프 반복은 전혀 다른 일이다리뷰만 반복하면 실제 결함 커버리지가 34% → 61% → 76%. 수정을 섞으면 계약 준수율 개선은 0인데 코드만 +24% ~ +152% 커졌다1부
2리뷰 한 번은 부족하다같은 코드를 그대로 두고 반복 리뷰해도 매번 다른 결함을 본다. 어떤 실제 결함은 10번 중 한 번만 발견됐다1부
3여러 번 지적됐다고 더 진짜인 것도 아니다파일 하나만 준 조건에서는 오탐이 실제 결함보다 더 자주 반복됐다. 리뷰어들이 같은 정보를 모두 갖고 있지 않았기 때문이다1부
4저장소를 읽게 해주는 것이 가장 강한 오탐 제거 수단이었다파일 하나만 보여줬을 때 오탐률 29%. 호출자·헤더·초기화 경로를 확인하게 하자 같은 오탐 4개가 두 모델 모두에서 사라졌다5부
5AI가 조용해졌다고 리뷰가 끝난 것은 아니다한 모델은 3라운드부터 “지적 없음”을 냈지만, 같은 코드에 구조화된 프롬프트를 주자 다른 실패 모드를 계속 찾았다3부
6테스트가 모두 통과했다고 코드가 좋아졌다는 뜻도 아니다47개 라운드에서 계약 준수율은 전부 같았지만, 코드 크기와 복잡도는 조건별로 크게 갈렸다3부

그래서 현재의 실무 처방은 단순하다.

리뷰는 여러 번. 수정은 한 번.

그리고 수정한 코드가 충분히 많이 바뀌었다면, 그것은 “재리뷰”가 아니라 새로 작성된 코드에 대한 첫 리뷰로 보는 편이 맞다.

위 여섯 줄을 어디까지 믿을 수 있는가. 다른 조건이나 다른 모델에서 다시 확인된 문장은 셋뿐이다. 리뷰 반복과 수정 루프가 다른 활동이라는 것, 단일 파일 조건에서는 가장 자신 있게 보고된 지적이 가장 의심스럽다는 것, 계약 준수율은 아무것도 판별하지 못한다는 것. 나머지는 한 번씩 관측된 것이다.

그리고 이것은 모델 순위표가 아니다. 재는 대상은 모델을 감싸는 루프·프롬프트·도구 권한·정지 조건이지 어느 모델이 더 똑똑한가가 아니다. 순위로 인용되면 오독이다.


1부. 리뷰 한 번은 얼마나 보는가

1. 시작은 단순한 의심이었다

우리 워크플로에는 AI 리뷰가 들어가 있었다.

코드 작성

AI 리뷰

수정

다시 AI 리뷰

필요하면 다시 수정

최대 세 라운드까지 자동으로 반복했다.

이 구조는 사실 하나의 가정에 기대고 있다.

리뷰를 더 하면 코드가 더 좋아진다.

문제는 이 문장이 너무 당연해 보여서, 실제로 재보지 않았다는 것이다.

그래서 직접 측정하기로 했다.

다만 이 연구에서 가장 많이 배운 것은 AI 리뷰 자체보다 측정하는 방법이었다. 실험을 진행하면서 내가 내린 결론이 계속 뒤집혔다. 대부분은 처음 생각보다 덜 극적인 방향이었다.

가장 대표적인 사례는 이것이다.

“AI 수정이 같은 버그를 4/4로 재현했다.”

처음에는 그렇게 보였다.

나중에 확인해 보니 버그가 아니라 테스트가 프로덕션에서 도달할 수 없는 상태를 고정하고 있던 것이었다.

이 연구는 그런 정정을 계속 쌓아간 기록이다.


2. 같은 코드를 그대로 두고 10번 리뷰해봤다

첫 대상은 실제 펌웨어 모듈이었다.

  • 파일: swing_capture.c
  • 크기: 359줄
  • 구조: 센서 스레드가 쓰고 ML 스레드가 읽는 링버퍼
  • 포함된 문제 영역: 동시성, 상태 불변식, 자원 한계
  • 테스트: 18개

여기서는 코드를 전혀 바꾸지 않고 같은 입력을 10번 독립적으로 리뷰시켰다.

각 리뷰는 완전히 새로운 프로세스로 실행했다. 같은 대화 안에서 반복 질문하면 이전 답변이 다음 리뷰에 영향을 주기 때문이다.

리뷰어에게는 대상 코드만 줬고, 저장소는 파일시스템 수준에서 제거했다.

2.1 리뷰 결과는 생각보다 크게 흔들렸다

run:       01  02  03  04  05  06  07  08  09  10
findings:   6   6   5   7   5   7   9   5   4   7

같은 코드인데 어떤 실행은 4건, 어떤 실행은 9건을 냈다.

2.25배 차이다.

10번의 리뷰에서 나온 61개 지적을 같은 결함끼리 묶으니 14개의 고유 그룹이 됐다.

이 묶음이 자의적이지 않은지도 확인했다.

  • 순서를 섞어 3회 다시 클러스터링: ARI 1.000
  • 다른 회사 모델로 같은 61건을 다시 클러스터링: 동일한 14개 그룹

ARI(Adjusted Rand Index) — 두 클러스터링이 같은 분할인지를 재는 값. 1.000이면 완전히 같고, 0이면 무작위로 묶은 것과 다르지 않다. 여기서는 “같은 결함끼리 묶은 결과가 묶는 사람(모델)이나 순서에 따라 달라지지 않는가”를 확인하는 데 쓴다.

이 기준으로 리뷰 횟수를 늘렸을 때 발견되는 고유 결함 비율을 계산하면 다음과 같다.

리뷰 1회 :  6.1 / 14  = 44%
리뷰 3회 : 10.0 / 14  = 71%
리뷰 5회 : 11.6 / 14  = 83%
리뷰 8회 : 13.2 / 14  = 94%

처음에는 이것만 보고 이렇게 결론 내리기 쉬웠다.

“AI 리뷰 한 번은 절반 정도를 본다.”

그런데 14개를 실제 코드 전체와 대조해 판정하자 이야기가 달라졌다.


3. 가장 자주 발견된 지적이 오히려 오탐이었다

14개 그룹 중 실제 결함은 10개, 오탐은 4개였다.

진짜 결함 10개
오탐      4개
오탐률    29%

오탐(false positive) — 리뷰어가 결함이라고 보고했지만 실제로는 결함이 아닌 것. 이 글에서 “진짜/오탐” 판정은 대상 파일이 아니라 저장소 전체를 읽고 내린 것이다.

문제는 오탐의 위치였다.

오탐 4건의 평균 발견 빈도    6.8 / 10
진짜 10건의 평균 발견 빈도   3.4 / 10

발견 빈도 상위 5개 중 4개가 오탐이었다.

그룹빈도리뷰어 주장실제
D028/10재초기화 시 큐에 남은 디스크립터가 오염된다init()은 부팅 시 1회만 호출
D037/10ring_total == 0에서 언더플로가 난다ring_push()가 항상 먼저 실행돼 해당 상태에 도달 불가
D047/10force_expire()가 다른 스레드에서 호출돼 경쟁 조건이 생긴다실제로는 같은 sensor_thread
D075/10특정 호출 순서에서 오작동한다실제 호출 순서가 다름
10회 독립 리뷰에서 오탐 4건은 평균 6.8회 보고됐고 진짜 결함 10건은 평균 3.4회 보고됐다. 가장 많이 보고된 5개 중 4개가 오탐이다.

네 오탐에는 공통점이 있었다.

대상 파일 안에서는 반박할 수 없고, 다른 파일을 봐야만 틀렸다는 것을 알 수 있었다.

  • 호출 순서
  • 초기화 경로
  • 실제 실행 스레드
  • 외부 불변식

리뷰어들은 같은 파일만 봤다. 따라서 모두 같은 정보를 몰랐다.

그래서 같은 잘못된 결론에 반복해서 도달했다.

이 지점에서 중요한 교훈이 하나 생겼다.

합의는 진실의 증거가 아닐 수 있다.

때로는 여러 리뷰어가 모두 같은 정보를 갖고 있지 않기 때문에 같은 오답에 수렴한다.

오탐을 제외하고 다시 계산하면 리뷰 1회의 실제 결함 커버리지는 44%가 아니라 34%였다.

리뷰 횟수고유 그룹 14개 기준판정된 실제 결함 10개 기준
1회44%34%
3회71%61%
5회83%76%

따라서 이 데이터에서 더 정확한 표현은 이쪽이다.

리뷰 한 번은 실제 결함의 약 1/3만 본다.


2부. AI가 만든 버그인 줄 알았더니 테스트였다

4. 리뷰와 수정을 반복하면 좋아질까

다음에는 실제 auto-fix 루프를 만들었다.

리뷰 → 수정 → 리뷰 → 수정 → ...

5라운드를 한 루프로 보고 3회 반복했다.

수정하는 AI는 테스트를 보지 못하게 했다. 매 라운드가 끝난 뒤 테스트만 별도로 실행했다.

결과만 보면 위험해 보였다.

15개 라운드 중 12개가 RED
네 번의 독립 실행에서 정확히 같은 4개 테스트가 반복해서 실패

여기서 “네 번”은 루프 세 개의 라운드 1과, 그와 별개로 돌린 게이트 실행 한 번이다. 네 실행은 지적 건수도(4·5·6·7건) 결과 코드 크기도(439~505줄) 달랐는데 파손 지점은 같았다.

처음에는 거의 확신했다.

“AI가 같은 버그를 반복해서 만든다.”

그런데 이 결론은 틀렸다.

4.1 깨진 것은 코드가 아니라 테스트의 가정이었다

AI가 추가한 핵심 수정은 이런 가드였다.

if (ring_total == 0) {
    return;
}

한 파일만 보면 합리적인 방어 코드다.

문제는 실제 프로덕션에서는 해당 함수가 호출되기 전에 ring_push()가 반드시 먼저 실행된다는 점이었다.

즉:

프로덕션:
ring_push()

arming()

이 순서가 보장되므로 ring_total == 0 상태에서 arming이 호출될 수 없다.

그런데 일부 단위 테스트는 reset() 직후 push 없이 arming을 호출하고 있었다.

즉 테스트가 프로덕션에서 존재하지 않는 상태를 고정하고 있었다.

그래서 AI가 추가한 가드는 실제 동작에는 영향을 주지 않았지만 테스트는 깨뜨렸다.

코드는 안 깨졌다. 테스트만 깨졌다.

이 경험 때문에 이후 실험에서는 테스트를 단순한 “정답지”로 취급하지 않았다.

테스트가 실패했다면 먼저 물어야 한다.

이 테스트가 정말 프로덕션에서 도달 가능한 상태를 검사하고 있는가?


5. 이번에는 계약부터 만든 뒤 다시 실험했다

첫 실험의 가장 큰 문제는 오라클이었다.

오라클(oracle) — 어떤 결과가 옳은지 그른지를 판정하는 기준. 여기서는 인수 테스트가 오라클이다. 오라클 자체가 틀리면 그 위에서 잰 모든 수치가 무효가 된다.

테스트 실패를 보고도 항상 이렇게 반박할 수 있었다.

“그 테스트가 과잉 명세였던 것 아닌가?”

그래서 두 번째 과제에서는 순서를 바꿨다.

1. SPEC 작성
2. 19개 acceptance criterion 동결
3. SPEC만 보고 테스트 작성 (19개 조건에서 인수 테스트 23개가 나왔다)
4. SPEC만 보고 구현
5. 구현자는 테스트를 보지 못함
6. acceptance test로 준수 여부 측정
7. review → fix 반복

대상은 지수 백오프, full jitter, circuit breaker를 포함한 Python 재시도 정책 모듈이었다.

acceptance criterion (AC, 인수 조건) — 명세가 보장하는 동작을 검증 가능한 문장 하나로 쪼갠 것. “동결”은 구현이 존재하기 전에 확정하고 이후 고치지 않는다는 뜻으로, 테스트를 구현에 맞춰 끼웠다는 반박을 막는 유일한 장치다.

이후 “23/23”은 인수 테스트 전부 통과를, “19/19”는 19개 조건 전부 준수를 뜻한다.

5.1 먼저 오라클 자체를 검증했다

독립적인 세 구현이 모두 첫 시도에 계약을 통과했다.

참조 구현          23/23
codex 독립 구현    23/23
claude 구현        23/23

테스트가 정말 잘못된 동작을 잡는지도 확인했다.

정상 구현                19/19
AC-19만 고의로 위반       18/19  → AC-19 정확히 실패
import 불가능한 코드        0/19
SPEC 작성, 19개 인수 조건 동결, SPEC만 보고 테스트 23개 작성, 테스트를 감춘 채 구현. 그 뒤 양성 대조 세 건이 모두 23/23을 통과하고, 음성 대조에서 AC-19만 고의로 위반한 구현이 18/19로 AC-19에서만 실패한다.

이제 “실패 = 스펙 위반”이라고 말할 근거가 생겼다.


3부. 계약을 다 지킨 코드에도 지적은 남는다

6. 정답 코드도 AI에게 보여주면 지적이 나온다

출발점은 이미 검증된 19/19 구현이다.

즉 계약 준수만 놓고 보면 더 좋아질 여지가 없다.

그런데 리뷰어에게 보여주면 계속 지적이 나왔다.

Claude
라운드별 지적 수 (조건마다 7회 실행)

bare prompt:
5, 5, 6, 8, 7, 8, 6
중앙값 6

structured prompt:
11, 11, 12, 11, 12, 12, 11
중앙값 12

14번 전부 지적이 있었다.

bare 프롬프트 / structured 프롬프트 — bare 는 “이 코드를 리뷰해라”에 가까운 최소 지시, structured 는 무엇을 어떤 순서로 볼지와 출력 형식을 규정한 지시다. 두 조건의 차이는 프롬프트뿐이고 코드·모델·라운드 수는 같다.

그리고 많은 지적이 억지는 아니었다.

예를 들면:

  • state 프로퍼티 읽기만으로 상태 전이가 발생
  • BaseException을 잡아 KeyboardInterrupt까지 실패로 처리
  • backoff exponent overflow 가능성

이런 문제는 19개 acceptance criterion 밖에 있었다.

즉 테스트를 모두 통과한다고 해서 리뷰할 것이 없는 것은 아니었다.

계약이 코드 전체를 설명하지 않기 때문이다.


7. 그런데 모델마다 “끝을 아는 방식”이 달랐다

같은 코드와 같은 bare prompt를 codex에도 줬다.

라운드별 지적 수 (조건마다 7회 실행)

claude bare
5, 5, 6, 8, 7, 8, 6

codex bare
1, 1, 1, 1, 0, 1, 1

codex는 실제로 “보고할 것이 없다”고 답한 실행이 있었다.

루프를 돌리면 차이가 더 분명했다.

codex bare
1 → 1 → 0 → 0 → 0

codex structured
6 → 3 → 2 → 1 → 1

claude bare
5 → 6 → 7 → 6 → 7

claude structured
12 → 11 → 10 → 11 → 9

codex는 감소했다.

claude는 감소하지 않았다.

그런데 네 조건 모두 계약 준수율은 계속 19/19였다.

여기서 두 가지를 구분해야 한다.

1. 지적이 줄어드는가

모델 성향에 크게 좌우됐다.

2. 실제로 다 봤는가

지적 수만으로는 알 수 없었다.

codex bare가 5라운드 동안 본 고유 문제는 사실상 두 계열뿐이었다.

반면 structured 조건은 bare가 한 번도 언급하지 않은 실패 모드를 더 찾았다.

  • clock failure
  • process-control exception
  • half-open wedge
  • stale cooldown

따라서:

침묵은 완주의 증거가 아니다.

덜 말해서 멈춘 것과 충분히 봐서 멈춘 것은 외형상 구분하기 어렵다.


8. 테스트는 모두 통과했는데 코드 품질은 크게 갈렸다

이 시점에서 새로운 질문이 생겼다.

테스트가 모두 통과한다면, 실제 코드는 좋아지고 있는가?

계약 준수율만 보면 47라운드가 전부 동일했다.

그래서 기존 스냅샷에 다른 지표를 얹었다.

  • Differential testing(차등 테스트) — 두 판본에 같은 입력을 그대로 재생해 관측 동작이 갈리는지 보는 방법. 테스트가 안 보는 영역까지 “실제로 동작이 바뀌었는가”를 잡는다.
  • LOC — 코드 줄 수.
  • Cognitive Complexity — 코드를 읽을 때 머리에 쌓아야 하는 중첩·분기의 양을 재는 지표. 순환복잡도와 달리 중첩에 가중치를 준다.
  • worst-function complexity(최악 함수) — 파일 전체 합이 아니라, 가장 복잡한 함수 하나의 값. 읽는 비용이 어디에 뭉쳐 있는지를 본다.
  • mutation score(뮤테이션 점수) — 코드에 인위적 결함을 심었을 때 테스트가 잡아내는 비율. “테스트가 실제로 무엇을 보증하는가”의 대리 측정이다.
  • relative churn(상대 churn) — churn 은 한 라운드에서 추가·삭제된 줄 수, 즉 변경량 이다(품질이 아니라 손댄 양). 상대 churn 은 그 누적치를 파일 크기로 나눈 값이라 크기가 다른 파일끼리 비교할 수 있다.

8.1 계약 밖에서 실제 동작이 얼마나 바뀌었나

300개 결정적 시나리오로 v0와 각 라운드를 비교했다.

47라운드 전체에서 관측 가능한 동작 변화는 한 종류뿐이었다.

half_open → open

쟁점은 다음과 같다.

cooldown은 지났지만 아직 아무도 호출하지 않은 circuit breaker를 open으로 볼 것인가, half_open으로 볼 것인가?

계약은 이 질문에 답하지 않았다.

그래서 어느 쪽도 19/19를 위반하지 않았다.

흥미로운 것은 같은 리뷰 루프가 이 선택을 여러 번 뒤집었다는 것이다.

한 라운드에서는:

cooldown이 지났어도 실제 trial call 전까지는 open이어야 한다.

다음 라운드에서는:

cooldown이 끝났다면 실제 동작상 이미 half_open이므로 그렇게 보고해야 한다.

둘 다 그럴듯했다.

둘 다 계약을 지켰다.

그리고 서로 반대였다.

이것이 중요한 이유는 다음과 같다.

루프가 같은 결정을 반복해서 뒤집는다면 코드 문제가 아니라 스펙의 빈칸일 가능성이 높다.


9. “계약이 성기면 진동한다”도 정확한 설명은 아니었다

처음에는 자유 공간이 많으면 AI가 그 안에서 계속 흔들린다고 생각했다.

그래서 19개 AC 대신 8개 AC만 가진 TTL cache 과제를 새로 만들었다.

계약이 훨씬 성겼다.

그런데 결과는 예상과 달랐다.

  • claude 독립 구현 3개
  • codex 독립 구현 1개
  • 모두 8/8 통과
  • 구현 크기는 54~138줄로 2.5배 차이
  • 6개 구현 쌍의 differential test는 전부 0/400
  • 10라운드에서 104개 지적을 반영해도 관측 동작은 그대로

계약이 비워둔 부분이 많아도 행동은 일치했다.

왜일까?

관례가 강한 영역이었기 때문이다.

관례가 강한 영역
- TTL 경계
- LRU refresh
- len 의미

→ 네 구현 모두 같은 선택

반면 잘못된 타입에 어떤 예외를 던져야 하는지처럼 관례가 약한 곳에서는 구현이 갈렸다.

관례가 약한 영역
- TypeError?
- ValueError?
- 검증하지 않음?

→ 네 구현이 세 가지 선택

그래서 현재의 더 정확한 결론은 이것이다.

진동을 만드는 것은 단순히 계약이 성긴 것이 아니라, 계약도 없고 강한 관례도 없는 자리다.


4부. 무엇이 실제로 코드를 바꿨나

10. structured prompt는 단순히 “더 많이 찾는 프롬프트”가 아니었다

같은 모델, 같은 코드, 같은 라운드 수인데 프롬프트에 따라 최종 코드가 크게 달랐다.

v0:

LOC 121
Cognitive Complexity 26
worst function 7

최종 결과:

조건LOCCognitive Complexityworst function
claude structured120.721.76.3
claude bare150.732.710.3
codex structured105249
codex bare126297
fixer에게 계약 없음1784115

structured 조건은 두 모델 모두 bare보다 코드 팽창을 억제했다.

특히 claude에서는 방향 자체가 반대였다.

structured → 더 작고 단순
bare       → 더 크고 복잡

다만 Cognitive Complexity와 LOC의 상관도 높았다.

이 데이터에서는 r = 0.807이었다.

그래서 “복잡도가 독립적으로 크게 개선됐다”고 과장해서는 안 된다.

더 안전한 해석은:

structured prompt가 코드 부피와 복잡도 증가를 함께 억제했다.


11. churn은 품질보다 비용에 가까웠다

라운드별 변경 줄 수를 보면 모델 차이가 매우 컸다.

codex bare
10 → 6 → 0 → 0 → 0

codex structured
41 → 16 → 15 → 5 → 8

claude bare
29 → 45 → 73 → 53 → 68

claude structured
69 → 71 → 73 → 49 → 100

codex는 실제로 변경량이 마른다.

claude는 그렇지 않았다.

하지만 churn이 많다고 최종 코드가 항상 나쁜 것은 아니었다.

가장 많이 뜯어고친 claude structured가 오히려 더 작고 단순하게 끝난 경우도 있었다.

따라서 이 실험에서 churn은:

품질 지표라기보다 비용과 재검토 범위를 나타내는 지표

에 더 가까웠다.


12. 한 번 크게 고친 뒤에는 다시 리뷰해야 할까

처음 처방은 단순했다.

여러 번 리뷰하고, 이슈를 합친 뒤, 한 번에 수정하고 끝내자.

그런데 한 가지 문제가 남았다.

수정이 크면 그 수정 자체가 새로운 코드를 만든다.

그럼 다시 봐야 하지 않을까?

이 질문을 실험했다.

12.1 먼저 “코드를 안 바꿔도 새 지적은 나온다”를 통제했다

동일 코드에 독립 리뷰를 반복하면 표집 분산 때문에 새 지적이 나온다.

그래서 기존 리뷰 결과에서 가능한 서로소 조합을 전수 계산해 “코드가 안 바뀌었을 때의 신규율”을 만들었다.

claude, 코드 무변경:
중앙값 11%
범위 0~31%

귀무분포(null distribution) — “아무 효과도 없을 때 이 값이 어디까지 나오는가”의 분포. 코드를 바꾸지 않았을 때의 신규율이 여기에 해당하고, 실제 수정 뒤의 값이 이 범위를 벗어나야 비로소 “수정 때문”이라고 말할 수 있다.

그 다음 실제 수정 뒤의 재리뷰를 비교했다.

수정 후 신규율:
77%
89%
63%

단순한 표집 분산으로 설명하기 어려웠다.

그리고 수정자가 받은 이슈의 74~80%는 실제로 다음 리뷰에서 사라졌다.

즉 두 가지가 동시에 참이었다.

  1. 한 번에 수정하는 방식은 실제로 대부분의 기존 이슈를 해결한다.
  2. 수정 폭이 크면 그 수정으로 인해 새로 리뷰해야 할 코드가 많이 생긴다.

수정량과 진짜 신규 지적 비율의 상관은:

r = 0.837  (n = 6)

표본은 작지만 방향은 분명했다.

그래서 재리뷰 기준을 이렇게 바꾸는 편이 자연스럽다.

몇 번째 라운드인가가 아니라 얼마나 많이 바뀌었는가를 본다.

  • 작은 수정 → 재리뷰 수확도도 작다.
  • 파일의 절반을 다시 썼다 → 그것은 재리뷰가 아니라 새 코드에 대한 첫 리뷰다.

13. 리뷰어를 카테고리별로 쪼개면 더 나을까

많은 AI 리뷰 하네스는 fan-out을 사용한다.

예를 들어:

  • design
  • functionality
  • complexity
  • robustness
  • naming
  • style

각 영역을 별도 리뷰어에게 맡기고 마지막에 합친다.

이를 세 조건으로 비교했다.

A. structured 1회
B. 카테고리별 6회
C. 같은 structured prompt 6회

C가 중요하다.

B와 A만 비교하면 단순히 “6번이 1번보다 많이 찾는다”는 결과밖에 안 된다.

같은 6콜끼리 비교해야 fan-out 구조 자체의 효과를 볼 수 있다.

13.1 Python 과제에서는 fan-out이 분명히 이겼다

Claude
동일 structured 6콜
고유 지적 19.7
원시 지적 69.3

카테고리 6콜
고유 지적 30.0
원시 지적 54.7

같은 6콜에서:

  • 고유 지적 +52%
  • 원시 지적 -21%

즉 사람이 읽어야 할 총량은 줄고, 발견되는 종류는 늘었다.

새로 나온 항목은 주로:

  • robustness
  • naming
  • style

이었다.

일반 리뷰어가 중요도 순으로 결과를 제한할 때 잘리는 영역이었다.

13.2 하지만 C 펌웨어에서는 재현되지 않았다

C 펌웨어에서 동일 예산으로 비교했을 때:

single reviewer recall: 50%
fan-out recall:          43%

재현율(recall) — 존재하는 진짜 결함 중 몇 퍼센트를 찾아냈는가. 정밀도(precision) 는 반대로, 보고한 지적 중 몇 퍼센트가 진짜였는가다. 둘은 대개 맞바꿈 관계다 — 말을 아끼면 정밀도가 오르고 재현율이 떨어진다. 이 글의 재현율 분모는 알려진 결함 집합이므로 실제보다 낙관적이다(§23.5).

오히려 단일 리뷰가 조금 나았다.

이유로 가장 그럴듯한 것은 리뷰 축이 서로 잘 분리되지 않았기 때문이다.

예를 들어 state-invariant 렌즈는 모듈 내부 상태에 집중하게 만들었고, 그 결과 저장소 접근으로 제거했던 오탐 하나가 다시 나타났다.

따라서 fan-out을 일반 법칙으로 쓰면 안 된다.

리뷰 축이 실제로 분리되는 대상에서만 fan-out이 이득이다.


5부. 가장 강한 레버는 저장소를 읽게 하는 것이었다

14. 저장소 접근은 이 연구에서 가장 강한 오탐 제거 수단이었다

1부의 오탐 4개는 모두 다른 파일을 보면 반증할 수 있었다.

그래서 조건을 딱 하나 바꿨다.

- 너에게는 다른 파일과 저장소가 없다.
+ 프로덕션 소스 트리를 읽을 수 있다.
+ 호출자, 헤더, 초기화 경로, 스레딩을 확인한 뒤 결함을 주장해라.

정답지가 들어 있는 work-docs/는 계속 차단하고 src/만 읽게 했다.

14.1 Claude 결과

조건원시 지적고유 그룹진짜 결함오탐
파일만6114104
repo 접근30770

오탐 4개가 모두 사라졌다.

그리고 파일 하나만 봐서는 찾기 어려운 실제 결함도 새로 발견했다.

예를 들면:

  • FIFO overflow 이후 링 히스토리에 시간 구멍이 남는 문제
  • pre-roll과 debounce 길이의 상호작용으로 앞 스윙이 뒤 스윙 키로 기록되는 문제
  • 저전력 50 Hz 샘플을 1000 Hz로 라벨링하는 문제

이런 문제는 파일 하나만 봐서는 성립 여부를 판단할 수 없다.

14.2 다른 모델에서도 같은 오탐 4개가 사라졌다

codex에서도 같은 조건을 돌렸다.

결과:

D02  8/10 → 0
D03  7/10 → 0
D04  7/10 → 0
D07  5/10 → 0

서로 다른 회사의 두 모델이 같은 오탐 4개를 모두 제거했다.

이 연구에서 가장 강한 cross-model 재현 결과다.

다만 두 모델의 행동 방식은 달랐다.

claude: 지적 수 감소 (61 → 30)
codex:  지적 수 증가 (27 → 37)

저장소를 읽게 했을 때 한 모델은 말을 줄였고, 다른 모델은 오히려 더 많이 찾았다.

중요한 것은 말의 양이 아니라 사실 확인 가능성이었다.


15. 저장소 접근에도 비용이 있다

저장소 접근 조건은 정밀도를 크게 높였지만 재현율은 떨어졌다.

파일만
precision 71%
recall    77%

repo 접근
precision 100%
recall    54%

왜일까?

리뷰어가 실제 호출 경로를 확인하면서:

“이론적으로는 가능하지만 실제로는 거의 도달하지 않는다.”

라고 판단하고 지적을 덜 내기 때문이다.

그 과정에서 오탐뿐 아니라 저빈도 실제 결함도 일부 사라졌다.

즉:

저장소 접근은 공짜가 아니다. 오탐을 줄이는 대신 희미한 결함도 일부 버린다.

둘 다 필요하면 file-only와 repo-open 조건을 병행하는 방법도 있다.

이 실험에서는 두 조건의 합집합이 20콜에 13/13까지 갔다.

물론 호출 비용은 두 배다.

파일만 준 조건은 지적 61건, 고유 그룹 14개, 진짜 10개, 오탐 4개, 정밀도 71%, 재현율 77%. 저장소를 읽게 한 조건은 30건, 7개 그룹, 진짜 7개, 오탐 0개, 정밀도 100%, 재현율 54%. 같은 오탐 네 개가 서로 다른 회사의 두 모델에서 모두 사라졌다.

16. “계약을 주면 회귀가 막힌다”는 결론은 기각됐다

초기에는 다음처럼 보였다.

계약 없음 → 수정자가 지적을 거의 모두 받아들임
계약 있음 → 일부 지적을 거절함

그래서 처음에는:

수정자에게 계약을 주면 회귀를 막을 수 있다.

라고 생각했다.

하지만 2×2 실험에서 이 가설은 기각됐다.

칸의 값은 인수 테스트가 깨진 라운드 수 / 그 조건에서 돌린 전체 라운드 수다.

과제계약 있음계약 없음
C 펌웨어3 / 44 / 4
Python0 / 400 / 5

Fisher exact test:

p = 1.0

Fisher 정확검정 · p 값 — 표본이 작은 2×2 표에서 두 조건의 차이가 우연으로 설명되는지를 재는 검정. p = 1.0 은 “관측된 차이가 우연과 구분되지 않는다”는 뜻이고, 계약 유무로 파손이 설명되지 않았다는 말이다.

계약 유무로 C 쪽 파손이 설명되지 않았다.

앞서 본 것처럼 핵심 원인은 테스트가 도달 불가능한 상태를 고정하고 있었던 것이었다.

따라서 지금은 이렇게 정리하는 편이 맞다.

계약은 회귀를 막는 마법의 안전장치가 아니다.

그렇다고 계약이 쓸모없다는 뜻도 아니다.

다른 효과는 분명했다.


17. “계약”은 사실 두 가지였다

프롬프트를 다시 뜯어보니 우리가 계약이라고 부르던 것이 두 요소를 묶고 있었다.

A. 요청받지 않은 코드까지 건드리지 마라
B. 공개 인터페이스와 동작 범위는 고정이다
C. 그 고정된 범위의 구체적 내용이 담긴 SPEC

B와 C를 분리해서 실험했다.

수정자에게 준 것거절률최종 LOC누적 churn
A만26%254195
A+B50%237136
A+B+C26%202191

거절률 — 수정자에게 넘긴 지적 중 수정자가 고치지 않기로 하고 근거를 밝힌 비율. 무시하고 지나간 비율이 아니라 명시적으로 거절한 비율이다. 높다고 나쁜 것도, 낮다고 좋은 것도 아니다 — 무엇을 근거로 거절했는지가 실제 정보다.

여기서 역할이 다르게 보였다.

B: “공개 표면은 고정”

코드 팽창과 불필요한 변경을 줄였다.

LOC 254 → 237
churn 195 → 136
C: 실제 SPEC

수정자의 판단을 더 구체적으로 만들었다.

SPEC이 없을 때는 애매하면 코드 자체의 docstring을 근거로 거절했다.

SPEC이 있으면 AC 조항을 직접 인용해 판단했다.

그래서 더 정확한 표현은:

범위 고정은 팽창을 막고, SPEC은 판단을 교정한다.


18. 테스트는 수정자에게 숨기는 것보다 읽게 하되 못 고치게 하는 편이 실무적이다

연구 초기에는 테스트를 수정자에게 숨겼다.

그래야 테스트가 독립 오라클로 남기 때문이다.

하지만 실무에서는 수정자가 테스트를 읽을 수 없는 것이 오히려 비현실적이다.

그래서 후반 실험에서는:

  • 테스트 읽기 허용
  • 테스트 수정 금지

조건을 만들었다.

이 변화는 예상보다 효과가 컸다.

이전에는 리뷰가 모듈 밖의 올바른 수정까지 제안하면 좁은 테스트 하네스가 링크에 실패하는 경우가 있었다.

테스트를 읽은 수정자는 이런 판단을 하기 시작했다.

“이 수정은 테스트까지 바꿔야 한다.”

“현재 테스트가 이 동작을 고정하고 있다.”

“이 문제는 실제이지만 이 파일 범위에서는 고칠 수 없다.”

그 결과 한 루프 전체에서 반복되던 빌드 실패가 0으로 줄었다.

단, 이것은 해결이 아니라 defer다.

accept / decline / defer — 지적 하나에 대한 수정자의 세 가지 처분. accept 는 고침, decline 은 “문제가 아니다”, defer 는 “문제는 맞지만 이 범위에서는 못 고친다”. defer 는 해결된 것이 아니라 미뤄진 것이므로, 세지 않으면 조용히 사라진다.

실제 문제인데 범위 밖이라 고치지 않는 경우도 생긴다.

그래서 읽기 전용 테스트 접근은 안전장치이면서 동시에 범위 제한 장치다.


19. 전체 처방을 묶어 돌려보니 또 하나가 틀렸다

후반에는 그동안 얻은 처방을 한 번에 적용했다.

사람의 최종 선별만 제외하고 다음을 켰다.

  • 저장소 접근
  • 구조화 리뷰
  • fan-out
  • 이슈 병합
  • 한 번 수정
  • 테스트 읽기 / 수정 금지
  • 크기·복잡도·churn 측정
  • 정지 기준 적용

준수만 보면 결과가 좋았다.

RED = 인수 테스트가 깨진 라운드 (깨진 라운드 / 전체 라운드)

기존 file-only loop
RED 12 / 15

repo-access loop
RED 6 / 10

통합 처방
RED 0 / 4

그런데 같은 조건에서 코드는 가장 빠르게 불렸다(4라운드에 +152%). 그리고 정지 규칙이 실패했다.

당시 처방은:

churn이 0에 가까워지면 멈춘다.

였고, 임계값 5줄을 돌리기 전에 코드에 박아뒀다.

실제 churn은:

194 → 262 → 395

오히려 계속 증가했다.

루프가 끝난 이유는 churn이 아니라 지적이 0이 됐기 때문이었다.

즉 내가 피하자고 했던 기준으로 실제 루프가 종료된 셈이다.

왜 앞선 Python 과제에서는 churn이 줄고 C에서는 늘었을까?

가장 그럴듯한 차이는 계약 밀도다.

하지만 이 역시 모든 조건을 완전히 분리한 실험은 아니다.

그래서 현재의 안전한 결론은:

“churn이 0으로 수렴하면 종료”는 보편적 정지 규칙이 아니다.

계약이 약한 코드에서는 round cap 같은 강제 상한이 여전히 필요하다.


6부. 그래서 어떻게 운영할 것인가

20. 결국 무한 리뷰 루프를 어떻게 끝낼 것인가

정지 기준 후보는 네 가지였다.

1. churn → 0
2. 지적 → 0
3. 라운드 상한
4. 아예 수정 루프를 반복하지 않는다

앞의 세 개는 모두 문제가 있다.

churn → 0

일부 조건에서만 작동했다.

지적 → 0

모델에 따라 의미가 다르다.

침묵한 리뷰어가 덜 본 것일 수도 있다.

라운드 상한

수렴이 아니라 단순한 절단이다.

그래서 지금 남는 실무 전략은 오히려 단순하다.

리뷰와 수정의 반복 루프 자체를 기본값으로 두지 않는다.


21. 현재라면 이렇게 운영한다

21.1 가장 중요한 원칙

리뷰는 여러 번. 수정은 한 번.

이 연구에서 가장 강한 결과다.

리뷰만 반복
실제 결함 커버리지
34% → 61% → 76%

리뷰-수정 루프 반복
계약 준수 개선 0
코드 크기 +24% ~ +152%

발견의 이득은 리뷰 쪽에서 나왔다.

위험은 수정 쪽에서 생겼다.

둘을 한 사이클로 묶어 계속 돌리면 “이득당 수정 위험”이 가장 커진다.


21.2 리뷰어에게 저장소 읽기 권한을 준다

이 연구에서 가장 강한 오탐 감소 레버다.

file-only
오탐 4/14

repo-open
오탐 0

그리고 이 결과는 두 모델에서 재현됐다.

단, 저장소 접근 권한이 실제로 붙었는지는 반드시 확인해야 한다.

연구 중에도 여러 번:

모델이 원래 못 한다.

라고 생각했던 것이 사실은 실행 환경 설정 문제였다.

실무용 canary

카나리(canary) — 재기 전에 계측 장치가 실제로 붙어 있는지를 확인하는 프로브. 광산의 카나리와 같은 역할이다. 핵심은 양방향이어야 한다는 것 — 보여야 할 것이 보이는지(POS)와 안 보여야 할 것이 정말 안 보이는지(NEG)를 함께 본다. POS 가 실패했는데 그것을 “격리가 잘 됐다”로 읽으면, 아무것도 측정하지 못한 실행을 성공으로 기록하게 된다.

한 달에 한 번 정도 리뷰어에게 이렇게 시켜도 된다.

“방금 본 파일이 아니라 호출자 파일의 N번째 줄을 그대로 인용해라.”

못 하면 실제로 저장소 접근이 안 붙어 있는 것이다.


21.3 합의 횟수를 진위 점수로 쓰지 않는다

file-only 조건에서:

오탐 평균 발견 빈도   6.8 / 10
진짜 평균 발견 빈도   3.4 / 10

즉 이 데이터에서는 합의가 오히려 반-예측적이었다.

리뷰어들이 같은 정보를 모르는 상태라면 다수가 같은 오답에 동의할 수 있다.


21.4 가능하면 두 모델을 섞는다

기존 실행 결과의 모든 조합을 전수 계산했을 때 모델 혼합이 같은 모델 반복보다 효율적이었다.

혼합 3콜 (claude 2 + codex 1)
→ 평균 진짜 3.57개

claude 단독 6콜
→ 평균 진짜 3.43개

같은 모델을 여러 번 돌리는 것보다 서로 다른 모델의 관점을 섞는 편이 낫다는 결과다. 콜 수가 2 이상인 모든 지점에서 혼합이 우세했다.

이 역시 repo-open 조건에서 두 모델이 서로 다른 실제 결함을 보는 것과 일치한다.


21.5 fan-out은 무조건 쓰지 않는다

Python 과제에서는 같은 6콜에서 고유 지적이 +52% 늘었다.

하지만 C 펌웨어에서는 같은 예산에서 이득이 없었다.

따라서 먼저 봐야 할 것은:

리뷰 축이 실제로 독립적인가?

이다.

design / naming / robustness처럼 비교적 분리되는 영역은 효과가 있을 수 있다.

반대로 concurrency / state invariant / lifetime처럼 서로 겹치는 축은 호출만 늘릴 수 있다.


21.6 사람이 한 번 훑는다

fan-out 결과 39건을 판정했을 때:

REAL   31%
STYLE  69%
WRONG   0%

흥미로운 것은 리뷰어가 사실을 지어낸 것이 아니라, 대부분이 이름·주석·구조 같은 사소한 항목이었다는 점이다.

(판정자는 이 실험의 저자이고, 팬아웃 배타 항목과 공통 항목의 차이는 p = 0.236으로 유의하지 않다.)

이런 것까지 전부 자동 수정자에게 넘기면 코드가 불필요하게 흔들린다.

그래서 최종 수정 전에 사람의 선별 단계가 필요하다.


21.7 수정자에게는 세 가지를 준다

  1. 합쳐진 이슈 목록
  2. SPEC 또는 최소한 “공개 표면은 고정”이라는 범위 지시
  3. 읽기 전용 테스트

그리고 모든 지적에 대해:

accept
decline
defer

중 하나를 명시하게 한다.

지적을 받았다고 무조건 수정하게 하지 않는다.


21.8 테스트만 보지 않는다

테스트 통과 여부와 함께 최소한 다음을 기록한다.

test compliance
LOC
complexity
worst-function complexity
churn

계약 준수율은 47라운드 내내 같았다.

하지만 코드 크기와 복잡도는 크게 갈렸다.

테스트는 모두 통과하는데 코드가 계속 커지고 복잡해지면 리뷰 루프가 품질을 개선하고 있다고 보기 어렵다.


21.9 재리뷰는 “몇 라운드째인가”가 아니라 수정량으로 결정한다

작은 수정이면 끝낸다.

큰 수정이면 다시 본다.

다만 그때의 사고방식은:

“한 번 더 검증하자.”

보다:

“새로 쓴 코드가 많으니 이 코드의 첫 리뷰를 한다.”

가 더 정확하다.


22. 예산이 제한돼 있다면

기존 실행의 모든 부분집합을 전수 계산했다.

분모는 판정된 실제 결함 10개다.

호출 수file-onlyrepo model Arepo model Brepo mixed
25.04 / 3.62 FP2.09 / 03.07 / 03.18 / 0
36.11 / 3.90 FP2.54 / 03.27 / 03.57 / 0
46.93 / 3.98 FP2.90 / 03.40 / 03.90 / 0
68.20 / 4.00 FP3.43 / 03.60 / 04.43 / 0

file-only는 콜당 더 많이 찾는다.

하지만 오탐도 같이 따라온다.

즉 비용이 사라진 것이 아니라 사람이 나중에 분류하는 비용으로 이동한 것이다.

두 콜만 쓸 수 있다면
model A 1회
model B 1회

가 현실적인 선택이다.

네 콜을 쓸 수 있다면
model A 2회
model B 2회

가 한 모델을 10회 돌린 결과에 상당히 근접했다.

⚠️ 이 표의 분모는 대조군이 찾은 진짜 10개다. repo 조건이 새로 찾은 결함은 미판정이라 빠져 있으므로, 이 표는 저장소 접근에 불리한 쪽으로 치우쳐 있다.


23. 아직 모르는 것

이 연구에서 확인한 것보다 확인하지 못한 것을 분리해서 적는 것이 중요하다.

표본부터 적는다. 계약과 동결 테스트만으로 판정이 끝나는 tier-1 은 Python 과제 두 개, 사람이 진위를 판정한 tier-2 는 C 펌웨어 모듈 하나이고, 모델은 이다. 대부분의 결과가 루프 세 개 위에 서 있다. 날카로운 대비는 표본이 늘면 대개 줄어든다.

그리고 이 연구가 아예 재지 않은 것이 있다.

  • 설계가 옳은가, 의도가 드러나는가, 이름이 정확한가. 코드리뷰가 보통 1순위로 놓는 것들인데 여기서는 하나도 측정되지 않았다. Cognitive Complexity 는 읽는 노력의 대리 측정이지 이해 그 자체가 아니다.
  • 어느 모델이 더 나은 프로그래머인가.
  • 이 결과가 당신의 코드베이스에서도 성립하는가.

자동 실행으로 나온 수치는 상한이 아니라 바닥이다. 처방의 한 단계인 “사람이 지적을 한 번 훑는다”가 빠져 있기 때문이다.

23.1 fan-out이 언제 유효한가

Python에서는 이겼고 C에서는 이기지 못했다.

“리뷰 축의 분리도”가 중요해 보이지만, 그 분리도를 사전에 어떻게 측정할지는 아직 모른다.

23.2 repo access와 narrow lens를 어떻게 같이 쓸 것인가

repo access는 밖을 보라고 한다.

좁은 fan-out lens는 특정 내부 영역에 집중하라고 한다.

둘이 충돌하면 저장소 접근으로 제거했던 오탐이 다시 나타날 수 있다.

아직 안정적인 필터 규칙이 없다.

23.3 계약 없는 대형 코드에서 churn 기반 정지 규칙이 가능한가

현재 데이터에서는 실패했다.

다른 정지 지표가 필요한지, 단순한 상한이 더 현실적인지는 추가 검증이 필요하다.

23.4 “외부 호출자가 있는가”와 “언어·크기” 효과

C 펌웨어와 Python 과제가 너무 다르다.

외부 호출자가 있는 Python 모듈 같은 중간 셀이 있어야 원인을 더 분리할 수 있다.

23.5 개별 지적의 최종 진위 판정

일부 그룹은 판정했지만 전체를 독립 판정자가 다시 검증한 것은 아니다.

특히 저자가 직접 판정한 tier-2 결과는 사례연구로 봐야 한다.

또한 재현율의 분모는 알려진 결함의 집합이지 존재하는 결함의 집합이 아니다. 아무도 못 찾은 것은 분자에도 분모에도 없으므로 모든 커버리지 수치는 위쪽으로 치우쳐 있다.

23.6 실험 환경 격리

실험 후 감사 결과 실제 저장소 접근은 통제됐지만(세션 전사 190개에 도구 호출 0건), 모델별 도구 능력이 완전히 대칭이었던 것은 아니다.

특히 codex는 네트워크 도구를 끌 방법이 없고 claude 쪽은 껐다. 실제 사용 로그에서 해당 도구가 쓰인 것은 아니지만, “모든 능력이 완전히 동일한 조건이었다”고 말해서는 안 된다.

23.7 오염(contamination)

retry_policyttl_cache는 교과서 패턴이고, 측정된 모델들이 훈련 중에 비슷한 구현을 수없이 봤을 것이 거의 확실하다.

여기서 채점하는 것이 “올바른 구현을 낼 수 있는가”가 아니라 “이미 정답인 코드를 둘러싼 루프에서 어떻게 행동하는가”이기 때문에 실격 사유는 아니다. 다만 절대 지적 수는 과제 간에 비교할 수 없다 — 익숙한 대상일수록 더 자신 있는 논평이 나온다.


24. 이 연구에서 가장 많이 바뀐 것은 내 결론이었다

실험을 진행하며 처음 쓴 문장이 계속 수정됐다.

처음 생각나중에 확인한 것
AI 수정이 같은 버그를 4/4로 만든다프로덕션에 존재하지 않는 상태를 테스트가 고정하고 있었다
정답 코드도 AI 리뷰는 끝나지 않는다claude에서는 그랬지만 codex는 실제로 침묵했다
codex가 빨리 멈추는 것이 우월하다침묵한 쪽이 실제로는 훨씬 적게 봤을 수 있다
수정자에게 계약을 주면 회귀가 막힌다2×2 실험에서 기각, p = 1.0
계약 밖 동작은 많이 흔들릴 것이다47라운드에서 실제로 움직인 동작은 한 종류뿐
덜 고칠수록 안전하다더 많이 고친 쪽이 더 작고 단순하게 끝난 경우도 있었다
여러 리뷰어가 반복 지적하면 진짜다file-only에서는 오탐이 실제 결함보다 더 자주 반복됐다
한 번 크게 고치면 다시 리뷰할 필요 없다수정량이 클수록 실제 신규 지적도 크게 늘었다
fan-out은 잡음만 늘린다Python에서는 같은 6콜에 고유 지적 +52%, 원시 지적 -21%
fan-out은 항상 낫다C 펌웨어에서는 같은 예산에서 이득이 없었다
churn이 0으로 수렴하면 멈추면 된다계약이 약한 C 모듈에서는 오히려 194→262→395 증가
codex는 repo-access 조건을 못 돌린다/tmp 실행 환경 설정 문제였다

이 표가 사실 이 연구의 핵심에 가깝다.

AI 리뷰를 연구했지만, 계속 실패한 것은 AI만이 아니었다.

  • 실험 조건을 잘못 해석했고
  • 테스트를 과신했고
  • 설정 문제를 모델 특성으로 착각했고
  • 한 과제에서 나온 결론을 너무 빨리 일반화했다.

그리고 그때마다 다음 질문이 결론을 바꿨다.

정말 버그인가?

다른 모델에서도 같은가?

그 차이는 모델 때문인가, 환경 때문인가?

테스트가 실제로 무엇을 보증하는가?

코드가 테스트를 통과하는 것 말고 실제로 좋아졌는가?


결론

처음 질문은 단순했다.

AI 코드 리뷰는 몇 번 돌려야 할까?

지금은 그 질문 자체가 조금 잘못됐다고 생각한다.

리뷰 횟수와 수정 횟수를 하나의 “라운드”로 묶으면 중요한 차이가 사라진다.

이 데이터에서 반복할 가치가 있었던 것은 리뷰였다.

반복할수록 실제 결함을 더 많이 봤다.

반면 반복 수정은 계약 준수율을 개선하지 않은 채 코드 변경량과 위험만 키우는 경우가 많았다.

그래서 현재의 답은 이것이다.

리뷰는 여러 번. 수정은 한 번.

그리고 수정이 충분히 크다면, 그 다음 리뷰는 “한 라운드 더”가 아니다. 새로 작성된 코드에 대한 첫 리뷰다.

AI 리뷰의 종료 시점을 모델의 침묵에서 찾으려고 하지 않는 편이 낫다.

더 중요한 것은:

  • 리뷰어가 실제 코드 문맥을 볼 수 있는가
  • 테스트가 정말 프로덕션 동작을 고정하고 있는가
  • 수정 범위가 어디까지인가
  • 코드가 얼마나 바뀌었는가
  • 테스트 밖에서 크기와 복잡도가 어떻게 변했는가
  • 같은 설계 결정을 계속 뒤집고 있지는 않은가

이다.

결국 “몇 번 돌릴까?”보다 더 좋은 질문은 이것이다.

무엇을 반복하고, 무엇은 한 번만 해야 하는가?

ecro
글쓴이 ecro

LLM 펌웨어 벤치마크를 만들었습니다. EmbedEval →

프로젝트에 맞는 Claude Code / Cursor / Codex 하네스를 생성하는 도구를 만듭니다. harness-maker →

Datasheet을 읽는 터미널을 만들고 있습니다. NeuroTerm →

댓글

댓글 불러오는 중...