~/khan

핑퐁 리뷰 게이트를 실제로 이렇게 만들었다 — 두 관문, 세 라벨, '위험 0'으로만 끝내기

· 16 min read · by Khan
#Claude Code#Codex#AI 협업#멀티에이전트#코드리뷰#하네스 엔지니어링#회고

앞서 핑퐁 리뷰 루프, 몇 번이 적정선인가 라는 글에서, 저는 "핑퐁이 길어질수록 코드는 안전해지는 게 아니라 비대해진다" 는 결론에 도달했습니다. 그 글이 진단이었다면, 이 글은 처방입니다. 그 결론을 실제 하네스의 게이트 스킬 하나로 앉혀 본 이야기 — 제가 만든 핑퐁 리뷰 게이트가 실제로 어떻게 생겼는지 를 공유합니다.

핵심 아이디어부터 한 줄로. AI 혼자 리뷰하면 자기 코드에 관대하니까, 외부 모델 하나와 독립 교차검토를 시키되, 발산하지 않게 종료 조건을 객관적으로 못박는다. 이 한 줄을 어떻게 코드로 풀었는지가 이 글의 전부입니다.

1. 게이트가 사는 자리 — 관문은 두 개다

먼저 이 게이트가 작업 흐름의 어디에 끼는지부터. 저는 관문을 두 곳에 뒀습니다.

계획 수립 ──▶ ⟦관문 A: pre-implementation⟧ ──▶ [코드 작성]
                (계획을 검토, 코드 짜기 직전)
 
구현 완료 ──▶ 커버리지 검수 ──▶ ⟦관문 B: commit-direct⟧ ──▶ git add
                                  (변경을 검토, add 직전)
  • 관문 A (pre-implementation) — 계획이 나온 직후, 코드를 짜기 전에 계획 자체를 검토합니다. 여기서 잡는 건 잘못된 전제, 누락된 영향분석, 범위 폭주 같은 것들입니다. 잘못된 설계로 코드를 다 짠 뒤에 되돌리는 것보다, 계획 단계에서 막는 게 압도적으로 쌉니다.
  • 관문 B (commit-direct) — 구현이 끝나고 git add 직전에 변경된 코드를 검토합니다. 버그·보안·회귀·데이터 무결성이 대상입니다.

두 관문은 같은 검토 엔진을 공유하되 입력만 다릅니다(A는 계획 본문, B는 코드 diff). "설계 실수는 코드 전에, 코드 실수는 커밋 전에" — 실수를 가장 싸게 잡을 수 있는 두 지점에 각각 문을 단 셈입니다.

2. 세 라벨 강제 — 리뷰를 파싱 가능하게

리뷰어(외부 모델)에게는 자유 서술을 허용하지 않습니다. 딱 세 라벨의 한 줄 포맷만 강제합니다.

[위험] <파일:라인 또는 식별자> — <한 줄 근거>
[경고] <파일:라인 또는 식별자> — <한 줄 근거>
[안전] <파일 또는 영역> — <한 줄 근거>
  • [위험] — 머지 또는 구현 시작 전에 반드시 고쳐야 하는 결함 (버그·보안·회귀·데이터 무결성. 계획 관문에서는 누락된 영향분석·잘못된 전제·범위 폭주 포함)
  • [경고] — 진행은 가능하지만 개선 권고 (가독성·네이밍·마이너 리팩터)
  • [안전] — 의도대로 구성됐고 결함이 없는 부분

포맷을 강제하는 이유는 두 가지입니다. 하나는 서두·총평·마무리 멘트를 없애서 리뷰가 에세이로 번지는 걸 막는 것, 다른 하나는 기계가 종료 조건을 판정할 수 있게 만드는 것입니다. "[위험]이 0건인가?" 는 사람 눈치가 아니라 문자열 카운트로 답할 수 있어야 합니다. 이게 4절의 조기 종료로 이어집니다.

그리고 외부 모델은 read-only 로만 호출합니다. 리뷰어가 파일을 직접 고칠 수 있으면 "독립 검토" 라는 전제가 무너지기 때문입니다. 검토자는 지적만 하고, 수정은 항상 제가(구현자가) 합니다.

3. 편향을 줄이는 병렬 자체평가

외부 모델만 믿지 않습니다. 매 라운드, 저(구현한 쪽)도 같은 라벨 체계로 제 변경을 스스로 분류합니다. 자기 코드에 대한 비판도 [위험]/[경고]로 솔직히 답니다.

중요한 건 순서와 타이밍입니다.

  • 외부 모델 응답을 보기 전에 자체 평가를 씁니다 → 리뷰어 의견에 끌려가는 편향을 줄입니다.
  • 자체 평가는 diff/계획만 있으면 되므로 외부 모델 호출을 기다릴 필요가 없습니다. 그래서 둘을 병렬로 시작하고, 두 응답이 다 도착한 시점에 합의 판정으로 넘어갑니다. 직렬로 하면 의미 없는 대기 시간만 생깁니다.

즉 매 라운드는 "독립적인 두 의견을 동시에 받아 대조하는" 구조입니다.

4. '위험 0' 으로만 끝낸다 — 발산을 막는 한 줄

여기가 이 게이트의 심장입니다. 종료 조건을 이렇게 못박았습니다.

양쪽 다 [위험]이 0건일 때만 게이트가 열린다. [경고]는 몇 건이든 라운드를 늘리지 않는다.

왜 [경고]를 종료 조건에서 뺐을까요. 이전 글에서 진단한 그 발산 때문입니다. [경고]는 주관적 자문(가독성·네이밍·마이너 리팩터)이라, 재검토를 시키면 AI가 새 트집을 무한히 만들어내 0으로 수렴하지 않습니다. [경고]로 루프를 계속 열면, 진짜 이슈는 이미 없는데도 "혹시 모르니 더 막아두자" 식 방어 코드만 라운드마다 쌓입니다 — 정확히 이전 글이 경고한 병리입니다.

그래서 규칙을 이렇게 갈랐습니다.

  • [위험]만 확인용 다음 라운드를 유발한다. [위험]이 0이면 [경고]가 몇 건이든 즉시 게이트를 연다.
  • 남은 [경고]는 게이트가 열리는 라운드에 한 번만 보고하고, 사용자가 승인 단계에서 취사한다(자동 반영·재검토 없음).

이전 글에서 "모호한 종료 조건 + 지적하려는 본능 = 자연 발산" 이라고 썼는데, 그 모호함을 "[위험] 0" 이라는 객관적 카운트로 치환한 것이 이 처방의 핵심입니다. 첫 라운드부터 [위험]이 0이면 1라운드에서 끝납니다.

5. 라운드 사이 자동 적용 — 사람은 마지막에 한 번만

핑퐁 사이에 [위험]을 고칠 때는 사용자 승인 없이 즉시 적용합니다. 매 라운드마다 "이거 고쳐도 돼요?" 를 물으면 게이트의 유일한 가치인 속도가 죽기 때문입니다.

  • 변경 한 줄 요약을 채팅에 남기고 → 즉시 적용 → 코드 관문이면 영향 범위 테스트를 (여러 모듈이면 병렬로) 재실행해 green 확인 → 다음 라운드에서 그 수정을 재검토.
  • 사람 허들은 합의 후 최종 결과 1회뿐입니다.
  • 라운드 상한은 최대 3, 카운터는 외부 모델 호출 1회당 +1.

이 "승인 없이 자동 적용" 은 평소 제 원칙(파일 수정 전 승인)의 의도된 예외입니다. 그래서 이 예외는 CLAUDE.md의 예외 표에 명시적으로 등재해 두고, 그 범위를 "게이트 내부의 [위험] 해소" 로만 한정했습니다. 예외를 두되, 예외의 경계를 문서로 못박는 것 — 이게 자동화를 안전하게 쓰는 방법이라고 봅니다.

단, 자동 적용을 멈추고 사람에게 넘기는 경우도 정해 뒀습니다. 같은 컴파일/테스트 실패가 2회 반복되거나, 수정이 본래 영향 파일 목록을 벗어나거나(스코프 크리프), 외부 모델이 같은 [위험]을 같은 근거로 반복 지적하면(해소가 안 됨) — 즉시 에스컬레이션입니다.

6. 안 끝나면 사람에게 — 에스컬레이션

3라운드 안에 [위험]이 안 풀리면 강행하지 않습니다. 사람에게 결정을 넘깁니다.

3라운드 미해소 (또는 포맷 반복 실패 / 호출 실패)


  사람에게 항목별 결정 요청
        ├─ 한 건이라도 "적용" ─▶ 결정 반영 후 +1 라운드
        └─ 전부 "무시"       ─▶ 합의로 간주 ─▶ 최종 승인 ─▶ 다음 흐름

핵심 원칙은 하나입니다. 사용자가 파일을 열지 않고 채팅 본문만으로 결정할 수 있어야 한다. 카운트·라벨만 던지면 "왜 이게 [위험]이지?" 라는 되물음이 생기고, 그만큼 컨텍스트가 부풀어 오릅니다. 그래서 에스컬레이션 블록에는 핵심 근거 한 단락과 실제 코드/계획 인용을 미리 박아 둡니다.

이전 글의 결론 — "6회 이상은 금지, 사람이 들어와야 한다" — 을, "3라운드 미해소 시 최종 결정권은 항상 사람" 이라는 규칙으로 구체화한 부분입니다.

7. 그리고, 자주 안 돌린다 — 스킵 조건

핑퐁 루프 자체가 비용입니다(토큰·시간, 그리고 누적되는 과설계 위험). 그래서 "언제 도는가" 만큼 "언제 안 도는가" 를 정해 두는 게 중요합니다. 아래 중 하나라도 해당하면 한 줄 보고만 남기고 스킵합니다.

  • 변경 라인 10 이하 이면서 새 분기 0 · 시그니처 변경 0 · 외부 호출부 0 (외과적 리네이밍·상수 치환의 전형)
  • 1~2줄 단순 수정, 오타, 포맷팅
  • 문서 단독 변경, 빌드/IDE 메타파일만 변경
  • 중복 회피 — 방금 관문 A(계획)를 통과했고 구현 diff가 계획의 영향 범위 안에 머물면, 관문 B는 자동 스킵 (같은 걸 두 번 볼 이유가 없으니까). diff가 범위를 벗어나면(스코프 크리프) 정상 호출.

물론 사용자가 "핑퐁 돌려" 라고 명시하면 임계값을 무시하고 무조건 돕니다.

8. 상태기계 한 장으로

지금까지의 규칙을 한 장으로 모으면 이렇게 됩니다. 이 그림 한 장을 스킬 상단에 박아 두니, 흩어져 있던 종료·스킵·에스컬레이션 조건을 매번 재조립하지 않아도 됐습니다.

  진입: (A) 계획 검토  /  (B) 커밋 직전


  스킵 조건 충족? ──예──▶ 한 줄 보고 후 통과 (핑퐁 없음)
     │ 아니오

  ┌─▶ Round N/3 ── 외부 모델 검토  ∥  자체 평가  (병렬)
  │      │
  │      ▼  양쪽 [위험] 0?
  │      ├─ 아니오 ─▶ [위험] 자동수정(승인 없이) ─▶ 카운터 +1 ─┐
  │      └─ 예 ─▶ 게이트 OPEN ─▶ [경고] 1회 보고 ─▶ 사람 승인 ─▶ 다음 흐름
  └────────────────────(N<3)──────────────────────────────────┘
         │ N=3 미해소 / 포맷·호출 실패

  사람 에스컬레이션 ── 항목별 결정
         ├─ 1건이라도 "적용" ─▶ +1 라운드
         └─ 전부 "무시"      ─▶ 합의 간주 ─▶ 최종 승인 ─▶ 다음 흐름

마무리

이전 글과 이어서 한 줄로 정리하면 이렇습니다.

"핑퐁은 발산한다" 는 진단을, "[위험] 0 으로만 끝낸다" 는 한 줄 규칙으로 처방했다.

거창한 멀티에이전트 프레임워크가 아닙니다. 외부 모델 하나를 read-only로 부르고, 두 의견을 병렬로 받고, 종료를 객관적 카운트로 못박고, 안 풀리면 사람에게 넘기는 것 — 이 네 가지가 전부입니다. 발산을 막는 힘은 라운드를 늘리는 데 있는 게 아니라, 무엇으로 끝낼지를 주관이 아니라 숫자로 정해 두는 데 있었습니다.

스킬 파일 받기

이 게이트의 실제 스킬 파일을 그대로 내려받아 각자 환경에 맞게 고쳐 쓰실 수 있습니다.

codex-review-pingpong.SKILL.md 내려받기

참고