leo.dev
infra

AI가 짠 코드를 믿을 수 있게 만드는 하네스

— SERIES AI 코딩 하네스 Part 01 / 03
  1. Part 01 AI가 짠 코드를 믿을 수 있게 만드는 하네스
  2. Part 02 AI가 한 작업을 스스로 기록하게 만들기
  3. Part 03 죽은 검증 훅과 AI 하네스 드리프트

AI(Claude Code)에게 기능 하나를 맡기면 5분 만에 “다 됐습니다”가 돌아온다. 그런데 그 5분짜리 결과를 검증하는 데는 30분이 걸리고, 30분짜리 작업을 맡기면 돌아온 코드를 읽고 검증하는 데 몇 시간이 든다. 생성은 빨라지는데 검증은 그만큼 빨라지지 않는다. 게다가 그 코드엔 존재하지 않는 메서드, N+1 쿼리, 빠진 권한 검사가 섞여 있곤 한다. 매번 전부 정독하면 AI를 쓴 속도가 사라지고, 안 보면 깨진 게 그대로 들어간다. 이 딜레마는 사람이 더 부지런해지는 걸로는 안 풀린다. 검증을 사람의 눈이 아니라 시스템 쪽으로 옮겨야 했고, 그러려면 먼저 “AI가 무엇을 못하는가”부터 알아야 했다.

AI 출력은 검증 전 초안이다

관찰한 실패 패턴이 다섯 가지였다.

한계증상
할루시네이션존재하지 않는 API·메서드·타입을 자신 있게 생성
성능 무지Promise.all + 개별 조회 같은 N+1을 기본값처럼 생성
보안 누락Guard 없는 엔드포인트, companyId 필터 빠진 쿼리
컨텍스트 붕괴파일이 커지면 앞부분을 못 보고 다른 패턴으로 새 코드
자기 오보고”동작한다”고 보고하지만 tsc 오류가 남아 있음

가장 위험한 건 마지막이다. AI는 자기가 끝냈다고 믿는다. 그러니 AI의 출력은 완성품이 아니라 검증 전 초안으로 취급하고, 그 검증을 사람의 정독이 아니라 시스템으로 옮겨야 한다.

결정론은 훅, 판단은 LLM

검증을 둘로 가른다. 기계적으로 판정 가능한 것(tsc·eslint)은 결정론적 셸 훅으로, 의미와 맥락 판단(누락된 로직, 엣지케이스)은 LLM으로. 이 분리 위에 네 레이어를 쌓았다.

Layer 0 CLAUDE.md 컨벤션 매 세션 자동 로드 (컨텍스트 주입)
Layer 1 Hook Write/Edit 시 자동 (결정론 게이트)
Layer 2 Skill 작업 종류별 호출 (반복 패턴 고정)
Layer 3 self-healing-loop 구현 완료 후 호출 (판단형 검증 루프)

Layer 0은 규칙을 매 세션 다시 주입한다. AI는 세션마다 초기화되니, 컨벤션이 문서로 없으면 매번 다른 패턴을 짠다. Layer 1은 파일이 쓰일 때 eslint --fixtsc --noEmit을 자동으로 돌린다. Layer 2는 작업 종류별 스킬(모듈 생성, 기능 추가, 마이그레이션)로 골격을 고정해 AI가 매번 다른 구조를 만들지 않게 한다. Layer 3은 구현이 끝난 뒤 도는 검증 루프로, 종료 조건이 다 충족될 때까지 반복한다. tsc·eslint 0 오류, self-review, 서브에이전트 독립 리뷰, 커밋까지.

훅이 모델을 깨운다

Layer 1에서 한 가지가 비자명했다. tsc 훅이 오류를 발견했을 때 단순히 실패로 끝내지 않는다. exit 2로 모델을 다시 깨우고 오류 컨텍스트를 주입한다. AI가 “끝났다”며 멈추려 해도, 타입 오류가 남아 있으면 강제로 다시 작업하게 되는 구조다. 결정론적 게이트가 LLM을 되돌려 세우는 지점이라, 자기 오보고를 기계가 막는다.

”검사 0개”를 통과로 오인하지 않는다

검증에서 가장 무서운 건 빨간불이 아니라, 통과처럼 보이지만 실제로는 아무것도 검사하지 않은 경우다. 몇 개를 직접 밟았다. pnpm --filter backend tsc는 실제 패키지명과 안 맞아 0개 검사 후 통과하고, 루트 tsc --noEmit은 project references라 no-op이고, 프론트는 -p tsconfig.app.json을 안 주면 엉뚱한 걸 본다. 이 false PASS들을 검증 루프에 명시해, “검사 0개”를 “통과”로 읽지 않게 못 박았다. baseline은 믿지 말고 실측한다는 규칙이 여기서 나왔다.

버그를 규칙으로 박제한다

마지막은 자기수정이다. 버그를 고친 뒤 두 질문을 던진다. 기존 규칙을 따랐으면 막혔을까. 다른 도메인에서도 재발하나. 둘 다 yes면 CLAUDE.md나 스킬을 강화하고 같은 커밋에 포함한다. 그렇게 규칙이 조금씩 늘었다.

AI가 한 실수박제한 규칙
Promise.all + 개별 조회(N+1)Promise.all은 N+1을 해결하지 않는다, leftJoinAndSelect
migration:generate 실행 시도마이그레이션 수동 작성, DROP 금지
타입 모를 때 any 도배any 금지, 제네릭·교차타입
Guard 없는 엔드포인트3단계 Guard 필수, companyId 바인딩

같은 실수가 다음 세션에서 자동으로 교정된다. 버그를 고치는 데서 끝내면 같은 버그를 또 만나지만, 규칙으로 박으면 그 패턴이 닫힌다.

그래서

결국 한 일은 단순하다. AI 출력을 검증 전 초안으로 보고, 기계로 판정할 건 훅에 판단이 필요한 건 LLM에 맡기고, 검증을 통과한 것만 커밋하고, 버그는 고치는 데서 멈추지 않고 규칙으로 박았다. 거창한 설계라기보다, 같은 실수를 두 번 만나기 싫어서 하나씩 끼운 장치들이다.

↑↓ 이동 열기esc 닫기