meta
두 번째 AI를 어떻게 부르고 언제 멈추나 — headless adversarial 코드리뷰 루프
· Ascendy Engineering
TL;DR
- 구현 에이전트와 리뷰 에이전트(다른 모델)를 페어로 굴린다. 기존엔 리뷰어를 별도 화면 세션으로 띄워 사람이 프롬프트를 복붙하고 sentinel 파일로 결과를 받는 수동 중계였다 — 가장 큰 마찰.
- 리뷰어를 화면 제어가 아니라 headless subprocess로 바꿨다. 프롬프트는 stdin, 마지막 줄에
REVIEW_DECISION: APPROVED|CHANGES_REQUESTED+ 발견 항목을 ID로 강제해 stdout을 기계 파싱한다. 화면 파싱·별도 세션·sentinel 중계가 통째로 사라졌다. - adversarial 리뷰는 수렴하지만 점근적이다 — 방치하면 끝없이 더 깊은 명세를 요구한다. 멈춤선은 사람이 긋고 리뷰어가 그 선의 정당성을 판정하게 하자, 설계 문서 리뷰가 9 → 7 → 3 → APPROVED로 깨끗이 끝났다.
- 그 리뷰가 잡은 결함들(계층 경계 위반, 비원자적 레이트리밋, 외부 API 실패를 성공으로 삼킴, 시크릿이 에러 로그로 샐 뻔한 것, “큰따옴표만 검사하는” 가드 허점)은 사람 리뷰가 놓치기 쉬운 종류였다.
소스 노트. 이 글은 백엔드 팀의 인테이크(
docs/intake/from-backend/2026-05-31-headless-adversarial-review-loop.md)를 정제한 것이다. 사내 하니스·에이전트 제품명·CLI 이름·모듈 경로 등 내부 식별자는 일반화했다. 멀티 AI를 붙였더니 느려졌다가 진화 서사라면, 이 글은 그 “그래서 실제로 돌려보니” 데이터편이다.
진짜 질문은 “리뷰를 시키느냐”가 아니었다
구현 에이전트(코드를 쓰는 쪽)와 리뷰 에이전트(다른 모델)를 페어로 굴리는 하니스가 있다. AI 코드리뷰를 자동화한다고 하면 보통 “두 번째 모델한테 리뷰를 시킨다”를 떠올린다. 그런데 실제로 발목을 잡은 건 시키느냐가 아니라 어떻게 부르느냐, 그리고 언제 멈추느냐였다.
기존 구조는 리뷰어를 별도 화면 세션으로 띄우는 것이었다. 사람이 프롬프트를 복붙하고, 리뷰어가 화면에 답을 뱉으면 그걸 다시 읽어 sentinel 파일로 결과를 받았다. 이 수동 중계가 가장 큰 마찰이었고, 화면을 읽는 방식은 본질적으로 취약했다 — ghost 텍스트, 타이밍, 포커스 인시던트.
화면 제어를 버리고 subprocess로
리뷰어 CLI가 headless(non-interactive) 모드를 지원하는 걸 확인하고, 리뷰를 화면 제어가 아니라 subprocess 호출로 바꿨다.
- 프롬프트는 stdin으로 준다.
- 출력 형식을 강제한다 — 마지막 줄에
REVIEW_DECISION: APPROVED|CHANGES_REQUESTED, 그리고 발견 항목을 ID로. - stdout을 기계 파싱한다.
이 한 번의 전환으로 화면 파싱, 별도 세션, sentinel 중계가 통째로 사라졌다. 그리고 부수적으로 경계 하나가 분명해졌다 — 협업 상대(다른 레포에 살아있는, 맥락을 쌓아가는 에이전트)와 리뷰 함수(같은 작업을 검토하는 stateless 호출)는 다른 것이다. 전자는 살아있는 세션이어야 하지만, 후자는 그냥 함수처럼 불렀다 버리면 된다. headless로 보내야 하는 건 후자뿐이다.
adversarial 리뷰는 수렴한다 — 단, 멈춤선이 있어야
이 루프로 이번 변경들을 실제로 리뷰했다. 그중 설계 문서 하나를 4라운드 돌린 결과가 인상적이었다.
| 라운드 | CHANGES_REQUESTED 항목 |
|---|---|
| 1 | 9 |
| 2 | 7 |
| 3 | 3 |
| 4 | 0 — APPROVED |
항목 수도, 토큰도 단조 감소했다. 핵심은 3라운드였다. 리뷰어가 “이건 설계 문서이고, 필드 단위 명세는 구현 PR에서 정한다”는 사람이 그은 경계를 명시적으로 받아들이고, 더 깊은 명세를 재요구하지 않았다.
이게 수렴의 열쇠다. adversarial 리뷰는 방치하면 끝없이 더 깊은 디테일을 요구한다 — 점근적으로 수렴하긴 하지만 영영 끝나지 않는다. “ideation은 방향, 구현 PR은 명세”라는 멈춤선을 사람이 긋고, 리뷰어가 그 선의 정당성을 판정하게 하자 4라운드에서 깨끗이 끝났다. 리뷰어에게 “멈춰”라고 명령하는 게 아니라, “여기까지가 이 문서의 책임 범위인데 동의하나?”를 묻는 것이다.
그리고 진짜 결함을 잡았다
한 PR(신규 API 엔드포인트)을 “단순하니까”라며 리뷰 게이트 없이 올렸다. headless 리뷰가 단계적으로 잡아낸 것:
- 계층 경계 위반 — 라우터가 인프라 클라이언트를 직접 호출
- 비원자적 레이트리밋 — 크래시 시 영구 차단 가능
- 외부 API의 4xx/5xx를 성공으로 삼킴
- 시크릿이 에러 로그로 새어나갈 뻔한 것 (발견 즉시 같은 PR에서 수정)
다른 PR(리팩터)에선 더 흥미로운 게 나왔다. 내가 추가해둔 “하드코딩 금지” 가드 테스트가 큰따옴표만 검사하는 허점이 있었는데(작은따옴표면 통과), 리뷰어가 그걸 직접 재현해서 코드로 보여줬다. 그래서 가드를 AST 기반으로 고쳤다. 지적을 말로만 하는 게 아니라 증명하는 행동의 가치다.
이 결함들의 공통점은 “단순해 보이는 변경”일수록 리뷰 게이트를 건너뛴 대가가 크다는 것이다 — 그리고 계층 위반·시크릿 로그 노출·quote-only 정적검사 허점은 모두 사람 리뷰가 놓치기 쉬운 종류였다.
가져갈 것
- 두 번째 AI는 화면 제어가 아니라 headless subprocess + 구조화 출력(
REVIEW_DECISION/finding-id)으로 불러라. 결과를 stdout/파일로 받는 게 화면 파싱보다 압도적으로 견고하다. - “협업 상대”와 “리뷰 함수”를 구분하라. 맥락을 쌓는 살아있는 에이전트와, 한 번 불렀다 버리는 stateless 검토는 다르다. headless로 보낼 건 후자뿐이다.
- adversarial 리뷰엔 사람이 멈춤선을 긋고, 그 선의 정당성을 리뷰어에게 물어라. “ideation vs 구현 명세” 같은 경계가 무한 정밀화를 끊는다.
- “단순해 보여도” 경계를 넘는 변경(새 API·다중 모듈 리팩터)은 리뷰 게이트를 건너뛰지 마라. 자동 리뷰는 사람이 놓치는 결함을 데이터로 잡아낸다.
저작·인용: 이 글은 Ascendy Engineering이 작성했으며 출처 표기 시 재인용 가능합니다. 잘못된 정보를 발견하면 GitHub 이슈로 알려주세요.
Tags: ai-agents, code-review, automation, dogfooding, developer-tooling, convergence