코드 리뷰, 파트 2: 더 잘 속이는 법을 배운 리뷰어
요약
멀티 에이전트 적대적 코드 리뷰 프로세스를 구축하여 Claude Code와 Gemini의 리뷰 성능을 비교 실험한 사례를 다룹니다. Gemini가 코드 컨텍스트에 접근할 수 있게 되었을 때 오히려 존재하지 않는 정보를 꾸며내는 환각 현상이 심화됨을 발견했습니다.
핵심 포인트
- 서로 다른 계보의 모델을 사용하여 코드 리뷰를 수행하는 적대적 방식 제안
- Claude Code와 Gemini를 GitHub Actions에 연결하여 자동 리뷰 환경 구축
- Gemini가 코드 컨텍스트를 확보하자 오히려 환각(Hallucination)이 증가하는 현상 관찰
- 실제 대규모 멀티 테넌트 코드베이스를 대상으로 한 6주간의 실험 데이터 기반 분석
몇 개의 포스트 전, 저는 개발 파이프라인의 일부로서 멀티 에이전트 적대적 코드 리뷰 (multi-agent adversarial code review) 프로세스를 설정하는 것에 대해 글을 썼습니다. 그 전제는 한 팟캐스트에서 얻은 것이었습니다: 만약 하나의 프런티어 모델 (frontier model)이 코드를 작성하고 있다면, 리뷰는 다른 계보의 모델 (lineage model)이 수행하게 해야 한다는 것입니다. 저는 이미 이 방식의 비공식적인 버전을 실행하고 있었습니다. 적대적 프롬프트 (adversarial prompt)를 사용하여 Claude Code가 Claude Code를 리뷰하게 하는 방식이었죠. 실제 문제들을 잡아내는 운이 놀라울 정도로 좋았습니다. 단순히 느낌(vibe)만 믿기에는 충분히 괜찮았기에, 저는 실제 데이터를 직접 수집하기로 결정했습니다.
그래서 제가 설정한 내용은 다음과 같습니다. Claude Code가 PR (Pull Request)을 작성했습니다. 모든 PR은 GitHub Actions를 통해 병렬로 실행되는 2개의 리뷰어에 의해 자동으로 리뷰되었습니다: 적대적 프롬프트를 사용하는 Claude Code와 적대적 프롬프트를 사용하는 Gemini입니다. 저는 모든 내용을 직접 읽었습니다. 그 후, 대부분의 PR을 작성했던 동일한 Claude Code 에이전트가 두 리뷰어의 피드백을 로컬로 가져와 6주 동안 PR별로 점수가 매겨진 장부 (scored ledger)로 증류 (distill)했습니다.
GitHub Action을 통해 PR을 리뷰하도록 Claude Code를 연결하는 것은 사소한 일이었습니다. Gemini를 같은 방식으로 연결하는 것은 그렇지 않았습니다. Claude Code는 GitHub Action 내부에서 Gemini CLI가 작동하도록 하는 방법을 찾아내지 못했고, 결국 저는 Gemini CLI를 로컬에 설치하여 그것이 연결 작업을 수행하도록 했습니다.
데이터를 수집한 지 몇 주가 지났을 때, 저는 Gemini의 리뷰가 피상적이라는 것을 알아차렸습니다. 정확히 틀린 것은 아니었습니다. 얇았습니다. 저는 Gemini가 실제로 저장소 (repository)에 대한 읽기 권한을 가지고 있는지, 아니면 전달받은 diff (차이점)만 보고 있는 것인지 궁금해지기 시작했습니다. 확인해 보았습니다. diff였습니다. 오직 diff뿐이었습니다. diff 외에는 아무것도 없었습니다. 파일 읽기, git 히스토리, 아무것도 없었습니다. 그리고 애초에 GitHub Action을 설정한 주체가 바로 Gemini였습니다. Gemini는 스스로 눈가리개를 설정한 셈이었습니다.
제가 이를 수정하자 Gemini의 리뷰는 더 나빠졌습니다. 더 시끄러워지거나 빈도가 잦아진 것이 아니었습니다. 특정한 방식으로 더 나빠졌는데, 바로 더 자신만만해졌다는 점입니다. 수정 전에는 눈이 먼 Gemini가 무언가를 확인할 수 없다고 정직하게 말하며 수동으로 확인하라고 안내했습니다. 그것은 정직한 실패 모드 (failure mode)였습니다. 하지만 수정 후, 실제로 코드를 읽을 수 있게 되자 Gemini는 꾸며내기 시작했습니다. 존재하지 않는 파일을 인용하고, 작성된 적도 없는 함수와 API 호출을 인용했습니다. 실재하지 않는 발견 사항에 대한 증거를 구축하고, 그것이 실제 발견 사항일 때와 동일한 어조로 전달했습니다.
이것은 실험이었으므로, 수치를 살펴보겠습니다:
약 6주 동안 인증 (auth), 조직별 데이터 격리 (per-org data isolation), 그리고 Prisma 마이그레이션 (migrations)이 포함된 실제 멀티 테넌트 (multi-tenant) 코드베이스(검토 가능한 코드 약 61,000행, 데이터베이스 시드 포함 시 약 400,000행)를 대상으로 95개의 PR (Pull Request)을 점수화했습니다. 사전 사용자 (pre-users) 단계일 수도 있지만, 이것은 장난감 저장소 (toy repo)나 작은 개인 프로젝트가 아닙니다.
저는 데이터를 얻기 전에 3가지 지표를 결정했습니다. 이는 나중에 이야기에 유리한 지표를 선택하지 않기 위함이었습니다: 탈출률 (escape rate, 리뷰어와 CI를 모두 통과하여 나중에 드러난 실제 문제), 허위 합의율 (false consensus rate, 두 리뷰어 모두가 지적했지만 실제로는 사실이 아닌 발견 사항), 그리고 상관관계 결여 (decorrelation, 두 리뷰어 중 한 명만이 잡아낸 실제 문제의 비율)입니다. 모든 발견 사항은 리뷰어가 코드에 대해 주장한 내용이 아니라, 실제 코드와 대조하여 수동으로 분류되었습니다.
최종 집계: 실험 기간 동안 발견된 200개의 실제 문제 중 탈출 사례는 0건이었습니다. 두 리뷰어가 독립적으로 제기한 25개의 발견 사항 중 허위 합의는 0건이었습니다. 그들이 동의할 때마다 두 리뷰어 모두 옳았습니다. 상관관계 결여 (decorrelation)는 약 88%였습니다. 두 리뷰어는 거의 완전히 다른 것들을 잡아냈습니다.
Claude의 발견은 약 81%의 확률로 실제였습니다. Gemini의 발견은 약 45%의 확률로 실제였습니다. 이 수치는 이미 관대한 편입니다. Gemini가 눈이 먼 상태로 리뷰를 수행하며, 순전히 diff (차이점) 읽기에 대한 불안감 때문에 그럴듯하게 들리는 차단 요소 (blockers)를 지어냈던 주간들은 제외된 수치이기 때문입니다.
0개의 탈출(escapes)이 탈출 가능한 것이 0이라는 뜻은 아닙니다. 이는 아직 프로덕션 트래픽(production traffic)을 처리하지 않는 코드베이스에서 우리가 잡아내지 못한 것이 0개라는 의미입니다. 제가 실제 얻은 것보다 더 자신만만하게 들리기 전에 이 차이점을 분명히 기록해 두고 싶습니다. 저는 코드 리뷰 프로세스를 빠져나간 19개의 정당한 이슈들을 발견했지만, 그중 절반은 저의 부실한 QA(Quality Assurance) 작업이나 요구사항 정의(requirements definitions)의 책임입니다.
탈상관성(decorrelation) 수치는 2명의 리뷰어를 운영해야 하는 가장 강력한 근거입니다. PR #148이 그에 대한 가장 깔끔한 증거입니다. 그것은 스프린트 데이터 수집(sprint-data ingestion) 변경 사항이었습니다. Claude가 이를 리뷰했을 때는 아무런 문제도 발견하지 못했습니다. 하지만 Gemini는 Claude가 완전히 놓친 2가지를 잡아냈습니다: 트랜잭션(transaction)으로 감싸이지 않은 멀티 쓰기(multi-write) 시퀀스와, 여러 보드가 하나의 팀 정체성으로 조용히 합쳐지는 데이터 모델(data-model) 버그였습니다. 이 중 어느 것도 사소한 지적(nitpick)이 아닙니다. 만약 제가 그 PR에 Claude만 실행했다면, 이 두 가지 모두 그대로 배포되었을 것입니다.
하지만 탈상관성(decorrelation)이 동등성(parity)과 같은 것은 아닙니다. 단 한 명의 리뷰어에 의해서만 발견된 175개의 실제 이슈 중, 131개는 Claude의 것이었고 44개는 Gemini의 것이었습니다. Gemini의 발견은 고르게 분산되기보다는 몇몇 PR에 집중되어 있었습니다. 둘 다 유지하는 것은 맞습니다. 하지만 둘을 동등하게 유지하는 것은 아닙니다.
이것에 대한 제 생각을 실제로 바꾸어 놓은 발견이 있습니다. Gemini가 실제 읽기 권한(read access)을 갖게 되자, 그 결과물로 나타나는 오탐(false positives)들이 더 이상 불확실하게 들리지 않고 증거처럼 들리기 시작했습니다.
가장 명확한 시연은 같은 날, 서로 다른 두 개의 PR인 171번과 173번에서 일어났습니다. Gemini는 두 PR 모두에서 동일한 동작을 수행했습니다: 쿼리(query)가 조직(organization)에 적절히 스코프(scoped)되어 있는지 확인했다고 주장하며, 이를 증명하기 위해 백엔드 코드 스니펫(code snippet)을 붙여넣었습니다. 171번 PR의 경우, 파일, 함수, 심지어 인용한 인증(auth) API조차 존재하지 않았습니다. Gemini는 실제 코드와 똑같은 형식으로 완전히 허구의 내용을 지어냈습니다. 반면 173번 PR에서는 동일한 동작, 동일한 형식, 동일하게 자신감 넘치는 어조로 실제 저장소(repository)를 토씨 하나 틀리지 않고 정확하게 인용했습니다.
동일한 리뷰어. 동일한 날짜. 동일한 기법. 하나는 조작되었고, 하나는 실제였습니다. 출력물 중 그 무엇도 어느 것이 무엇인지 알려주지 않았습니다. 그것들을 구별할 수 있는 유일한 방법은 제가 직접 파일을 열어보는 것뿐이었습니다.
그 단 하나의 PR 쌍이 검증되지 않은 AI 리뷰가 게이트 역할을 할 수 없다는 주장의 전부입니다. 신중한 오답은 짜증나지만, 자신만만하고 잘 형식화되었으며 완전히 허구인 답변은 위험합니다. 왜냐하면 누군가 확인하기 전까지는 진실처럼 보이기 때문입니다.
몇 개의 PR이 더 지난 후에도 같은 일이 발생했습니다. Gemini는 UI 변경 사항에 대해 심각한 크로스 테넌트(cross-tenant) 데이터 유출을 감지했다고 표시하며, 코드베이스 어디에서도 존재하지 않는 쿼리 및 세션 필드를 인용했고, 병합하지 말라고 저에게 통보했습니다. 두 차례가 지난 후, 이전에 검토했던 같은 PR을 다시 살펴보고 그 주장을 철회했습니다. 이전 발견은 오탐지(false positive)였다고 말입니다. 제가 직접 그것이 진짜가 아님을 확인해야만 결국 자신의 환각(hallucination)을 스스로 잡아냈습니다.
조작 자체를 제외하더라도, 판결 자체가 안정적이지 않았습니다. 완전히 변경되지 않은 동일한 코드가 다른 검토 라운드에서 녹색(green), 심각(critical), 주황색(orange)으로 평가되는 등 변동이 있었습니다. 그리고 이러한 에스컬레이션은 마크다운 파일만 건드린 커밋에 집중되었습니다. Claude는 같은 기간 동안 동일한 커밋들을 검토했지만, 변화가 없었습니다. 이것은 단순히 재검토하는 과정에서 발생하는 잡음(noise)이 아니었습니다. Gemini에게만 특정한 문제였습니다.
반사적인 패턴도 있었습니다.
전체 실험에서 가장 가치 있었던 발견은 논리적 버그(logic bug)가 전혀 아니었습니다. PR #165에서 Claude는 git add -A 명령어로 인해 실제 개발용 인증 우회(auth bypass) 설정이 커밋에 포함된 것을 발견했습니다. 이는 누구나 인증을 완전히 건너뛸 수 있게 만드는 환경 변수(environment variables)였습니다. 해당 설정은 상단에 문자 그대로 "커밋용 아님(not for commit)"이라고 적힌 초안 블로그 포스트 바로 옆에 놓여 있었습니다. Gemini는 그것이 디프(diff)에 바로 나타나 있었음에도 불구하고 완전히 놓쳤습니다. 수정 사항이 반영된 후, 해당 PR에 대한 Gemini의 실제 코멘트는 그 결과로 나타난 권한 상승(privilege-escalation) 경로를 "훌륭한 보안 제어(excellent security control)"라고 불렀습니다. 반면 Claude의 코멘트는 해당 파일을 삭제하라고 지시하며, 특히 인증 우회가 포함된 파일을 명시했습니다.
디프에 아예 포함되어서는 안 될 것을 잡아내는 리뷰어가 영리한 논리적 오류를 찾아내는 리뷰어보다 제 역할을 더 잘 해냈습니다.
한 가지 더 언급할 가치가 있는 것은, 다소 불편한 사실이지만 제가 왜 단일 AI 에이전트가 자신의 작업물을 채점하는 것을 완전히 신뢰하지 않는지에 대한 이유입니다. 이 PR들의 대부분을 작성한 바로 그 Claude 에이전트가 두 리뷰어 모두를 점수 매기는 장부(ledger)를 판정하는 역할도 맡았습니다. PR #68에서 해당 에이전트는 자신에게 유리하도록 오차(off-by-one)를 기록했습니다. 이로 인해 실제 결함이었어야 할 부분이 문제가 전혀 없는 것으로 기록되었습니다. 두 리뷰어 모두 이를 독립적으로 잡아냈습니다. 해당 행에 대한 Gemini의 직관("병합하기에 안전하지 않음(not safe to merge)")은 Claude 스스로 내린 "그저 문서일 뿐입니다(it's just docs)"라는 판단보다 더 정확한 결정이었음이 드러났습니다.
저는 코드를 작성하지도, 코드를 리뷰하지도 않습니다. 저는 프로세스를 관리합니다. 제가 여전히 그 장부의 모든 행을 직접 읽는 이유는 바로 이것입니다. 작업을 수행하는 에이전트는 그 작업의 신뢰할 수 있는 심판이 될 수 없으며, 이는 에이전트가 타인의 PR을 채점할 때도, 특히 자신의 PR을 채점할 때는 더욱 그러합니다.
그래서 제가 여러분의 파이프라인에 2개의 AI 리뷰어를 두고 실제로 권장하는 방법은 다음과 같습니다. 둘 다 유지하십시오. 상관관계가 낮다는 것(decorrelation)은 실재하는 현상입니다. PR #148 하나만 보더라도 두 번째 모델을 실행할 가치가 충분합니다. 하지만 그중 단 하나만이 머지(merge)를 통제(gate)할 수 있으며, 그 모델은 단순히 가끔씩 정확한 발견을 하는 모델이 아니라, 판결(verdict)이 안정적인 모델이어야 합니다. 인간이 배후에 없는 파이프라인은 여러분의 가장 중요한 변경 사항을 처리할 준비가 되지 않았으며, 이는 탈출률(escape rate)이 높았기 때문이 아닙니다. 탈출률은 0이었습니다 (앞서 언급한 주의 사항은 제외하고). 그 이유는 조작된 심각한 발견과 잘못된 머지 사이, 또는 실제 심각한 발견과 무관심 사이를 가로막고 있는 것이, 실제 코드와 대조하여 두 리뷰어를 모두 확인하는 유능한 검증기(verifier)였기 때문입니다. 이 검증기는 이 시스템들이 스스로 할 수 없는 일을 수행하고 있는데, 그 이유는 시스템들이 결정론적(deterministic)이지 않기 때문입니다. 동일한 리뷰어가, 변경되지 않은 동일한 코드에 대해, 디프(diff)에 설명할 만한 아무런 변화가 없음에도 불구하고 여러 라운드에 걸쳐 '이상 없음(clean)'과 '심각함(critical)' 사이를 오갔습니다. 완전 자율형 소프트웨어 생성(fully autonomous software generation)을 향한 모든 시도는 이 문제를 조용히 무시하고 넘어가려 합니다. 왜냐하면 인간이 없는 파이프라인은 기계의 판단이 고정되어 있어야 하는데, 여기의 데이터는 그렇지 않다는 것을 보여주기 때문입니다. 아무런 이유 없이 마음을 바꾸는 판결을 근거로 머지를 통제할 수는 없으며, 그런 현상이 발생하기 전까지는 이를 확인하는 존재를 제거할 수도 없습니다.
그 검증기를 제거한다고 해서 탈출(escape)이 더 많이 발생하는 것은 아닙니다. 대신 더 많은 허위 경보(false alarms)와, 실제 포착된 내용과 똑같아 보이는, 확신에 찬 헛소리(confident nonsense)를 얻게 될 뿐입니다.
직관에 반하는 발견이자 실제로 저를 놀라게 했던 점은, Gemini에게 더 많은 능력을 부여할수록 게이팅(gating) 문제가 더 쉬워지는 것이 아니라 더 어려워졌다는 것입니다. 무언가를 확인할 수 없다고 솔직하게 말하는 눈먼 리뷰어가, 확인했다고 자신 있게 말하는 자원이 풍부한 리뷰어보다 더 안전합니다.
모델을 판단하기 전에 여러분의 하네스(harness)를 먼저 검증하십시오. 저는 그러지 못했습니다. 5주 동안이나 말입니다. 그 기간 동안의 데이터는 제가 원했던 것보다 가치가 떨어집니다.
95개의 PR, 1개의 코드베이스, 1명의 작성자, 6주. 이것은 그리 많은 증거는 아니며, 저는 이것을 실제보다 더 대단한 것처럼 꾸미지 않을 것입니다. 하지만 이것은 단순한 느낌 (vibe) 이상의 것이며, 이 문제가 시작되었던 지점보다는 더 확실하며, 앞으로 제가 이 파이프라인 (pipeline)을 운영하는 방식을 바꿀 만큼 충분한 근거입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기