같은 PR, 서로 다른 두 개의 리뷰
제 네트워크에 있는 한 개발자가 CI 코드 리뷰어로 Claude Code를 붙였습니다. “PR은 그냥 Claude한테 보게 하면 돼요”라고 하더군요. “제가 놓치는 것도 잡아 줘요.” 그래서 같은 PR을 Claude에게 두 번 돌려 보라고 했습니다.
첫 번째 리뷰는 error handling이 충분히 포괄적이라고 했습니다. 두 번째 리뷰는 불충분하다고 지적하면서 수정 세 가지를 제안했습니다. 그는 어느 Claude가 맞는지 고민하며 5분 동안 화면만 바라봤습니다.
둘 다 맞지 않았고, 둘 다 틀리지도 않았습니다. LLM은 deterministic rule을 적용한 게 아니라 probabilistic guess를 한 것입니다. 그리고 그것이 AI를 verification에 쓰는 근본적인 문제입니다. verification에는 LLM이 갖고 있지 않은 두 가지 속성이 필요합니다. Determinism. 같은 입력이면 항상 같은 출력이 나와야 합니다. Completeness. 가능한 실패 모드를 매번 빠짐없이 잡아야 합니다.
Determinism 문제
같은 코드를 Claude Code에 두 번 넣어도 서로 다른 리뷰가 나올 수 있습니다. temperature, context window, prompt phrasing이 모두 variance를 만듭니다. brainstorming에서는 variance가 장점입니다. verification에서는 버그입니다. probabilistic quality gate 위에 신뢰할 수 있는 소프트웨어를 세울 수는 없습니다.
deterministic check는 이 문제가 없습니다. tsc --noEmit은 항상 같은 오류를 냅니다. 같은 config의 ESLint는 항상 같은 이슈를 잡습니다. 이런 도구는 확률이 아니라 규칙을 적용합니다. 지루합니다. 그래서 작동합니다.
O(1) Guard Stack
우리의 pre-commit stack은 이렇게 생겼습니다. 모든 check는 deterministic입니다. 모든 check는 milliseconds 단위로 끝납니다. 이 코드가 사람이 썼는지 Claude Code가 썼는지는 아무 상관이 없습니다.
# Pre-commit (< 1 second total)
TypeScript strict mode # tsc --noEmit
ESLint boundary rules # module isolation
Import restrictions # dependency direction
dependency-cruiser # graph analysis
Unit tests # contract verification
# CI (< 30 seconds)
Full test suite
Bundle size checks
Fresh clone launch test
핵심 지표는 코드베이스 크기에 대한 O(1) 입니다. type checking은 incremental합니다. lint는 file 단위입니다. dependency-cruiser는 import graph를 선형적으로 분석합니다. Vibe-coded 앱이 커져도 속도가 크게 무너지지 않습니다. 반대로 Claude Code 리뷰는 코드베이스가 context window를 넘어설수록 느려지고 정확도도 떨어집니다.
Deterministic Check가 실제로 잡아내는 것
지난주 dependency-cruiser가 screen component가 barrel이 아니라 service implementation을 직접 import한 PR을 잡아냈습니다. PR은 compile도 됐고, test도 통과했습니다. Cursor가 import를 자동으로 생성한 것이었습니다. 하지만 우리 아키텍처 규칙을 어겼습니다. implementation은 composition root만 import할 수 있다는 규칙 말입니다.
LLM 리뷰도 이걸 잡았을 수도 있습니다. 아니면 PR이 크고 import가 파일 중간에 묻혀 있어서 놓쳤을 수도 있습니다. dependency-cruiser는 절대 놓치지 않습니다. 코드를 읽는 게 아니라 graph를 분석하기 때문입니다.
// .dependency-cruiser.cjs
// 8 lines. 100ms. Catches this every time, forever.
{
name: 'no-deep-service-imports',
comment: 'Only service barrels are public.',
severity: 'error',
from: { pathNot: '^src/services/' },
to: {
path: '^src/services/[^/]+/',
pathNot: '^src/services/[^/]+/index\.ts$',
}
}
8줄입니다. 100ms도 걸리지 않습니다. 모든 PR에서, 영원히, 같은 문제를 잡습니다. Claude Code 리뷰는 토큰이 들고, 몇 초가 걸리고, 놓칠 수도 있습니다. 계산이 비교도 안 됩니다.
AI가 실제로 있어야 할 자리
저는 AI 반대론자가 아닙니다. Cursor와 Claude Code를 매일 씁니다. 코드 초안 작성, 접근 방식 탐색, 테스트 작성, 디버깅에 아주 유용합니다. 하지만 verification 역할에는 두지 않습니다. verification은 deterministic해야 합니다. 지루해야 합니다. 매번 완전히 같아야 합니다.
guardrail은 보이지 않으면서도 피할 수 없어야 합니다. 누가 일부러 기억하지 않아도 문제를 잡아야 합니다. deterministic check가 주는 것이 바로 그것입니다. 절대 자지 않고, 지치지 않고, 코드베이스가 커져도 context를 잃지 않는 safety net입니다.
Autotomy Expo Starter Pack은 TypeScript strict mode, ESLint boundary rule, dependency-cruiser configuration, pre-commit hook을 모두 미리 설정해 둡니다. 코드 생성에는 Cursor의 속도를, 검증에는 기계 수준의 정밀함을 가져옵니다. 그렇게 해야 production을 깨뜨리지 않고 Vibe coding을 할 수 있습니다.