TL;DR
- 타입 검사(Type checking), 린팅(lint), 테스트가 통과하더라도 남아있는 '경계를 넘나드는 의미적 불일치'를 찾는 리뷰를 Claude Code의 스킬과 서브 에이전트만으로 구축했습니다.
- 작성자 본인의 세션에는 판정권을 주지 않았습니다. 불변 조건(Invariant condition)을 추출하는 역할과 검증하는 역할을 별도의 서브 에이전트로 분리했으며, 어느 쪽에도 설계 의도나 대화의 문맥은 전달하지 않았습니다.
- 추출 과정은 관점별로 4개의 렌즈에 나누어 병렬로 실행했고, push를 막을 정도의 지적만 3체에게 각기 다른 관점에서 반증하게 했으며, 단 한 곳이라도 반증하면 등급을 낮추었습니다(하향 조정).
- 약 2개월 반 동안 총 97회 구동했으며, 그중 절반가량(49회)에서 block에 해당하는 지적을 도출했습니다.
- 다만 동일 모델을 사용했기 때문에, 별도의 모델(Codex)만큼의 독립성은 없습니다. 같은 HEAD를 모두 본 11건 중에서는 판정이 일치한 것이 단 2건이었습니다.
이전 글과의 관계
이전 글에서는 Claude가 작성한 코드를 push하기 전에 Codex에게 심사시키는 hook을 소개했습니다. 그 과정에서 Codex를 사용할 수 없을 때의 대안으로 'Claude 측의 다른 리뷰'에 간략히 언급했었습니다. 이 글은 그 내용에 대한 이야기입니다.
Codex를 사용하지 않는 사람이라도 Claude Code만으로 동일한 방식의 관문(Gate)을 만들 수 있습니다. 다만, 무엇이 같고 무엇이 다른지는 마지막에 숫자로 살펴보겠습니다.
경계 통합성이란 무엇인가
여기서 말하는 경계 통합성(Cross-Boundary Consistency)이란, 계층이나 경계를 넘나들 때 양쪽에서 의미가 충돌하지 않는지를 의미합니다. 타입 검사도 린팅도 테스트도 통과함에도 불구하고 깨지는 종류의 버그는 대부분 여기에 있습니다.
예를 들어 다음과 같은 경우(설명용 예시)가 있습니다.
추출은 정의 측과 소비 측을 나열하는 기계적인 작업이므로, 모델은 가벼운 것(Sonnet)으로 충분하다. 판정의 정확도는 다음 검증 단계에서 보장하도록 설계했으며, 검증 이후에는 모델을 낮추지 않는다.
검증: 추출 결과를 맹신하지 않는 또 다른 주체
추출 결과는 추출과는 별도의 서브 에이전트에게 전달하여 검증시킨다. 여기서의 지시는 "추출 측의 지적을 맹신하지 말고, 누락과 오탐지 양쪽 모두를 의심하며 독립적으로 재검증해 주세요. 추출 측이 언급하지 않은 불일치도 스스로 찾아주세요."
심각도는 이전 글의 Codex와 동일한 기준으로 판정시킨다. 도달하는 경로가 현재 운영에 없는 이론상의 문제는 "제안", 도달할 수 있더라도 빈도가 낮아 재조작으로 회복되는 것은 "경미". 각 지적 사항에는 상황, 빈도, 복구 가능성을 작성하게 하고, 작성할 수 없는 것은 기록만 남긴다.
필수 증거
눈으로 읽었거나 기존 테스트가 통과했다는 것만으로는 완료하지 않는 변경도 있다. 예를 들어 상태 전이(state transition)를 변경했다면, 다음 표를 만들게 한다.
|
step | 입력state | command/event | 출력state | 지켜야 할 불변 조건 |
필드나 event 페이로드(payload)를 추가했다면, 어디를 거쳐서 어디에서 소비되는지에 대한 대응표를 만들게 한다.
|
값・필드 | 정의・생성원 | event/state | 영속화 | consumer・projection・API | 유지 방침 |
표를 채우려고 하면, "이 필드를 projection의 어디에서 사용하고 있는지"가 적을 수 없다는 형태로 누락(gap)이 발견된다.
재검증: block하는 지적만 3체로 확인하기
검증은 1체의 1패스이기 때문에, 그 개체가 놓치거나 오탐지한 것이 그대로 block에 파급된다. 그래서 정리 담당자가 "반드시 수정" 수준이라고 판단한 지적만을 추가로 3체의 서브 에이전트에게 병렬로 검증시킨다. 3체에는 각기 다른 관점을 하나씩 할당한다.
- 정확성: 그 불변 조건 위반이 정말 성립하는가
- 재현성: 어떤 구체적인 입력/상태에서 실해가 발생하는지, 그 경로에 실제로 도달할 수 있는지
- 사양 적합성(Specification Compliance): 사양의 근거 라인을 다시 읽어보고, 그 동작이 정말 오류라고 말할 수 있는지
지시는 "이 지적은 오탐지라는 전제하에 반증을 시도해 주세요. 반증할 수 없는 경우에만 지지한다고 답해 주세요." 추출 때와는 역방향의 적대적 프레이밍(adversarial framing)이 되어 있다.
3체 모두가 지지했을 때만 "반드시 수정" 상태로 채택하고, 1체라도 반증하면 "확인 필요(要確認)"로 하향 조정한다. 하향 조정된 때는 이유와 반증 내용을 보고서에 남기고, 조용히 폐기하지 않는다. "확인 필요" 이하의 지적은 block과 관련이 없으므로 재검증 대상에 포함시키지 않는다.
다수결로 했다가, Codex에 제지당하다
사실 이 규칙은 이 글을 작성하는 도중에 수정한 것이다. 원래는 "2체 이상이 지지하면 채택"이라는 다수결 방식이었다.
이 글을 push(게시)하려 했을 때, 이전 글의 hook에서 Codex가 기사를 block했다. 지적의 핵심은 다음과 같다. 정확성, 재현성, 사양 적합성은 같은 질문에 대한 3표가 아니다. "반드시 수정"이 성립하기 위해서는 세 가지 모두를 충족해야 하는 별개의 조건이다. 다수결로 하면, 재현성 담당자가 "그 경로에는 실제로는 도달하지 않는다"고 반증하더라도, 나머지 2체의 지지로 채택되어 버린다. 심각도 기준으로 볼 때, 도달하지 않는 문제는 "제안"이어야 하므로, 이론상의 문제 때문에 push가 멈추게 된다.
지적이 옳았기 때문에, skill을 "1체라도 반증하면 하향 조정"으로 수정한 후 이 글도 다시 썼다. 하향 조정된 지적도 "확인 필요"로 보고서에 남으므로, 실제 결함이 1체가 실수로 반증하더라도 사라지는 일은 없다. Claude만으로 구성한 시스템의 허점을 다른 모델이 기사 문서에서 발견하게 된 형태가 되었다.
기록: Codex와 동일한 스키마로 남기기
결과는 Codex의 리뷰 로그와 동일한 스키마의 JSONL에 기록한다. verdict는 인수로 받지 않고, findings에 "치명적(致命的)" 또는 "중요"가 있는지 여부에 따라 자동으로 결정된다.
verdict=$(printf '%s' "$findings_json" | jq -r '
if any(.[]; .severity == "致命的" or .severity == "重要") then "block" else "pass" end
')
verdict를 호출하는 쪽에서 전달하는 구조라면, "block 지정인데 경미한 지적만 있다"와 같은 모순된 기록을 남길 수 있게 된다. 그렇게 되면, Codex와의 block률 비교가 탐지력(detection capability)이 아니라 판정 기준의 차이를 측정하게 된다.
최소 구성
실제 스킬은 약 100줄 정도 되지만, 골격은 다음과 같습니다.
~/.claude/skills/spec-consistency-review/SKILL.md
---
name: spec-consistency-review
description: 차이점의 경계를 가로지르는 의미적 불일치(값 집합, 필드 역방향 처리, 비정상 상태, 결합성)를 검사한다. push 전이나 PR 전에 사용하는 것이 좋다.
...
검사 항목의 내용(위 6개 카테고리의 상세 내용)은 별도의 파일에 두고, 각 서브 에이전트에는 담당 렌즈 분량만 읽게 했습니다.
2개월 반 동안 97회 구동한 결과
집계 대상은 2026년 8월 10일부터 10월 28일까지 기록된 셀프 리뷰 97회(훅 동작 확인용 기록 제외)입니다. 이 기간의 재검증은 수정 전 다수결 규칙으로 작동했습니다.
| 항목 | 값 |
|---|
| 리뷰 횟수 | 97 |
| ... | |
| 이전 기사의 훅은 운영 초기 며칠을 제외하고는, 이 셀프 리뷰가 Codex가 블록(block)했을 때 또는 Codex를 사용할 수 없을 때만 실행되었습니다. 따라서 Codex의 기록과 비교할 수 있는 것은 같은 HEAD를 양쪽 모두 판정했던 11건뿐이었습니다. | |
| 셀프 리뷰 \ Codex | block | pass |
|---|
| block | 1 | 4 |
| pass | 5 | 1 |
판정이 일치한 건수는 11건 중 2건에 불과했습니다. 건수가 적고, Codex가 블록했을 때 셀프 리뷰가 실행된다는 편향성도 있기 때문에, 여기서 정확도의 우열을 논하기는 어렵습니다. 확실하게 말할 수 있는 것은, 같은 차이점을 보더라도 Claude와 Codex는 서로 다른 것을 포착하고 있다는 점입니다. 셀프 리뷰만 블록한 경우가 4건, Codex만 블록한 경우가 5건 있었습니다.
어떤 불일치를 포착했나
실제로 블록에 해당한다고 포착된 것들을 렌즈별로 나열했습니다. 소재가 노출되지 않도록 의역한 내용도 포함합니다.
| 렌즈 | 포착된 불일치 |
|---|
| A 값 집합/타입 | 연습 기록 앱에서 리마인더 알림이 명시하는 구절과, 앱을 열었을 때 나오는 '오늘의 한 편' 선정 기준이 정반대였습니다. |
| ... | |
| 이전 기사의 훅 자체도 이 리뷰를 거쳤습니다. 예를 들어, '지난번 지적 사항을 가져올 때 건수를 10건으로 자르기 때문에, 새롭게 발생한 치명적인 지적이 영구적으로 손실될 수 있다'는 불일치를 포착했습니다. | |
모두 차이점 중 한 곳만 보면 올바르게 보입니다. 이벤트 발행 측과 구독 측, 컬럼 추가 측과 기존 데이터, 마이그레이션 작성 측과 적용 측처럼 경계 양쪽을 나란히 놓았을 때 비로소 불일치가 보이는 것입니다.
한계점
- 같은 모델의 습관은 사라지지 않는다. 서브 에이전트로 분리해도, 전제 설정 방식이나 간과하는 경향은 공유됩니다. 역할을 나누는 것으로 바이어스는 줄일 수 있지만, 다른 모델을 배치할 정도의 독립성은 없습니다.
- 토큰 소모가 크다. 렌즈 4개가 모두 구동되고 재검증까지 걸리면, 한 번의 리뷰에 최대 8개의 서브 에이전트가 작동합니다. 매번 push 할 때 돌리기는 무겁기 때문에, 이전 기사의 훅에서는 Codex를 사용할 수 없을 때의 대체재와 Codex가 블록했을 때의 추가 확인에 국한했습니다.
- 사양이 없으면 취약하다. 불변 조건의 근거가 되는 설계서나 ADR(Architecture Decision Record)이 없는 리포지토리에서는, 코드에서 읽어낼 수 있는 구조적인 일관성만 확인할 수 있습니다. 그 경우에는 '사양 미발견'임을 명시하도록 했습니다.
요약
- 타입 검사/lint/테스트로는 잡히지 않는 '경계를 가로지르는 불일치'는 생산 측과 소비 측을 대조하는 전용 리뷰로 찾아야 한다.
- 작성자 본인에게 판정하게 하지 않고, 추출과 검증을 독립된 서브 에이전트로 분리하며 의도나 문맥은 전달하지 않는다.
- '문제가 있다는 전제 하에 찾으라', '오탐지라는 전제 하에 반증하라'와 같이 단계별로 적대적인 방향을 바꾼다.
- Codex가 없다면 이것이 관문의 대체재가 된다. 다만 같은 모델이기 때문에, 사용할 수 있다면 다른 모델과 조합하는 것이 좋다.
Discussion