메뉴

← 아티클

AI-native engineering

에이전트의 "다 했습니다"를 믿지 않는 법: 병렬 개발과 회의적 검증 루프

이 블로그의 고도화를 두 코딩 에이전트에게 병렬로 맡겼습니다. 파일 소유권과 워크트리로 충돌을 막고, 별도 세션의 회의적 검증이 개발 세션의 보고를 반증하게 만든 하루의 기록입니다.

이 글의 내용

에이전트가 구현을 마쳤다고 보고한다. 변경 파일 목록이 있고, 테스트도 통과했다고 한다. 이 보고를 어디까지 믿어야 할까. 지난 글에서 완료의 단위는 코드 작성이 아니라 검증 가능한 상태 변화라고 썼다. 이번 글은 그 원칙을 실제 작업에 적용한 기록이다. 대상은 다른 회사의 프로젝트가 아니라 지금 읽고 있는 이 블로그다.

하루 동안 이 사이트에 네 가지가 추가됐다. 아티클의 저자 신원 메타데이터, 발행 템플릿과 체크리스트, RSS 피드, 그리고 새 글의 골격을 만들어 주는 스캐폴딩 도구다. 구현은 전부 코딩 에이전트가 했고, 두 트랙씩 병렬로 진행했다. 지금 읽고 있는 이 글의 골격도 그날 만든 도구로 생성했다.

핵심은 에이전트를 신뢰하는 것이 아니라, 에이전트의 주장을 반증할 수 있는 구조를 신뢰하는 것이다.

전략 문서에서 실행 계획으로

시작은 코드가 아니라 문서였다. 블로그를 어떤 방향으로 키울지 정한 전략에서 코드로 만들 수 있는 항목만 골라 플랜 문서로 옮기고, 저장소에 커밋했다. 문서에는 각 작업의 스펙과 함께 한 가지가 더 들어간다. 트랙마다 만질 수 있는 파일의 목록이다.

플랜을 채팅이 아니라 저장소에 두는 이유는 단순하다. 병렬로 뜨는 세션들이 같은 파일을 읽기 때문이다. 개발 세션은 그 문서를 보고 일하고, 검증 세션은 같은 문서를 기준으로 채점한다. 스펙이 대화 기록에 흩어져 있으면 세션마다 다른 요약을 기억하지만, 파일로 고정하면 전원이 같은 문장을 본다.

이번 작업의 트랙 구성 (두 라운드, 라운드당 두 트랙)
트랙 산출물 소유 파일
A. 신뢰 신호 저자 JSON-LD, 프로필 블록, 가이드 크로스링크 블로그 HTML 두 개, styles.css는 끝에 추가만
B. 발행 키트 아티클 템플릿, 발행 체크리스트 docs/blog/ 신규 파일만
C. RSS 피드 feed.xml, 피드·목록 동기화 검사 blog/feed.xml, 검사 스크립트는 함수 추가만
D. 스캐폴더 새 글 골격 생성 CLI scripts/new-article.js 신규

병렬의 전제: 소유권, 격리, 커밋 금지

두 에이전트가 같은 파일을 고치면 마지막에 쓴 쪽이 이긴다. 그래서 트랙은 기능이 아니라 파일 경계로 나눈다. 어쩔 수 없이 공유하는 파일에는 더 좁은 규칙을 준다. 이번 작업에서 styles.css는 "파일 끝에, 정해진 접두사의 클래스 추가만"이었고, 검사 스크립트는 "기존 함수는 건드리지 말고 새 함수 추가만"이었다.

격리는 git worktree가 맡는다. 트랙마다 저장소의 독립된 사본에서 일하므로, 한 세션이 무엇을 하든 다른 트랙과 통합 브랜치는 물리적으로 안전하다. 그리고 에이전트는 커밋하지 않는다. 변경은 워킹트리까지만 남기고, 커밋과 머지는 리뷰를 통과한 다음의 일이다. 이렇게 하면 에이전트가 만든 변경 전체가 하나의 diff로 리뷰 테이블에 올라온다.

결과부터 말하면, 네 트랙이 두 라운드에 걸쳐 머지되는 동안 충돌은 한 건도 없었다. 운이 좋았던 게 아니라 경계를 먼저 그은 결과다.

두 세션을 나란히 돌리기

이 파이프라인에는 세 층이 있다. 구현과 검증을 맡는 워커는 Codex CLI에서 돌아가는 gpt-5.6-luna 세션이다. 개발 세션은 추론 강도를 가장 높게, 검증 세션은 한 단계 낮게 설정했다. 트랙 분할, 세션 기동, 통합 리뷰, 커밋과 머지 같은 오케스트레이션은 Claude Code가 맡았다. 사람은 방향을 정하고 승인했다. 이 셋의 역할 구분은 뒤에 나올 사고 대응에서 값어치를 한다.

동시 실행은 두 세션까지로 제한했다. 처음부터 원칙이 있었던 건 아니고, 이 작업이 돌아간 로컬 머신이 세 개를 버티지 못했기 때문이다. 그래도 이 상한은 하네스의 일부라고 생각한다. 자원이 모자라 느려지는 세션은 실패한 세션보다 진단하기 어렵다.

모든 세션에는 45분짜리 워치독을 걸었다. 지난달 한 세션이 API 재연결 루프에 걸려 여덟 시간을 조용히 멈춰 있던 적이 있어서다. macOS에는 timeout 명령이 없어서 perl -e 'alarm shift; exec @ARGV' 2700 codex exec … 한 줄로 대신했다.

개발 세션에 주는 프롬프트는 설명문이 아니라 계약이다. 트랙 C에 줬던 것을 줄이면 이렇다.

플랜 문서의 Track C 섹션을 정독하고 구현하라.

절대 규칙
- 소유 파일 밖은 수정·생성 금지: blog/feed.xml(신규),
  scripts/check.js(함수 추가만), 블로그 HTML 두 개(링크 한 줄씩).
- git commit 금지. 변경은 워킹트리에만 남긴다.
- 새 의존성 금지. 피드에 이메일 주소를 넣지 않는다.

완료 전 필수
- npm test 통과를 직접 확인한다.
- 새 검사가 실제로 고장을 잡는지 확인한다: 피드 링크 하나를
  일부러 틀리게 바꿔 검사가 실패하는 것을 본 뒤 원상복구한다.
- 마지막 보고: 변경 파일, 구현 요약, 테스트 결과,
  판단이 필요했던 지점.

마지막 항목들이 이 글의 주제와 닿는다. "테스트를 추가했다"는 보고는 받지 않는다. 그 테스트가 실패할 수 있다는 증거까지 요구한다. 한 번도 실패해 본 적 없는 테스트는 아무것도 지키고 있지 않을 수 있다.

회의적 검증 루프: 자기 보고를 반증한다

개발 세션이 끝나면 같은 워크트리에서 새 세션을 연다. 같은 모델이지만 지시가 정반대다. 프롬프트의 첫 문장은 이렇다. "다른 에이전트가 이 워킹트리에 구현을 남겼다. 그 에이전트의 자기 보고는 신뢰하지 말고 모든 것을 직접 재검증하라." 판정은 마지막 메시지 첫 줄에 VERDICT: PASS 또는 FAIL로 강제한다.

이 루프는 장식이 아니다. 트랙 A의 개발 세션은 다섯 항목을 모두 구현했고 테스트도 통과했다고 보고했다. 검증 세션의 판정은 FAIL이었다. 아티클 목록 페이지의 JSON-LD에 저자 정보가 빠져 있었다. 스펙 문서와 산출물을 항목별로 대조해서 찾아낸 것이다. 개발 세션을 재개해 고치게 했고, 재검증에서 PASS가 났다.

흥미로운 건 개발 세션의 보고가 거짓이 아니었다는 점이다. 자기가 해석한 스펙 기준으로는 정말 다 했다. 문제는 해석의 빈틈이었고, 그 빈틈은 같은 컨텍스트를 공유하지 않는 두 번째 세션의 눈에만 보였다. 사람 둘이 하는 코드 리뷰가 작동하는 이유와 같다.

검증 세션에도 증거를 직접 요구한다. 테스트를 다시 실행하고, 개발 세션이 했다는 고장 재현 — 일부러 틀리게 바꿔 검사가 실패하는지 보는 것 — 을 그대로 따라 하고, 소유권 밖 파일이 변경되지 않았는지 diff로 확인한 뒤에야 판정을 쓰게 한다.

검증 루프가 놓친 것

검증이 PASS를 준 산출물에서도 통합 리뷰가 세 가지를 더 잡았다.

하나. 템플릿에 CSP meta 태그가 들어 있었다. 이 사이트의 CSP는 배포 설정의 경로별 헤더로 전달되는데, meta로 이중 정의하면 브라우저는 두 정책의 교집합을 적용해서 헤더 쪽 정책과 어긋난다. 검증 세션은 "CSP를 위반하는 외부 리소스가 없는가"를 확인했고, 그 기준으로는 통과가 맞다. "CSP를 전달하는 방식이 실제 배포와 같은가"는 누구의 스펙에도 없었다.

둘. 트랙 A가 만든 실제 화면의 클래스와 트랙 B가 만든 템플릿의 클래스가 미묘하게 달랐다. 각 트랙은 자기 스펙만으로 채점받으니, 트랙 사이의 틈은 어느 검증 세션의 몫도 아니었다.

셋. 템플릿 일부가 이스케이프 없는 치환 자리를 쓰고 있었다. 제목에 따옴표가 들어가면 HTML 속성이 그 자리에서 깨진다. 바로 이 글의 제목에 따옴표가 있고, 스캐폴더가 "로 바꿔 넣었다. 그때 고친 덕이다.

교훈은 분명하다. 검증 루프는 스펙 준수를 증명하지만, 스펙 자체의 구멍은 스펙 밖에서 봐야 보인다. 그래서 머지 직전의 통합 리뷰는 워커와 다른 컨텍스트, 즉 저장소 전체와 배포 구조를 아는 층이 맡는다.

사고 두 건, 격리의 값어치

파이프라인의 가치는 순항할 때가 아니라 사고가 났을 때 드러난다. 이날은 두 건 있었다.

첫째, 수정을 위해 개발 세션을 재개했는데 재개 명령이 작업 디렉터리 옵션을 받지 않았다. 세션은 현재 디렉터리에서 떴고, 하필 그곳이 통합 브랜치의 워크트리였다. 부팅 로그의 workdir 표시를 보고 몇 초 만에 종료했고, 확인 결과 변경된 파일은 없었다. 이후로는 재개도 새 실행과 같은 수준으로 부팅 로그를 검사한다.

둘째 사고는 에이전트가 아니라 오케스트레이터가 냈다. 트랙 머지를 통합 워크트리가 아닌 트랙 워크트리에서 실행한 것이다. git reset --hard 한 번으로 복구됐다. 브랜치와 워크트리로 미리 잘라 둔 경계 덕에, 실수의 반경이 명령 하나에서 멈췄다.

두 사고 모두에서 배운 것은 같다. 목표는 실수가 없는 구조가 아니라 실수가 싸게 끝나는 구조다.

결과와 비용

결과. PR 두 개가 당일 main에 머지됐고 프로덕션에 배포됐다. 피드는 정상 응답을 돌려주고, 아티클에는 저자 신원과 가이드 크로스링크가 붙었다. 이 글 자체가 그 산출물 위에 서 있다. 골격은 그날 만든 스캐폴더로 생성했고, 발행 절차는 그날 만든 체크리스트를 따랐다.

비용. 로그 기준으로 개발 세션은 트랙당 6만에서 14만 토큰, 검증 세션은 2만 7천에서 7만 2천 토큰을 썼다. 검증 루프 전체를 얹어도 개발 비용의 절반 정도가 추가되는 셈이다. 반대편에 놓을 것은 재작업의 값이다. 잘못된 메타데이터가 배포된 걸 나중에 발견하고, 원인을 찾고, 고치고, 다시 배포하는 비용은 그 절반보다 싸지 않다. 무엇보다 검증은 배포 전에 끝나고, 재작업은 배포 후에 시작된다.

사람이 쓴 시간은 방향 결정과 승인 몇 번이 전부다. 검증 루프가 없었다면 그 시간은 디버깅으로 바뀌었을 것이다.

내일 시작한다면

이 파이프라인의 최소 구성은 여덟 줄로 줄일 수 있다.

  1. 플랜을 파일로 만들어 저장소에 커밋한다. 트랙마다 소유 파일을 명시한다.
  2. 트랙별 워크트리를 만들고, 에이전트에게 커밋을 금지한다.
  3. 개발 프롬프트는 계약으로 쓴다. 목표, 금지 사항, 완료 전 확인, 보고 형식.
  4. 워치독과 동시 실행 상한을 건다. 자원 한계는 숨길 일이 아니라 설계 입력이다.
  5. 개발이 끝나면 새 세션으로 검증한다. "자기 보고를 신뢰하지 말라"로 시작하고 VERDICT 형식을 강제한다.
  6. 검사 코드를 받았다면 일부러 고장을 내 실패를 확인하게 한다. 실패해 본 적 있는 테스트만 믿는다.
  7. FAIL이 나오면 개발 세션을 재개해 고치고, 다시 검증한다.
  8. 머지 전에 트랙 사이의 틈과 스펙의 구멍을 통합 리뷰로 본다.

지난 글의 네 가지 설계로 보면 이 파이프라인은 새로운 것이 아니다. 계약형 프롬프트가 완료 조건을 정의하고, 소유권과 워크트리가 하네스를 만들고, 회의적 검증이 루프를 닫는다. 에이전트의 "다 했습니다"는 완료 선언이 아니라 검증 시작 신호다. 증거가 도착할 때까지, 일은 끝나지 않았다.

참고 자료

더 깊이 보기

이 파이프라인의 원리를 가이드에서 이어서 읽어보세요.