Articles
Guide 12분

고쳤습니다(안 고침) — AI 검증의 함정과 구조적 해결법

AI가 '해결했습니다'라고 보고할 때, 정말 해결된 건지 어떻게 알 수 있을까요? 상태 확인과 동작 검증을 구분하는 스킬을 설계한 과정을 공유해요.

중급 검증 디버깅 스킬 설계 Claude Code 에이전트
이 글을 읽으면
  • 배경: AI에게 버그를 고쳐달라고 했더니 매번 “고쳤습니다”라고 하는데, 다음 날 또 같은 문제가 생겼어요
  • 핵심 인사이트: “파일이 있다”와 “시스템이 그 파일을 읽고 처리한다”는 완전히 다른 검증이고, AI는 이 둘을 구분하지 못하는 경향이 있어요
  • 이런 분에게: AI에게 디버깅을 맡겼다가 “고쳤다는데 왜 또 같은 거야?”를 경험해본 분

”고쳤습니다” — 정말요?

AI에게 버그 수정을 맡기고 “해결했습니다”라는 답을 받았을 때, 안심이 되시나요? 아니면 살짝 불안한가요?

저는 어느 순간부터 불안한 쪽이 됐어요. 같은 문제가 “해결됐다”는 보고를 세 번째 받고 나서부터요.

Selforge 프로젝트에서 스케줄러가 계속 말썽이었어요. “크론이 비어있다”는 같은 에러가 반복됐거든요. 매번 AI에게 고쳐달라고 하면, AI는 열심히 분석하고, 뭔가를 고치고, “해결했습니다”라고 보고했어요. 그런데 다음 날 또 같은 에러가 떴어요.

세 번째 “해결했습니다”를 듣고 나서 깨달았어요 — 문제는 스케줄러가 아니라, AI가 “해결”이라는 단어를 너무 쉽게 쓴다는 거였어요.

Sullivan 채널의 스케줄러가 “크론이 비어있다”는 동일 증상으로 반복 실패했어요. 디버깅을 요청할 때마다 AI는 매번 다른 “근본 원인”을 식별하고, 다른 수정을 적용하고, “해결했습니다”를 보고했어요. 그런데 다음 세션에서 같은 증상이 재발했어요.

원인 체인을 추적해보니 이런 구조였어요:

AI가 세션 간 상태를 유지하지 못함 (stateless)
  → 매 세션 시작 시 이전 맥락 없이 증상만 봄
    → 눈에 보이는 첫 번째 이상을 "근본 원인"으로 착각
      → 증상을 제거하는 수정을 적용
        → 상태가 정상으로 보이는지만 확인
          → "해결했습니다" 선언

AI가 “확인했다”고 말할 때, 뭘 확인한 걸까

AI의 “확인” 보고를 자세히 들여다보니, 패턴이 보이더라고요.

  • “CronList에 5개가 있으니까 등록 완료입니다” — 목록에 있다는 건 확인했는데, 정시에 실행되는지는?
  • “settings.json이 유효한 JSON이니까 훅이 동작합니다” — 파일이 올바르다는 건 확인했는데, 시스템이 그 파일을 읽고 훅을 실행하는지는?
  • durable=true를 설정했으니까 영속화 완료입니다” — 설정을 바꿨다는 건 확인했는데, 세션을 재시작해도 유지되는지는?

비유하자면, 병원에서 약을 처방받고 나서 “약이 약봉투에 들어있으니까 치료 완료”라고 하는 것과 비슷해요. 약이 있는 건 맞는데, 먹었는지, 먹고 나서 나았는지는 전혀 다른 이야기잖아요.

AI의 검증 행동을 분석하니 구조적 혼동이 있었어요.

상태 확인 (Structural)동작 검증 (Behavioral)
파일이 존재한다시스템이 파일을 읽고 처리한다
JSON이 유효하다훅이 실제로 실행된다
CronList에 5개 있다크론이 정시에 동작한다
durable=true를 설정했다세션 재시작 후에도 유지된다

AI는 도구 호출 결과를 즉시 받기 때문에 “CronList가 5개를 반환했다”를 “스케줄러가 작동한다”와 동치로 취급하는 경향이 있어요. 이것은 특정 버그가 아니라 패턴이에요. 상태 확인(structural check)은 필요 조건이지 충분 조건이 아닌데, AI는 이 둘을 혼동해요.

세 종류의 “확인”

이걸 겪으면서 AI가 내놓는 모든 “확인”을 세 종류로 나눠볼 수 있다는 걸 알게 됐어요.

“직접 봤어요” (Behavioral) — 시스템이 실제로 동작하는 걸 관찰한 경우예요. 테스트를 돌려서 통과하는 걸 봤다든지, 크론이 정시에 실행돼서 메시지가 온 걸 확인했다든지. 이것만 “해결됐다”의 근거가 돼요.

“제자리에 있어요” (Structural) — 코드나 설정이 올바르게 배치된 건 확인했지만, 실행은 안 해본 경우예요. 필요하지만 충분하지 않아요. 약이 약봉투에 있는 거예요.

“될 거예요” (Assumed) — 논리적으로 작동할 것 같다고 추정하는 경우예요. “이 설정이 맞으니까 다음 세션에서는 될 거예요” 같은 거요. 이건 증거가 아니에요.

검증 증거를 3가지로 분류하는 체계를 만들었어요.

## Evidence Classification

**Behavioral** — 시스템이 실제로 동작하는 것을 관찰함
- 테스트를 실행해서 통과 확인. 엔드포인트를 호출해서 응답 수신. 크론을 기다려서 메시지 확인.

**Structural** — 코드/설정이 올바르게 배치되어 있음 (실행은 미확인)
- 파일 존재. JSON 유효. 설정값 확인. CronList에 항목 존재.

**Assumed** — 논리적으로 작동할 것이라고 추정함
- "settings.json이 맞으니까 훅이 실행될 것이다." "다음 세션에서 반영될 것이다."

핵심 규칙: Behavioral 증거만이 “fixed” 선언의 근거가 된다. Structural은 필요 조건이지 충분 조건이 아니에요. Assumed는 증거가 아니에요.

한 가지 더 — 사용자의 진술도 Assumed로 분류해요. “서버 재시작했어”라고 사용자가 말해도, lsofps로 확인하기 전까지는 Assumed예요.

구현 과정: 보고서 구조가 정직함을 만들게 하기

“앞으로는 꼼꼼하게 확인해”라고 AI에게 말하는 건 효과가 없었어요. 주의를 당부하는 건 한계가 있더라고요. 그래서 다른 접근을 택했어요 — AI가 따르는 프로세스 자체에 정직함을 구조적으로 심는 거예요.

핵심 아이디어는 간단했어요. AI가 수정 결과를 보고할 때, 보고서 양식에 “미검증 항목” 섹션이 있게 만든 거예요. 이 섹션이 비어있지 않으면, “고쳤습니다”라는 단어를 쓸 수 없어요. 대신 “수정을 적용했고, 부분적으로 검증됐습니다”라고 써야 해요.

보고서의 구조 자체가 거짓말을 어렵게 만드는 거예요. “미검증 항목” 칸을 채우지 않으면 보고서가 불완전해 보이니까, 스킬을 따르는 한 “다 됐습니다”라고 넘어갈 수가 없어요.

기존에 systematic-debugging, verification-before-completion, verify 등 디버깅 관련 스킬이 있었지만, 세 스킬 모두 상태 확인과 동작 검증의 구분을 명시적으로 강제하지 않았어요.

새 스킬의 설계 방향을 정할 때 세 가지 선택지를 검토했어요.

  1. 기존 스킬 보강 — systematic-debugging에 증거 분류 섹션 추가. 스킬 수는 안 늘지만 범위가 비대해짐.
  2. 독립 스킬 (두꺼운 버전) — Phase 06의 완전한 프로세스를 자체 포함. 자기완결적이지만 기존 스킬과 Phase 12가 완전히 겹침. 456줄.
  3. 독립 스킬 (래퍼 버전) — 근본 원인 분석은 systematic-debugging에 위임하고, 이 스킬 고유의 가치(증거 분류, 컨텍스트 복구, 정직한 보고, 영속 기록)에만 집중. 139줄.

최종적으로 선택지 3을 택했어요. 스킬의 핵심 구조는 이래요:

## Phase 4: Honest Status Report

### Verified (behavioral evidence)
- [실제로 동작을 확인한 항목과 그 증거]

### Unverified (structural evidence only)
- [올바르게 배치되었지만 실행 테스트를 하지 않은 항목]

### Cannot verify in this session
- [이 세션에서 검증 불가능한 항목과 검증 조건]

### Known risks
- [여전히 잘못될 수 있는 것]

핵심 기제: Unverified 섹션이 비어있지 않으면 “fixed”, “resolved”, “complete”, “done”이라는 단어를 사용할 수 없도록 했어요. 대신 “fix applied, partially verified”라고 표현하고 미검증 항목을 명시해야 해요.

그리고 Phase 5에서 <project>/.claude/<issue>-verification.md에 영속 기록을 남기도록 했어요. 다음 세션이 Phase 0에서 이 기록을 읽으면, “매번 다른 근본 원인”이 나오는 루프를 끊을 수 있어요.

실제 동작: 테스트에서 차이가 선명했던 순간

이 구조가 정말 효과가 있는지 테스트를 해봤어요. 같은 디버깅 시나리오를 두 가지 방식으로 돌렸어요 — 스킬 없이 vs. 스킬 적용해서.

가장 차이가 선명했던 건 이런 상황이었어요. 환경변수가 파일에는 적혀있는데 실행 시점에서 undefined가 나오는 문제였거든요. 사용자가 “서버 재시작했어”라고 말했을 때:

  • 스킬 없이: “서버를 재시작하셨군요. 그러면 환경변수가 반영됐을 겁니다.” → 사용자의 말을 사실로 수용
  • 스킬 적용: “서버 재시작은 assumed 증거입니다. lsofps로 프로세스가 실제로 새로 떴는지 확인해야 해요.” → 사용자의 진술도 검증 대상으로 취급

사용자가 한 말도 그냥 믿지 않고 확인한다는 게 처음엔 좀 과하게 느껴질 수 있는데요. 실제로 “재시작했다고 생각했는데 안 됐던” 경험이 있으신 분은 이 차이가 체감되실 거예요.

3개 시나리오를 스킬 유무로 나눠서 서브에이전트 6개를 병렬 실행하는 비교 테스트를 진행했어요.

테스트 시나리오:

  1. recurring-bug — 반복 실패하는 스케줄러 (실제 Sullivan 케이스)
  2. state-vs-behavior — 환경변수가 파일에는 있는데 런타임에서 undefined
  3. multi-session — CI Docker 빌드 간헐적 실패

recurring-bug 시나리오 결과:

항목스킬 없이스킬 적용 (slim)
이전 증거를 유형별 분류XO
순환 의존성 발견부분적O
수정 전 검증 계획 수립XO
Verified/Unverified 구분 보고부분적O
출력 길이227줄333줄

차별점이 가장 선명했던 건 state-vs-behavior 테스트였어요. 사용자가 “서버 재시작했어”라고 말했을 때, 스킬 없이는 사실로 수용했지만, 스킬을 적용하면 assumed 증거로 분류하고 lsof/ps로 확인해야 한다고 판단했어요. 사용자의 진술도 검증 대상으로 취급하는 것 — 이것이 기존 스킬들과의 근본적 차이였어요.

두꺼운 버전(v1)과 슬리밍 버전(v2)도 비교했는데, 핵심 차별점은 100% 동일하면서 출력은 27% 줄고 시간은 18% 줄었어요. 기존 스킬과 겹치는 부분을 유지할 이유가 없었고요.

의외의 발견: 스킬의 진짜 가치

테스트 중에 의외의 발견이 하나 있었어요.

스킬 없이 테스트한 AI도 꽤 좋은 분석을 했거든요. 왜 그랬는지 봤더니, 이전에 스킬을 적용한 세션에서 남긴 검증 기록(verification.md)을 읽었기 때문이었어요.

그러니까 스킬의 진짜 가치는 실시간 프로세스 자체에만 있는 게 아니었어요. 미래의 세션을 위한 기록을 남기도록 강제하는 것 — 이게 더 큰 가치였어요. 오늘의 검증 기록이 내일의 디버깅을 돕는 거예요.

스킬이 만들어내는 부산물이 스킬 자체보다 더 오래 가치를 발휘하더라고요.

테스트에서 예상 밖의 결과가 나왔어요. 스킬 없이 실행한 에이전트도 verification.md를 읽고 상당히 좋은 분석을 했어요. 그 문서는 스킬의 Phase 5가 만들어낸 부산물이었거든요.

이것이 시사하는 바: 스킬의 가치는 실시간 프로세스 자체보다, 미래 세션을 위한 영속적 기록물을 생산하도록 강제하는 것에 있었어요.

Phase 5의 영속 기록 구조:

# <issue>-verification.md

Date: 2026-04-14
Root cause: [한 문장]
Fix applied: [변경 사항]

## Verified (behavioral)
- [증거와 함께]

## Unverified
- [다음 세션에서 확인할 것]

## Next session must check
- [구체적 확인 항목]

이 기록이 다음 세션의 Phase 0(컨텍스트 복구)에서 읽히면, AI가 “이전에 같은 원인이 이미 ‘해결됐다’고 보고된 적이 있다”는 걸 인식하게 돼요. “매번 다른 근본 원인” 루프가 끊기는 지점이에요.

적용 가이드: 핵심만 빌려가기

이 스킬의 전체 구조를 그대로 가져갈 필요는 없어요. 핵심 아이디어만 빌려가도 충분해요.

가장 쉬운 시작: AI가 “해결했습니다”라고 보고할 때, 이 질문을 던져보세요.

“그걸 어떻게 확인했어? 직접 실행해본 거야, 아니면 코드가 맞아 보이는 거야?”

이 질문 하나만으로도 상태 확인과 동작 검증이 구분돼요. AI가 “코드가 올바르게 설정되어 있으니까…”라고 답하면, 그건 아직 검증이 아니에요.

한 단계 더: “미검증 항목이 뭐야?”라고 물어보세요. 대부분의 AI는 이 질문을 받으면 솔직해져요. 스스로 구분하지 못하는 것과, 물어봤을 때 구분하지 못하는 건 다른 이야기거든요.

반복 버그에 시달린다면: 이전 디버깅 결과를 파일로 남기고, 다음 세션에서 먼저 읽게 하세요. “지난번에 이 원인을 이미 ‘고쳤다’고 했는데 또 나왔어. 그때 기록 먼저 읽어봐.” 이 한마디가 “매번 다른 근본 원인” 루프를 끊는 시작이에요.

프로젝트에 적용할 때 참고할 포인트들이에요.

1. CLAUDE.md에 검증 원칙 추가

프로젝트의 CLAUDE.md나 에이전트 설정에 한 줄만 추가해도 효과가 있어요:

## 검증 원칙
- 상태 확인(파일 존재, 설정값 확인)과 동작 검증(실제 실행, 결과 관찰)을 구분한다.
- "fixed" 선언은 동작 검증(behavioral evidence)이 있을 때만 가능하다.

2. 보고 구조 강제

수정 후 보고 양식에 Unverified 섹션을 포함시키면, 구조적으로 정직함이 강제돼요:

보고할 때 이 구조를 사용하세요:
- Verified: [behavioral evidence로 확인한 항목]
- Unverified: [structural evidence만 있는 항목]
- Cannot verify now: [이 세션에서 확인 불가한 항목]

Unverified가 비어있지 않으면 "fixed"를 사용하지 마세요.

3. 영속 기록 패턴

반복 실패하는 이슈가 있다면:

수정 후 `.claude/<issue>-verification.md`에 기록을 남기세요.
다음 디버깅 세션 시작 시 이 파일을 먼저 읽으세요.
같은 "근본 원인"이 이전 기록에 이미 있으면, 더 깊이 파세요.

4. 래퍼 패턴 — 기존 도구와 겹치지 않기

새 스킬을 만들 때, 기존 스킬과 겹치는 기능을 복사하지 말고 위임하세요. 이 스킬에서 근본 원인 분석은 systematic-debugging에 맡기고, 증거 분류와 정직한 보고라는 고유 가치에만 집중한 결과 — 233줄에서 139줄로 줄었는데 핵심 차별점은 100% 유지됐어요.

마무리

이 스킬을 만들면서 가장 인상적이었던 건, AI 스스로는 자기 행동의 패턴을 잘 인식하지 못한다는 거였어요. “왜 매번 원인이 달라지고, 매번 해결됐다고 하는 거야?” — 이 질문은 사람이 먼저 던져야 했어요.

AI의 “고쳤습니다”를 의심하라는 게 아니에요. 다만, “뭘 확인했는지”를 같이 물어보는 습관을 들이면, 같은 문제에 여러 번 시간을 쓰는 일이 줄어들더라고요.

상태 확인과 동작 검증의 구분 — 이건 비단 AI 디버깅에만 해당하는 이야기가 아닐 수도 있어요. “확인했다”는 말이 실제로 무엇을 확인한 건지 돌아보는 것, 그게 더 넓은 범위에서도 유효한 습관이 아닐까 싶어요.