내 코드를 지키는 16명의 AI 리뷰어가 있었지만, 그들 중 누구도 존재하지 않았다
요약
Claude Code를 사용하여 에이전트 기반 워크플로를 구축할 때 발생하는 '조용한 실패(fail open)' 사례를 다룹니다. 잘못된 파일 경로, 누락된 프런트매터, 오타가 포함된 모델 ID 등이 오류 없이 작동하는 것처럼 보이는 위험성을 경고합니다.
핵심 포인트
- Claude Code는 에이전트를 찾을 때 상위 디렉토리로만 탐색함
- 에이전트 파일에 필수 프런트매터(name, description)가 없으면 로드되지 않음
- 잘못된 모델 ID 사용 시 오류 없이 다른 모델로 폴백되어 의도치 않은 동작 발생
- 에이전트 설명이 동일하면 AI가 에이전트를 선택할 때 무작위성이 발생함
- 메모리 부족 등으로 프로세스가 강제 종료(Killed)될 경우 테스트 통과로 오인될 수 있음
내 프로젝트 지침에는 몇 주 동안 다음과 같이 명시되어 있었습니다:
마이그레이션 (Migrations)은 자동 호출되는 migration-reviewer 서브 에이전트 (subagent)에 의해 강제됩니다.
하지만 그렇지 않았습니다. migration-reviewer는 없었습니다. migration-reviewer.md라는 파일과 그와 유사한 15개의 파일이 한 단계 너무 깊은 폴더 안에 놓여 있을 뿐이었습니다.
상황을 더 악화시키는 부분은 이겁니다: Claude Code가 그 파일들을 거기에 두라고 말했습니다. 에이전트 파일이 어디에 위치해야 하는지 물었을 때, 그것은 저장소 (repo) 내부라고 답했고, 저는 그것들을 옮겨 놓았습니다. 몇 주 후, 다른 세션에서는 그것들이 외부에 있어야 한다고 말했습니다. 왜 그것들이 한 번도 트리거 (trigger) 되지 않았는지 전혀 알 수 없었습니다. 그저 작동하지 않는다는 것만 알 뿐이었습니다. 알고 보니 Claude Code는 실행한 위치에서 위로 올라가며 에이전트를 찾습니다. 아래로 내려가며 찾지는 않습니다. 그래서 그것들을 절대 찾지 못했던 것입니다.
16명의 AI 리뷰어. 그중 단 하나도 실행된 적이 없습니다. 오류도, 경고도, 로그 라인 (log line)도 없었습니다. 그저 몇 주간의 정적뿐이었습니다.
이 포스트 전체가 다루고자 하는 버그 유형은 바로 이것이며, 여러분을 두렵게 만들어야 할 유형입니다: 실패 시 열린 상태로 남는 것 (fail open). 충돌 (crash)은 소란스럽습니다. 실패하는 테스트는 소란스럽습니다. 하지만 조용히 존재하지 않는 안전망은 마치 제대로 작동하고 있는 안전망처럼 보입니다.
저는 직접 찾아보았기 때문에 겨우 알아낼 수 있었습니다.
자세히 들여다볼수록 상황은 더 나빠집니다
실제로 그 에이전트 파일들을 읽어보니, 가장 최근의 4개 파일은 각각 저마다의 재앙을 품고 있었습니다:
reviewer.md에는 프런트매터 (frontmatter)가 전혀 없었습니다. 그것은 에이전트 폴더에 저장된 워크플로 산문(workflow prose) — 즉, 복사해서 붙여넣은 메모 — 에 불과했습니다. 에이전트 파일이 로드 (load) 되려면 name과 description이 필요합니다. 이 파일에는 둘 다 없었습니다. 결코 로드될 수 없었습니다.
그리고 내 지침은 AI에게 "APPROVED를 반환할 때까지 리뷰어 서브 에이전트를 호출하라"고 말하고 있었습니다. 나의 프리 커밋 (pre-commit) 리뷰 게이트 (gate)는 아무것도 참조하지 않는 상태였습니다. 문 뒤에 방이 없는 문과 같았습니다.
model: claude-opus-4.8 — 실제 ID는 claude-opus-4-8입니다. 하이픈 대신 점이 찍혀 있었습니다. 잘못된 모델 ID에 대해서는 아무런 오류도 발생하지 않습니다. 그저 조용히 다른 것으로 폴백 (fallback) 될 뿐입니다.
두 에이전트의 설명(description)이 바이트 단위로 동일했습니다. AI는 에이전트의 설명을 읽고 에이전트를 선택합니다. 두 설명이 동일하다는 것은 선택할 방법이 없다는 뜻입니다. 그것은 실험실 가운을 입은 동전 던지기와 다를 바 없습니다.
이 모든 사례는 조용히 실패(fail silently)합니다. 그것이 패턴입니다. 그것이 이 글의 핵심입니다.
"모든 테스트 통과"
여기 실제로 당신에게 비용을 발생시키는 사례가 있습니다.
당신의 AI가 "모든 테스트 통과"라고 말합니다. 합리적이죠! npm test를 실행했으니까요. 거짓말을 하는 게 아닙니다.
단, 다음과 같은 경우를 제외하면 말이죠:
테스트가 강제 종료되었습니다. 메모리 부족(Out of memory) 때문입니다. 프로세스가 0이 아닌 종료 코드(non-zero exit code)와 함께 종료되며 Killed라는 단어를 출력했습니다. FAILED도 아니고, 3 failing도 아닌, 그저 Killed였습니다. 검사기(checker)는 출력 텍스트에서 실패를 나타내는 단어를 스캔하다가 아무것도 찾지 못했고, 이를 통과(green)로 간주했습니다. 실제적이고 모호하지 않으며 기계가 읽을 수 있는 '이 작업이 성공했는가'에 대한 신호인 종료 코드는 바로 그 트랜스크립트(transcript)에 있었지만, 아무도 그것을 읽지 않았습니다.
통과 결과가 오래된 것이었습니다. 테스트가 실행되었습니다. 통과했습니다. 그 후 AI가 소스 파일을 수정했습니다. 그 후에야 AI는 "모든 테스트 통과"라고 말했습니다. 기술적으로는 사실이지만, 완전히 가치가 없습니다. 그 통과(green) 결과는 더 이상 존재하지 않는 코드를 가리키고 있기 때문입니다.
통과 결과가 파일 하나에 불과했습니다. npm test -- --grep billing은 테스트 스위트(suite)의 한 부분만 실행하고 통과합니다. 이는 히스토리에 "테스트 실행됨"으로 나타납니다. 주장: "모든 테스트 통과". 현실: 당신은 코드의 4%만 테스트했습니다.
이 중 어느 것도 AI가 거짓말을 하는 것이 아닙니다. 모든 사례는 AI가 당신이 생각하는 의미와는 다른 것에 대해 진실을 말하고 있는 것입니다.
매일 아침 쓰레기를 배송한 이메일
이 일은 제가 교사들을 위해 만들고 있는 교실용 지리 및 역사 게임인 EraPin 개발 중에 발생했습니다.
잘못된 파일 쓰기 한 번이 소스 파일을 재인코딩했습니다. 파일을 한 가지 텍스트 형식으로 읽고, 다른 형식으로 저장한 것입니다. 756개의 문자가 조용히 망가졌습니다. 그 파일에 있던 모든 화살표, 모든 체크표시, 모든 대시가 의미 없는 글자(gibberish)로 변했습니다.
아무도 이를 잡아내지 못했습니다. 테스트도 아닙니다. 테스트는 이메일을 읽지 않으니까요. 린터(linter)도 아닙니다. 유효한 코드였기 때문입니다. 코드 리뷰도 아닙니다. 374줄의 디프(diff)를 훑어보다 보면 눈이 그냥 지나쳐 버리기 때문입니다.
진짜 문제는 이것입니다: 변경된 374줄 중 365줄에 오염이 포함되어 있었고, 이전 커밋은 완전히 깨끗했습니다. 커밋 시점에 실행되는 단순한 30줄짜리 체크 로직만 있었어도 즉시 잡아낼 수 있었을 것입니다. 아무도 그런 것을 작성하지 않았습니다. 누가 인코딩 체크 도구를 작성하겠습니까? 바로 이런 일을 겪고 난 직후에나 작성하겠죠.
아무것도 바꾸지 못한 수정
가장 인간적이라 제가 가장 좋아하는 사례입니다.
저는 몇몇 이메일의 환경 레이블(environment label)을 "수정"했습니다. 제목 줄에 접두사(prefix)를 붙였죠. 완료된 것처럼 보였습니다. 디프(diff) 상으로는 완료된 상태였습니다.
하지만 그 두 이메일 함수는 절대 호출되지 않습니다. 스케줄된 작업(scheduled job)은 해당 함수들을 완전히 건너뛰고 대신 자체적인 요약본을 보내는 플래그(flag)와 함께 실행됩니다. 저의 수정 사항은 실행되지 않는 코드에 영향을 미쳤습니다. 그대로 배포되었을 것이고, 완벽해 보였겠지만, 실제로 당신이 받는 이메일에는 아무런 변화도 주지 않았을 것입니다.
디프(diff)는 이를 볼 수 없습니다. 스텁(stub)도 없고, TODO도 없으며, 누락된 파일도 없습니다. 모든 줄은 실제 코드입니다. 단지 데드 코드(dead code)일 뿐입니다.
그리고 제가 한동안 잘못 생각했던 점이 있습니다. 이런 문제를 잡아내려면 AI가 필요하다고 가정했습니다. 그렇지 않습니다. 커버리지 실행(coverage run)이 필요할 뿐입니다. "내가 변경한 줄이 실행했을 때 실제로 실행되었는가?"라는 질문은 지루하고 결정론적(deterministic)이며, AI가 전혀 필요 없는 답변을 가지고 있습니다. 그냥 실행하고, 카운터를 읽으면 됩니다. 제가 변경한 줄들은 한 번도 실행되지 않은 것으로 나타났을 것입니다.
이러한 관점의 전환은 이 포스트에 등장하는 그 어떤 단일 버그보다 더 중요합니다.
이제 불편한 부분입니다
저는 바로 이런 것들—거짓된 "완료", 소리 없는 무작정 실행(no-op), 가짜 성공(fake green)—을 잡아내는 도구인 Groundtruth를 만듭니다. 결정론적이며, 루프 안에 AI가 없습니다.
이번 주, 적대적 리뷰(adversarial review)를 통해 이 도구에서 13개의 실제 버그를 발견했습니다. 단 하나도 빠짐없이 이미 저의 테스트 스위트(test suite)와 수동 점검을 통과했던 것들이었습니다.
주요 사례들은 다음과 같으며, 이는 저를 겸허하게 만듭니다:
- "실제로 아무것도 테스트하지 않는 테스트"를 잡아내기 위해 제가 작성한 체크(check)는 첫 번째 버전에서 네 번의 거짓 양성 (false positive)을 발생시켰습니다. 실제로 작동하는 테스트들을 오류로 표시한 것입니다.
- 거짓 양성을 수정했더니 이번에는 거짓 음성 (false negative)이 발생했습니다. 즉, 도구가 실제 문제를 발견하지 못하고 조용히 넘어간 것입니다. 저는 같은 장소에서 같은 방식으로 이 실수를 세 번이나 반복했습니다.
- 거짓 양성을 수정하기 위해 분류기 (classifier)를 작성했지만, 그 분류기는 자신이 수정하려 했던 바로 그 거짓 양성을 다시 발생시켰습니다.
- "로드할 수 없는 에이전트 (agents)"를 찾아내는 체크는 커밋되지 않은 파일 (uncommitted files)을 인식하지 못했습니다. 이는 제가 새로 만든 고장 난 네 개의 에이전트를 모두 놓쳤을 것임을 의미합니다. 고장 난 것을 찾을 수 있는 가장 가능성 높은 장소는 바로 당신이 방금 작성하고 아직 커밋하지 않은 부분입니다.
- 한때는 그 거짓 양성을 설명하는 제 문장 자체에 대해 거짓 양성이 발생하기도 했습니다. 도구가 도구에 대한 경고를 경고로 표시한 것입니다.
이 글에서 한 가지만 기억한다면 이것입니다: 당신 자신의 테스트는 리뷰가 아닙니다. 테스트는 당신이 이미 생각한 것만을 확인합니다. 그것이 바로 이 모든 한계의 핵심입니다.
나를 가장 놀라게 했던 발견
모두가 거짓 양성을 성가신 것으로 취급합니다. 소음 (noise), 알림 피로 (alert fatigue), 혹은 UX 문제로 여깁니다.
하지만 그것보다 더 심각합니다. 거짓 양성은 거짓 음성을 숨깁니다.
제가 작성한 체크 중 하나가 끊임없이 작동했습니다. 단 한 세션 동안 대략 40번 정도였는데, 거의 모두 틀린 것이었습니다. 순수한 소음이었죠. 그래서 범위를 적절히 조정했더니 소음이 멈췄습니다.
그리고 소음이 멈추는 순간, 그 아래에 숨겨져 있던 진짜 구멍이 드러났습니다: 테스트 스위트 (test suite)를 실행하여 빨간색 실패를 확인한 다음, 아주 사소한 통과 테스트 하나를 실행하고
만약 당신이 AI 출력물을 검증하는 무언가를 만들고 있다면, 이 부분이 바로 훔쳐갈 만한 가치가 있는 대목입니다. 무언가를 확인하는 방법에는 세 가지가 있으며, 대부분의 사람들은 첫 번째 단계에서 바로 세 번째 단계로 건너뜁니다.
Tier 1 — 이미 가지고 있는 것을 확인하십시오. 차이점 (diff), 명령 기록 (command history), 종료 코드 (exit code). 무료이며, 즉각적이고, 증명 가능합니다. 대부분의 사람들은 이를 제대로 수행하지 않습니다. 저 역시 종료 코드를 읽지 않았습니다.
Tier 2 — 새로운 것을 실행하십시오. 커버리지 (Coverage), 변이 테스트 (Mutation testing). 내가 변경한 코드가 실제로 실행되었는가? AI는 필요하지 않습니다. 단지 한 번의 실행 비용만 더 들 뿐입니다. 이것은 거의 모든 사람이 존재 자체를 잊어버리는 단계이며, "내 수정 사항이 아무것도 바꾸지 못했다"라는 버그가 발생하는 지점입니다.
Tier 3 — AI에게 물어보십시오. 최후의 수단입니다. 느리고, 비용이 많이 들며, 놓치는 부분이 있습니다.
그리고 제가 깨닫는 데 일주일이 걸렸던 반전은 이것입니다: "오탐 (false alarm)은 치명적이다"라는 규칙 아래에서는, Tier 2 — 결정론적 (deterministic)이고 AI를 사용하지 않는 단계 — 가 오히려 위험한 단계라는 점입니다. 변이 테스트는 과잉 보고를 합니다. 양치기 소년처럼 굴죠. "결정론적"인 것이 결코 안전 속성 (safety property)이었던 적은 없습니다. "증명 가능하게 옳거나, 아니면 입을 다물 것"이 안전 속성입니다.
(솔직히 말해서) 여전히 할 수 없는 것들
- 런타임 (runtime)을 볼 수 없습니다. 저는 이번 주에 60개의 데이터베이스 행 (database rows)을 작성했습니다. 보이지 않습니다. 도구는 세상이 아니라 차이점 (diff)을 심판합니다.
- 더 나은 접근 방식이 존재했는지 알려줄 수 없습니다. "우연히 통과하게 된 더 나쁜 방식을 선택했다"는 점에 대해서는 대조할 수 있는 지면 진실 (ground truth)이 없습니다. 당신이 무엇이 "더 나은지"를 기록해 두지 않았기 때문입니다. 그 어떤 것도 그것을 확인할 수 없습니다. 도구도, AI도 아닙니다. 오직 당신뿐입니다.
- 자체 개발 과정에 초점을 맞췄을 때, 발견된 내용의 약 75%가 오탐 (false positives)이었습니다. 왜냐하면 당신의 작업 결과물이 센서가 찾는 문자 그 자체일 때, 모든 것이 걸려들기 때문입니다.
간직해야 할 한 문장
한 독자가 제 지난 댓글 섹션에 남겨주었고, 그 이후로 계속 설계 제약 조건이 된 문장입니다:
"오작동하는 검사기는 일주일 안에 무시당하고, 그 후에는 아무것도 잡아내지 못한다."
한 번도 실행된 적 없는 16명의 에이전트. 아무것도 가리키지 못하는 리뷰 게이트 (review gate). 종료된 프로세스로부터 온 초록색 체크 표시. 모든 테스트가 승인했지만 쓰레기로 가득 찬 이메일.
그중 어느 것도 오류를 일으키지 않았습니다. 그중 어느 것도 경고를 보내지 않았습니다. 그 모든 것이 정확히 성공한 것처럼 보였습니다.
그것이 실제 작업입니다: AI를 더 똑똑하게 만드는 것이 아니라, 침묵을 불가능하게 만드는 것입니다.
위의 모든 내용은 이틀간의 세션에서 얻은 실제 결과입니다. 5번의 릴리스(release), 약 500개의 체크 항목에서 702개로 늘어난 테스트 스위트(test suite), 그리고 버그 탐지기(bug-finder)가 찾아낸 13개의 버그 — 제가 아닌 리뷰를 통해 발견되었습니다.
저는 Groundtruth와 EraPin을 구축하면서 이 글을 쓰고 있습니다. 3계층 모델(three-tier model)은 두 명의 엔지니어가 제가 했던 것보다 아키텍처(architecture)를 더 잘 정교화해 준 dev.to 댓글 스레드에서 유래되었습니다. 저는 그들에게 공로를 돌렸습니다. 왜냐하면 이 도구의 핵심 목적은 정직함은 확장 가능하지만, 허세는 그렇지 않다는 것이기 때문입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기