지식

[Claude Code] 루프는 하나다 — 자율 에이전트에게 시킨 일이 끝났는지 어떻게 판정할 것인가

배경 — 자율 에이전트에게 "끝까지 돌려라"라고 시키면 생기는 문제

에이전트에게 구현 작업 하나를 통째로 맡기고 여러 번의 실행 주기(사이클)를 거쳐 완주시키는 방식을 흔히 "루프(loop)"라고 부른다. 사람이 매 턴 개입하는 대화형 세션과 달리, 루프는 스펙을 확정한 뒤 손을 떼고 에이전트가 계획 → 구현 → 검증 → 반영을 반복하게 둔다. 원리는 단순하지만 실제로 며칠간 루프를 돌려보면 세 가지 문제가 반드시 튀어나온다.

첫째, 정지 조건이다. "다 됐다"는 언제 알 수 있는가? 에이전트 스스로 "완료했습니다"라고 선언하는 것을 정지 신호로 삼으면, 막힌 작업 하나를 붙잡고 사이클을 무한히 반복하며 토큰을 태우는 사고가 난다. 둘째, 자기 채점이다. 코드를 작성한 바로 그 에이전트가 자기 코드를 검증까지 하면, 애초에 그 에이전트가 보지 못한 맹점은 검증 단계에서도 똑같이 보이지 않는다. 셋째, 체급 판단이다. "이 작업은 정식 루프로 돌릴 만큼 큰가, 아니면 그냥 한 세션에서 처리할 만큼 작은가"를 매번 판단해야 한다면, 그 판단 자체가 작업을 시작하기도 전에 발생하는 새로운 오버헤드가 된다.

이 글은 실제로 루프 방법론을 몇 주간 돌리면서 이 세 문제에 부딪히고, 그 경험을 근거로 규약을 개정한 과정을 정리한다. 개정 전 구조(이하 v1)는 스펙 문서 하나와 상태 파일 다섯 종류로 이루어져 있었고, 정지 조건은 "모든 작업 항목이 통과(passes: true)했는가" 단 하나였다.

무엇이 부족했나 — 드러난 갭 세 가지

v1로 열두 사이클짜리 프로젝트 하나를 완주시킨 뒤, 외부에서 공개된 루프 분류 체계(트리거 축으로 turn-based / goal-based / time-based / proactive 네 갈래로 나누는 분류)와 자체 구조를 대조해봤다. 대조 결과 드러난 갭은 세 가지였다.

  1. 정지 상한(서킷브레이커)의 부재. 구조적으로 막힌 작업 항목이 있어도, 이를 걸러내는 런타임 안전장치가 없었다. 이전 프로젝트에서 막힌 항목 하나가 멈춘 것은 계획 단계에서 미리 "이건 사람만 풀 수 있다"고 분류해뒀기 때문이지, 루프 자체가 위험을 감지해서가 아니었다.
  2. fresh-context 리뷰의 부재. 구현자(Worker)와 검증자(Advisor)를 분리하긴 했지만, 브리프를 쓰는 것도 그 브리프에 따른 결과를 채점하는 것도 같은 주체였다. 브리프 자체에 있는 맹점은 아무도 잡아내지 못하는 구조였다.
  3. 중간 체급의 부재. 대화형 세션(turn-based)과 파일 여섯 개짜리 정식 루프 사이에 아무것도 없었다. 반나절짜리 작업을 넣기엔 정식 루프가 과체중이고, 그렇다고 그냥 세션으로 처리하면 검증 규율이 통째로 사라졌다.

세 번째 갭에 대해 팀이 내놓은 방향은 이랬다. "좋은 루프는 일관적이어야 한다. 어려운 구현과 간단한 구현을 똑같이 취급해라. 불필요한 레이어를 걷어내고 근본적인 루프 구조를 하나로 설계하라." 이 한 문장이 이후 결정 전체의 축이 됐다.

결정 다섯 가지

  1. 정지 조건 교체. "전 항목 passes: true"에서 **"전 항목 판정 완료(passes 또는 blocked)"**로 바꿨다. blocked는 같은 작업 항목이 세 사이클 연속으로 검증에 실패하면 검증자가 사유와 함께 격상하는 서킷브레이커다. 격상 후에도 루프는 나머지 항목을 계속 돌리고, 최종 보고에 막힌 목록을 포함한다.
  2. 상태 파일 5종 → 2종. 계획 문서, 결정 로그, 작업 일지 세 파일을 폐지했다. 계획 문서의 "완료 판정"은 상태 파일의 통과 여부와 중복이었고 "단계 의존관계"는 이미 각 항목의 단계(phase) 번호가 담고 있었다. 결정 로그의 재량 결정은 진행 기록(progress) 파일의 사이클 항목 안 소절로 흡수됐다. 작업 일지는 규약 문서로 증류가 끝나면 순수 오버헤드였다 — 행동을 요구하는 관찰은 곧장 규약 문서 개정으로 반영하기로 했다.
  3. 사이클 루틴에 fresh-context 리뷰 고정. 커밋 직전에 별도 컨텍스트를 가진 리뷰 서브에이전트(또는 리뷰 슬래시 커맨드)를 반드시 거치게 했다.
  4. 마감 루틴에 검증 자산 스킬 증류 + 사이클당 토큰 소비 기록. 검증 노하우가 프로젝트와 함께 사라지지 않게 하고, 사이클 크기를 조정할 때 감이 아니라 숫자 근거를 남기게 했다.
  5. "루프는 하나다." 체급을 나누지 않는다. 어떤 작업이든 파일 세 개(스펙 원본 하나 + 상태 파일 두 종)와 같은 루틴, 같은 정지 조건을 쓰고, 내용의 분량만 작업 크기에 비례한다. 계획 단계 인터뷰도 마찬가지다 — "사람만 풀 수 있는 걸림돌을 전부 없앤다"는 원칙은 불변이고, 질문 개수만 스펙의 불확실성에 비례해 늘거나 준다.

실물: 상태 파일 스키마

말로만 하면 추상적이니 실제로 쓰는 파일 형태를 그대로 옮긴다. 상태 파일 두 종은 각각 JSON과 Markdown이다.

features.json — 완료 기준이 살아있는 장부

최상위는 프로젝트 이름, 거버넌스 규칙 한 문단, 작업 항목 배열 세 키뿐이다. 항목 하나는 이렇게 생겼다.

{
  "id": "F001",
  "phase": 1,
  "desc": "무엇을 만드는가 — 산출물 경로와 핵심 요건. desc는 방향 제시일 뿐 완료 판정 근거가 아니다.",
  "verify": "grep -in 'keyword' target.md 결과 3건 이상 && python3 -m json.tool schema.json 통과 && curl -s http://localhost:PORT/health 응답 200",
  "passes": false
}

passes는 검증자만 바꾼다. 그것도 verify에 적힌 절차를 직접 재실행해서 통과를 관찰한 뒤에만 true로 바꾼다. 구현자의 보고나 desc를 근거로 찍지 않는다. 한 번 true가 된 뒤에도 해당 항목의 스펙이나 대상 파일이 바뀌면 그 passes는 다시 신뢰하지 않는 대상이 된다 — verify를 재실행하기 전까지는.

막힌 항목은 이렇게 표시한다.

{
  "id": "F010",
  "phase": 3,
  "desc": "외부 결제 API 연동 테스트 자동화",
  "verify": "테스트 스위트 실행 → 응답 코드 200 확인",
  "passes": false,
  "blocked": "human_blocked: 결제 API 샌드박스 키 발급이 필요 — 사람의 승인 없이는 진행 불가"
}

blocked 사유는 접두사 두 가지 중 하나로 시작한다. resolvable:은 에이전트가 원리상 풀 수 있는 문제지만 세 사이클을 소진했다는 뜻이고(다음 루프나 사람이 힌트를 주면 재개 가능), human_blocked:은 크리덴셜·외부 승인·수동 단계처럼 애초에 사람만 풀 수 있다는 뜻이다. 후자는 세 사이클을 기다리지 않고 그 사실이 확인되는 즉시 격상한다.

progress.md — 사이클 항목 형식

진행 기록 파일은 상단에 고정 절(프로젝트 고유 함정 목록, 검증 인프라 경로) 두 개를 두고, 그 아래에 사이클 일지를 최신순으로 쌓는다. 항목 하나의 형식은 고정돼 있다.

## 사이클 4 — 2026-07-08 — F009 통과, 전 항목 판정 완료 (9/9 passes, blocked 0)

- 한 일: 참조 무결성 검사 → 전체 diff 마감 리뷰 verdict ITERATE(중간 1·낮음 2) → 전부 반영
  → 재검토 → 최종 verdict OKAY → F009 passes.
- 결정: 원래 문안이 "제외"로 단정했던 부분을 "필수면 전량 부여 + 제약으로 한정, 불필요하면
  제외"로 바꿈. 이유: 규약이 스스로 든 예시 두 건이 모순됐고, 남는 위험은 은폐보다 명시가 낫다.
- 토큰: 마감 리뷰 + 재검토 약 10만. 프로젝트 전체 합계 약 40만.
- **다음 할 일**: 없음 — 전 항목 판정 완료. 사람 몫: 배포 승인 대기.

소절 순서는 한 일 / 결정(형식 = 결정·이유·기각한 대안, 재량 결정은 코드 한 줄이라도 반드시 남긴다) / 토큰 / 다음 할 일이다. 마지막 항목은 필수이고 사이클의 끝에 온다 — 다음 실행 주기가 이걸 읽고 이어받기 때문이다.

Worker 브리프 템플릿

구현자에게 넘기는 지시문도 섹션이 고정돼 있다.

[작업] F0XX: <무엇을>
[역할] 너는 구현자다. 보고는 구현자 입장으로.
[컨텍스트] 파일 경로, 재사용할 기존 모듈(재구현 금지 명시), 컨벤션
[검증 인프라] 검증자가 구축한 도구·경로·실행법
[스펙 요점] 해당 부분 발췌 — 재탐색하지 않도록
[함정] 누적 함정 목록에서 해당되는 것 전부
[산출물의 후속 용도] 나중에 어디 재사용되는지 — 재사용 가능한 설계를 유도
[완료 기준] verify 필드 그대로 + "직접 실행해 결과를 보고에 포함하라"
[금지] 상태 파일 수정, 커밋/푸시, 다른 구현자 작업 영역

역할 혼동 방지 문구를 맨 위에 박는 이유는 단순하다 — 구현자 역할로 소환된 에이전트가 검증자 어투로 "완료했습니다, 문제없습니다"라고 자기 채점을 시작하는 사고를 실제로 겪었기 때문이다.

verify 작성 기준 — desc가 아니라 verify가 완료를 정의한다

완료 기준은 desc가 아니라 verify가 진다. desc는 구현자를 향하고, verify는 검증자의 채점표이자 루프의 정지 판정 근거다. 좋은 verify는 세 요건을 모두 만족해야 한다.

  1. 기계 판정 가능 — "자연스러운지", "잘 되는지" 같은 인상이 아니라 참/거짓이 갈리는 관찰이어야 한다.
  2. 실행 명령 포함 — 무엇을 실행해서 무엇을 보는지 명령 자체가 적혀 있어야 검증자가 그대로 재실행할 수 있다.
  3. 성공 판정값 명시 — 통과선을 숫자나 문자열로 못박는다. 실패 경로(잘못된 입력이 401이나 404를 반환하는지)도 하나 이상 포함하면 스텁 구현이나 보안 결함을 같이 잡는다.

실제로 쓴 좋은 예시 하나를 옮긴다.

note create 201 → get(stats 필드 존재) → update(PATCH 200) → delete 204 → get 404
+ 잘못된 키 401 + upload(PNG) 201, url 반환. 전체 출력 캡처에서 크리덴셜 문자열 grep 0건.
종료 후 개발 서버 정리

명령, 상태 코드, 실패 경로, 뒷정리까지 다 들어 있다. 반대로 실제로 걸러낸 나쁜 예시는 이런 식이었다.

"API 서버가 정상 동작하는지 확인한다"

실행 명령이 없고, 성공 판정값도 없고, 기계로 판정할 수 없다. 이런 문장으로는 애초에 passes를 관찰로 찍을 방법이 없다.

실전에 넣어본 결과

같은 날 개정한 스키마를 다른 프로젝트(서브에이전트 작성 규약을 다듬는 아홉 개 항목짜리 작업)에 바로 적용해봤다. 결과는 9/9 통과, blocked 0건으로 완주했다. 적용 도중 스키마 허점을 하나 발견했다 — 사이클 일지를 상단 고정 절(함정 목록·검증 인프라) 아래가 아니라 위에 쌓는 실수를 저질렀고, 이걸 교정해서 공용 함정 목록에 승격했다. 이 정도로 사소한 실수까지 잡아 문서화하는 게 번거로워 보일 수 있지만, 다음 프로젝트에서 똑같은 실수를 반복하지 않게 하는 유일한 방법이 이거였다.

이 서킷브레이커가 실전에서 어떻게 작동하는지 보여주는 사례도 있다. 다른 문서 정리 프로젝트에서 병렬로 띄운 구현자 하나가 편집을 마친 뒤 28분 동안 아무 보고도 하지 않았다. 규칙대로라면 "편집 종료 후 20분 대기 → 보고 요청 → 15분 더 대기 → 강제 중단하고 검증자가 직접 확인"인데, 이 사례에서 실제로 그 경로를 탔다. 구현자를 강제 중단시키고 검증자가 직접 결과물을 열어 서른네 개 문서를 검증했고, 나머지 마흔한 개 문서를 맡은 다른 구현자는 정상 보고를 냈다. 무보고 상태에서 무한정 기다리지 않고 시한을 두고 강제로 검증자가 넘겨받는다는 규칙이 없었다면, 이 사이클은 통째로 멈춰 있었을 것이다.

근거와 기각한 대안

결정에 이르기까지 검토했다가 버린 대안도 셋 있다.

  • 중간 체급(경량 루프 변형) 신설. 검증자 쪽에서 제안했지만 "일관성"을 이유로 기각됐다. 체급이 나뉘면 작업마다 "어느 구조로 갈지"를 판단하는 것 자체가 새로운 오버헤드가 되고, 체급 사이의 규율 격차가 생길 것으로 예상됐다. 대신 루프 자체를 가볍게 만들어 단일 구조를 유지하는 쪽을 택했다.
  • 상태 파일 5종 유지. 앞서 정리한 중복 분석대로, 폐지한 세 파일은 다른 파일에 이미 내용이 있거나 흡수 가능한 레이어였다.
  • 외부 목표 기반 정지 프리미티브 의존. 사용 중인 환경의 스킬 목록에 그런 기능이 아직 없었다. 결국 상태 파일 장부와 검증자의 직접 판정이 그 역할을 자체 구현하는 수밖에 없었다.

간단한 작업에서 남는 유일한 "오버헤드"는 verify를 쓰는 일인데, 이건 걷어낼 수 없는 코어라고 판단했다 — 완료를 정의하는 행위 자체이기 때문이다. 반대로 바꾸지 않은 것도 명시해둘 만하다. 구현자와 검증자의 분리, verify 3요건, 통과한 항목만 커밋하고 원격 저장소로 밀어 올리지는 않는 것, "다음 할 일"을 다음 실행 주기에 인계하는 것 — 이 넷은 여러 사이클의 실증에서 나온 척추라 손대지 않았다.

교훈과 재검토 조건

가장 크게 남은 교훈은, 루프의 신뢰는 결국 검증자가 "손을 뗄 수 있는 유일한 이유"라는 점이다. 정지 조건을 아무리 정교하게 설계해도, 통과 표시를 찍는 주체가 실제로 결과물을 열어보지 않으면 그 표시는 거짓말이 된다. 그래서 이 규약에서 가장 무거운 경고는 이거다 — "완료했다"는 주장이지 증명이 아니다. 검증자가 직접 관찰한 뒤에만 통과로 찍는다.

재검토가 필요한 조건도 미리 못박아뒀다. 만약 사용 중인 환경에 턴 상한을 내장한 목표 기반 정지 프리미티브가 새로 생긴다면, 서킷브레이커의 "세 사이클" 규약을 다시 검토해야 한다. 지금은 대체할 도구가 없어 자체 구현했지만, 더 나은 원시 도구가 생기면 굳이 직접 만든 상한을 고집할 이유가 없다.

루프 방법론 자체에 대한 참고 자료로는 Addy Osmani가 쓴 "Loop Engineering"(https://addyo.substack.com/p/loop-engineering)을 권한다 — automations·worktrees·skills·connectors·sub-agents·memory 여섯 요소로 루프를 분해하는 틀과, "작성자는 채점자가 될 수 없다"는 원칙이 이 글에서 정리한 결정 전체의 출발점이었다.

조회 2댓글 0

댓글

아직 댓글이 없습니다. 첫 댓글을 남겨보세요.