
Claude Code의 `/code-review` effort는 low로도 충분하다 ― low/high를 각각 3회씩 실측하여 비교한 검출률
요약
Claude Code의 `/code-review` 기능에서 effort 설정을 low와 high로 나누어 결함 검출률을 비교 실험했습니다. 실험 결과, critical 및 high 심각도의 결함은 low 설정만으로도 모두 검출되었으며, high 설정은 시간 소요가 훨씬 길지만 사소한 결함만 추가로 찾아내는 것으로 나타났습니다.
핵심 포인트
- critical/high 심각도 결함은 low 설정으로도 100% 검출 가능
- high 설정은 소요 시간이 수 분으로 늘어나지만 검출 이득이 적음
- 효율적인 코드 리뷰를 위해 기본적으로 low 설정을 권장
- 심각도별 검출 성능 차이를 실측 데이터로 검증
결함을 미리 7건 심어둔 PR에 Claude Code의 /code-review를 low와 high로 각각 3회씩 실행했습니다.
low가 찾아낸 것은 4건, high는 6건입니다.
건수만 보면 high의 승리처럼 보입니다.
하지만 심각도(severity)별로 다시 정렬하면, critical과 high 결함은 3회 모두 low가 전부 찾아냈습니다.
high가 추가로 찾아낸 것은 medium 1건과 low 1건뿐입니다.
그리고 소요 시간은 low가 십수 초인 것에 반해 high는 수 분이 걸렸습니다.
지난번 /code-review의 effort를 실측했을 때는 "토큰과 시간에 여유가 있다면 high"라고 적었습니다.
다만, 그때는 다른 PR에 각각 1회씩 실행하여 건수와 시간만을 살펴보았을 뿐입니다.
high의 10건이 low의 1건보다 많은 것은 사실이지만, 늘어난 부분이 무엇인지는 확인하지 않았습니다.
중요한 결함이 늘어난 것인지, 아니면 사소한 지적만 늘어난 것인지.
리뷰는 찾아낸 것보다 놓친 것이 더 치명적이기에, 그 부분을 명확히 하고 싶었습니다.
그렇게 생각하여 정답을 알고 있는 PR을 준비하고, 동일한 조건으로 다시 측정했습니다.
대상은 검증용 샌드박스인 cc-lab의 PR #1입니다.
태그 지정, 페이지네이션(pagination), 태스크 공유를 추가하는 diff에 미리 7건의 결함을 심어두었습니다.
내역은 critical 1건, high 2건, medium 3건, low 1건입니다.
예를 들어 타인의 태스크를 읽을 수 있는 인가 체크 누락(IDOR)이나, 페이지네이션의 off-by-one과 같이 리뷰에서 찾아내길 바라는 종류의 것들입니다.
npm test는 12건 모두 green이므로, 테스트를 돌려도 실패하지 않습니다.
이 PR과 7건의 내역은 적대적 리뷰(adversarial review)의 검출률을 측정했을 때와 동일한 것입니다. 심어둔 결함 목록은 그쪽에 게재되어 있습니다.
effort는 /code-review low와 같이 커맨드 인자(argument)로 전달하는 수준이며, 가벼운 쪽부터 low / medium / high / xhigh / max가 준비되어 있습니다.
이번에 실행한 것은 low와 high 두 가지입니다.
실행 조건은 다음과 같습니다.
| 항목 | 값 |
|---|---|
| Claude Code | v2.1.223 (모델은 Opus 5, auto mode on) |
| ... |
3회씩 실행한 이유는 1회의 결과가 우연인지 아닌지를 구분하기 위해서입니다.
또한, 이전 run의 컨텍스트가 다음에 영향을 주지 않도록 1회마다 세션을 새로 생성했습니다.
집계는 모든 run을 마친 후, 지적 사항을 파일과 내용 요지로 정규화하여 정리했습니다.
동일한 지적인지 헷갈리는 것은 별개의 것으로 간주하는 쪽으로 분류했습니다.
심어둔 결함과의 대조도 모든 run이 완료된 후에 진행했으며, 리뷰 중에는 정답 리스트를 읽게 하지 않았습니다.
참고로 v2.1.223에서 /review는 /code-review의 에일리어스(alias)가 되었습니다.
이전에는 별개로 구분해서 사용할 필요가 있었지만, 지금은 어느 것을 입력해도 동일한 리뷰가 실행됩니다.
이 기사의 수치는 이 PR 하나에 대한 low 3회, high 3회의 실측 결과입니다.
다른 diff나 다른 규모에서도 동일한 비율이 나오는지는 확인하지 않았습니다.
심어둔 7건을 심각도별로 나열하면 다음과 같습니다.
| 심각도 | 심어둔 수 | low로 검출 | high로 검출 |
|---|---|---|---|
| critical | 1 | 1 | 1 |
| ... |
급히 수정해야 할 critical과 high 3건은, low가 3회 모두 전부 찾아냈습니다.
effort를 높여서 늘어난 것은 medium과 low 각각 1건이었습니다.
개별 내역입니다. ○는 3회 모두 검출, ×는 한 번도 검출되지 않은 것을 나타냅니다.
| 심어둔 결함 (심각도) | low로 검출 | high로 검출 |
|---|---|---|
| 인가 체크 누락 · IDOR (critical) | ○ | ○ |
| ... |

1회차 low에서 돌아온 4건. 3회 모두 동일한 구성이었다
심어둔 7건에 대한 검출 범위는 effort에 따라 고정되어 있었습니다.
low의 4건도 high의 6건도, 3회 실행하는 동안 단 한 번도 바뀌지 않았습니다.
변동이 있었던 것은 이 외곽 부분이며, 그 부분은 후반부에서 다루겠습니다.
이는 공식 문서의 설명과도 일치합니다.
At
low
and medium는 가장 확신하는 결과만을 보고하므로, 오탐(false positives)이 적게 나타납니다. 반면 high부터 max까지는 더 넓은 그물을 던지며, 리뷰 결과가 덜 확실한 결과물도 포함될 수 있습니다.
low와 medium은 확신도가 높은 것만 반환하고, high부터 max는 그물을 넓혀 확신이 낮은 것까지 포함합니다.
effort는 정밀도의 조절 노브(knob)가 아니라, 어디까지 찾아낼 것인가를 결정하는 조절 노브인 셈입니다.
그만큼 찾아낸 결과물의 성격도 달랐습니다.
low의 4건은 모두 의도적으로 심어놓은 결함이었으며, 오답이 없었습니다.
반면 high의 10건은 심어놓은 결함이 6건, 심어놓지 않은 실제 버그가 1건(후술할 외래 키 관련 건), 그리고 나머지 3칸은 실행할 때마다 바뀌는 부분입니다.
이 3칸의 지적이 타당했는지까지는 대조해 보지 않았으므로 알 수 없습니다.
표의 최하단인 경쟁 상태(race condition)만은 둘 다 ×였습니다.
공유 자원에 대한 중복 삽입을 방지하는 처리가, 기존 레코드가 있는지 확인한 후 삽입하는 check-then-act 방식으로 작성되어 있습니다.
확인과 삽입 사이에 다른 요청이 끼어들면 중복이 허용되는 형태이며, 심지어 트랜잭션도 UNIQUE 제약 조건도 없습니다.
이 문제는 low와 high를 합쳐 총 6회 실행하는 동안 단 한 번도 지적되지 않았습니다.
즉, high라고 해서 반드시 망라(coverage)된다는 뜻은 아닙니다.
게다가 이번에 테스트한 범위 내에서는 effort를 높이거나 횟수를 늘려도 나타나지 않았습니다.
이러한 클래스의 문제는 UNIQUE 제약 조건이나 트랜잭션처럼 코드 측면에서 구조적으로 막아두는 것이 확실합니다.
low와 high는 소요되는 시간의 단위가 달랐습니다.
| effort | 1회차 | 2회차 | 3회차 |
|---|---|---|---|
| low | 13초 | 15초 | 15초 |
| high | 약 6분 | 약 8분 | 약 9분 |
low는 3회 모두 15초 이내였고, high는 분 단위였습니다.
수십 배의 차이입니다.
다만, 이 차이는 단순히 대기 시간만의 문제가 아닙니다.
low라면 diff를 보면서 가볍게 돌릴 수 있지만, high는 실행하기로 결정한 후 자리를 비워야 할 정도의 시간입니다.
먼저 low는 3회 모두 완전히 동일한 4건이었습니다.
변한 것은 보고된 행 번호뿐이었으며, IDOR 지적 위치가 :230 / :246 / :250으로 어긋났습니다.
같은 문제를 가리키고 있음에도, 어떤 행을 대표로 내보낼지가 흔들리는 모습입니다.
반면 high는 매번 정확히 10건이었지만, 내용은 달랐습니다.
3회 동안 나온 지적들을 클러스터(cluster)로 묶으면 총 13종류가 있었고, 그중 3회 모두 등장한 것은 7종류였습니다.
즉, 나머지 6종류가 회차마다 3칸을 차지하기 위해 경쟁하고 있는 것입니다.
애초에 high는 후보를 찾아내는 finder와, 이를 반증하려는 검증 에이전트(verification agent)의 2단계 구조로 작동합니다.
완료 보고에 그 내역이 나오는데, 후보 단계에서는 숫자가 일치하지 않습니다.
| run | 후보 | 검증 에이전트 |
|---|---|---|
| 1회차 | 42건 | 30체 |
| ... |
입구와 검증 규모가 다른데, 출구만 매번 10건입니다.
워크플로우의 자기 설명(self-explanation)에는 a ranked, capped findings report라는 표현이 있었습니다.
여기서부터는 추측입니다.
후보 수가 변해도 보고 수가 변하지 않는다면, 상위 랭킹 10건에서 잘린다고 해석하는 것이 자연스럽습니다.
그렇게 생각하면, 교체되는 3칸은 커트라인 근처의 지적들이 회차마다 순위가 바뀌는 것이라는 설명이 가능합니다.
공식 문서에서 이 상한선이 명시되어 있는지는 확인되지 않았으므로 단정하지는 않겠습니다.
다만, 만약 정말로 상한선이 있다면 high를 돌려도 보고에 포함되지 못한 채 사라지는 지적들이 남게 됩니다.
high를 돌리면 망라된다고 할 수 없는 이유가 바로 여기에 있습니다.
또 하나, 설명 자체가 어긋난 사례도 있었습니다.
의도적으로 심어놓은 7건 외에 high가 찾아낸 지적 중에는 외래 키(foreign key)와 DELETE 관련 문제가 있습니다.
1회차와 3회차는 "외래 키가 유효하므로 DELETE 시 500 에러가 발생한다"라고 보고했는데, 실제 기기에서 확인한 결과 이 보고가 맞았습니다.
하지만 2회차에서만은 "PRAGMA foreign_keys가 설정되지 않아 외래 키가 무효하며, 고아 행(orphan rows)이 남는다"라는 정반대의 메커니즘을 보고했습니다.
지적한 위치는 둘 다 src/db.ts:54로 동일합니다.
위치가 정확하다고 해서, 덧붙여진 설명까지 반드시 정확한 것은 아닙니다.
저의 운영 방식은 다음과 같이 변경했습니다.
일상적인 diff는 low. 십수 초면 끝나기 때문에, 작성하는 흐름 그대로 바로 돌려서 수정합니다. - 큰 변경 사항, 아키텍처 설계가 포함된 부분, 영향이 오래 지속되는 부분은 high
- high의 지적은 위치는 믿더라도 설명까지 그대로 받아들이지는 않습니다.
참고로 effort를 생략하면 직전에 입력한 레벨이 재사용되므로, 전환할 때는 /code-review high와 같이 명시하여 입력합니다.
낮은 레벨인 상태로 중요한 PR에 진입하는 것이, 이 사양에서 가장 발생하기 쉬운 사고입니다.
이번에 실행한 것은 low와 high뿐입니다.
medium은 일상적인 타협점으로 적당할 것이라 생각하며, high 위에는 xhigh와 max도 있지만, 둘 다 실측하지 않았기에 예측치에 불과합니다.
이전 글에서는 "여유가 있다면 high"라고 썼지만, 거기서 한 단계 변화를 준 것입니다.
중대한 결함이 low에서 잡힌다면, 일상적인 diff에 매번 몇 분씩을 할애할 이유는 적다고 생각했습니다.
판단 근거를 하나 더 들자면, 그 코드를 얼마나 설계한 후 작성했느냐입니다.
이는 검증에서 나온 수치가 아니라 운영상의 실감이지만, 사전에 설계를 치밀하게 할수록 low로 충분하며, 생각 없이 휘갈겨 쓴 코드일수록 high에 시간을 들일 가치가 있습니다.
단, 이것을 "low만으로 충분하다"라고 읽지는 말아주세요.
이번에 low가 critical과 high를 놓치지 않았던 것은, 7건을 심어둔 이번 PR의 사례입니다.
놓치면 타격이 큰 변경을 앞두고 몇 분을 아낄 이유는 없으므로, 그 부분은 솔직하게 effort를 높이는 편이 좋습니다.
그리고 잊지 말아야 할 것은 경합 상태(race condition)에 관한 1건입니다.
low에서도 high에서도, 6회 모두 나타나지 않았습니다.
어느 것을 선택할지의 문제 이전에, 리뷰에 전적으로 맡길 수 없는 영역이 존재합니다.
AI 주도 개발(특히 Claude Code)의 노하우나 Tips를 자주 발신하고 있습니다!
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기