홈시리즈멘토링

© 2026 정기창. All rights reserved.

본 블로그의 콘텐츠는 CC BY-NC-SA 4.0 라이선스를 따릅니다.

☕후원하기소개JSON Formatter러닝 대기질개인정보처리방침이용약관

© 2026 정기창. All rights reserved.

콘텐츠: CC BY-NC-SA 4.0

☕후원하기
소개|JSON Formatter|러닝 대기질|개인정보처리방침|이용약관

사람 손 없이 PR→머지→배포: AI 에이전트 자동 연쇄를 설계하며 배운 것

정기창·2026년 5월 30일

AI 코딩 에이전트로 개발 워크플로우를 자동화하다 보면 묘한 상태에 도달합니다. 코드 리뷰도, 머지 게이팅도, 배포도 각각은 이미 자동화되어 있는데, 정작 그 사슬을 처음 기동하는 트리거 한 고리만 매번 수동 호출로 남아 있더라는 것입니다. PR을 만든 뒤 리뷰 루프를, 리뷰가 끝나면 머지 게이트를, 그다음 배포를 — 단계마다 제가 다음 단계를 직접 호출해야 했습니다. 검증된 부품을 다 만들어 놓고도, 사슬의 첫 시동만 수동 단계로 남겨둔 셈이었습니다.

이 글은 그 마지막 한 고리를 메우면서 겪은 설계와 시행착오를 정리한 기록입니다. AI 코딩 에이전트(저는 Claude Code를 씁니다)로 자기 개발 워크플로우를 자동화하려는 1인 개발자나 플랫폼 엔지니어를 독자로 가정했습니다. 추상적인 베스트 프랙티스가 아니라, 어디서 막혔고 무엇을 기각했는지를 솔직히 적어 보려 합니다.

검증된 사슬은 다 있는데, 진입점만 수동이었다

제 워크플로우의 뼈대는 이렇습니다. 워크트리에서 기능을 완성하면 PR을 만들고, 맥락 없는 에이전트가 리뷰하고, 피드백을 고치고, 다시 리뷰하는 루프를 돕니다. 이걸 review-loop라고 부릅니다. 라운드를 돌려 치명적/높음 등급 코멘트가 0이 되면, 머지 게이트(pr-merge-gate)가 여러 조건을 점검한 뒤 squash 머지를 하고, 브랜치를 정리하고, 마지막으로 배포 에이전트가 변경된 서비스를 배포합니다.

각 단계는 따로따로 잘 동작했습니다. 문제는 단계 사이의 이음매였습니다. PR을 만든 다음 review-loop을 제가 수동으로 호출해야 다음 단계가 돌기 시작했고, 리뷰가 끝난 뒤에도 머지 게이트를 다시 띄우는 건 제 몫이었습니다. 돌이켜 생각해보면, 검증이 끝난 사슬일수록 진입점이 수동 단계로 남아 있는 게 더 아깝다는 생각이 들었습니다. 부품은 신뢰할 만한데 첫 시동만 수동이니까요.

사슬의 골격과 각 단계의 책임 경계

먼저 사슬 전체를 한눈에 보면 다음과 같습니다. 중요한 것은 각 단계가 자기 책임만 지고, 남의 책임은 건드리지 않는다는 원칙입니다.

PR 생성
  └─ review-loop        : 라운드 반복 리뷰 → 피드백 수정 → 재푸시
       └─ pr-merge-gate : 게이트 통과 검사 → squash 머지
            └─ cleanup  : 브랜치/워크트리 정리
                 └─ deployer : 변경 서비스 매핑 → 순차 배포

이 구조에서 한 가지 의도적으로 지킨 규칙이 있습니다. 게이트는 차단만 하고, 해결은 하지 않는다는 것입니다. 머지 게이트가 "이 PR은 베이스 브랜치보다 뒤처져 있어서 머지할 수 없다"고 판단하면, 게이트는 거기서 멈추고 사람이나 별도 도구에 넘깁니다. 게이트가 직접 리베이스까지 하도록 만들고 싶은 유혹이 컸지만, 그렇게 하면 게이트가 "검사자"이면서 동시에 "수선공"이 되어 책임이 흐려진다는 생각이 들었습니다.

머지 게이트는 무엇을 검사하는가 — 보고를 믿지 않고 GitHub를 믿는다

머지 게이트는 일곱 가지를 순서대로 확인합니다. 단순히 "리뷰 끝났대요"라는 보고 본문을 믿지 않고, GitHub 자체를 ground truth로 삼은 게 핵심입니다.

게이트 확인 내용
① 기본 상태 PR이 열려 있고 머지 가능한 상태인가
② 머지 가능성 mergeable + mergeStateStatus가 CLEAN 또는 UNSTABLE인가
③ CI 상태 체크 결과가 SUCCESS / NEUTRAL / SKIPPED 범위 안인가
④ 리뷰 스레드 치명적/높음 스레드가 실제로 resolved인가 (GraphQL로 직접 확인)
⑤ UI 평가 UI 변경 PR이면 시각/경험 평가의 P0가 0인가
⑥ 외부 커밋 "수정 완료" 코멘트 이후 들어온 외부 커밋으로 stale해지지 않았는가
⑦ 직전 재확인 머지 버튼을 누르기 직전 race condition 재확인

가장 신경 쓴 것은 ④번입니다. 리뷰 에이전트가 "치명적 코멘트 모두 해결했습니다"라고 보고해도, 그 문장을 그대로 믿지 않았습니다. 대신 GitHub GraphQL API의 reviewThreads.isResolved 필드를 직접 조회해서, 스레드가 정말로 닫혔는지를 확인했습니다.

gh api graphql -f query='
  query($owner:String!, $repo:String!, $pr:Int!) {
    repository(owner:$owner, name:$repo) {
      pullRequest(number:$pr) {
        reviewThreads(first:100) {
          nodes { isResolved }
        }
      }
    }
  }' -f owner=OWNER -f repo=REPO -F pr=123

에이전트의 자기 보고를 신뢰 경계로 삼으면, "고쳤다고 말했지만 실제로는 스레드가 열려 있는" 경우를 거를 수 없습니다. 솔직히 말하면 이 차이를 처음부터 안 것은 아니고, 보고만 믿고 게이트를 통과시켰다가 어긋난 경험을 한 뒤에야 보고 본문은 신뢰하지 않고 플랫폼 상태를 직접 조회한다는 원칙을 박았습니다.

한 가지 덧붙이면, 이 게이트를 도입하면서 기존에 갖고 있던 "자동 머지 금지"라는 규칙을 명시적으로 뒤집어야 했습니다. 다만 규칙을 그냥 지운 게 아니라, "머지 게이트가 일곱 관문을 통과시킨 경우에 한해 자동 머지를 허용한다"는 식으로 이력을 보존하며 정책을 갱신했습니다. 과거의 나에게도 이유가 있었을 테니, 그 흔적을 지우지 않는 편이 낫다는 생각이 들었습니다.

마지막 한 고리 메우기 — 룰 박스 vs hook vs wrapper

본론으로 돌아오면, 메우고 싶었던 건 "PR을 만든 직후 별도의 수동 호출 없이 review-loop이 자동으로 시작되는" 고리였습니다. 구현 방법으로 세 가지를 두고 고민했습니다.

방식 동작 한계
A. 룰 박스 에이전트 지침에 "PR 생성 직후 자동으로 리뷰 루프를 호출한다"는 규칙을 명문화 에이전트가 매 턴 규칙을 따른다는 보장에만 의존
B. Stop hook 도구 실행 이벤트를 가로채는 결정적 hook으로 트리거 hook이 등록되지 않은 환경에선 동작 안 함
C. wrapper 스킬 PR 생성을 감싸는 별도 스킬로 흐름을 강제 매번 그 스킬을 거쳐야 함 (우회 시 무력)

결론은 A와 B의 이중 안전망이었습니다. 룰 박스(A)를 단일 ground truth로 두어 에이전트가 매 턴 스스로 트리거를 책임지게 하고, hook(B)은 그것을 보강하는 백업으로 깔았습니다. 어느 하나가 빠진 환경에서도 사슬이 굴러가도록, 그러나 둘이 동시에 발동해도 무해하도록(멱등) 설계한 셈입니다. hook을 ground truth로 삼지 않은 이유는, hook이 등록 안 된 환경에서도 에이전트가 규칙만으로 트리거할 책임을 지게 하고 싶었기 때문입니다.

충돌은 누가 책임지나 — 전담 에이전트를 기각한 이유

머지 게이트가 ②번에서 "베이스보다 뒤처짐(BEHIND)" 또는 "충돌(DIRTY)"을 만나면 차단만 합니다. 그렇다면 충돌 해결은 누가 할까요. 처음엔 충돌만 전담하는 에이전트를 따로 두는 안을 진지하게 검토했습니다. 그런데 두 가지 기준으로 따져보니 기각하는 게 맞겠다는 결론에 이르렀습니다.

첫째는 blast-radius(영향 반경)입니다. 충돌이라고 다 같은 충돌이 아니었습니다.

충돌 유형 위험도
lock 파일, 포매터 차이 안전 (기계적 해결 가능)
공유 타입 인터페이스, ORM 스키마, 이중 쓰기 코드 위험 (의미를 모르면 깨뜨림)

둘째는 빈도입니다. 위험한 충돌은 자주 일어나지 않았습니다. 자주 일어나지도 않는 위험 작업을 위해 전담 에이전트라는 상시 구조를 세우는 건 과한 투자라는 생각이 들었습니다. 그래서 충돌 해결은 별도의 일괄 리베이스 도구에 맡기고, 그 도구가 단순한 충돌은 스스로 풀되 위험한 충돌은 멈추고 사람에게 넘기도록 경계를 그었습니다. 게이트는 차단, 리베이스 도구는 안전한 것만 해결, 위험한 건 사람. 책임이 세 곳으로 깔끔히 나뉘었습니다.

'완료'가 곧 '배포 시점'은 아니다 — 배포 보류 게이팅

사슬을 다 이어놓고 나서 새로 깨달은 게 있습니다. PR이 머지됐다고 해서 항상 즉시 배포하면 안 된다는 것입니다. 예를 들어 하나의 작업을 여러 단계로 쪼갠 마이그레이션 시리즈를 진행할 때, 중간 단계 PR이 머지될 때마다 배포가 돌면 미완성 상태가 운영에 나갈 수 있습니다.

그래서 "배포 보류" 신호를 사슬에 흘려보내는 장치가 필요했습니다. 이걸 세 개의 층으로 분리한 게 의외로 중요했습니다.

policy-body  : 규칙 본문 (보류의 정의와 조건)
   ↑ 참조
forwarder    : 리뷰 루프가 마커를 다음 단계로 전달
   ↑ 전달
consumer     : 머지 게이트가 마커를 읽고 배포를 건너뜀

마커는 명시적인 토큰(예: --skip-deploy)으로도 줄 수 있고, "남은 PR이 있어서 배포는 보류해줘" 같은 자연어 신호로도 동등하게 처리되도록 했습니다. 사람은 매번 정해진 토큰을 외워 쓰지 않으니까요. 마커를 한 곳에만 박지 않고 소비자/전달자/정책 본문 세 층으로 나눈 덕분에, 한 층을 고쳐도 나머지가 흔들리지 않았습니다.

갭 보강 — 머지 후 stale main, 그리고 추측을 소스로 교차검증

사슬을 운영하다 보니 미처 보지 못한 틈이 드러났습니다. 워크트리에서 작업하면 메인 레포 디렉토리는 PR이 머지된 뒤에도 옛 상태로 남아 있습니다. 다음 작업을 메인에서 시작하면 stale한 코드 위에서 출발하게 됩니다. 이걸 자동으로 메우되, 안전 조건을 깐 가드를 넣었습니다.

# 머지 후 메인 디렉토리 동기화 (안전 조건 충족 시에만)
# 1) 워크트리 목록에서 메인 디렉토리를 자동 탐지
# 2) 체크아웃이 main 이고, uncommitted 변경이 0일 때만
# 3) fast-forward 만 허용 (병합 커밋/충돌 방지)
git -C "$MAIN_DIR" pull --ff-only origin main

조건을 단 이유는 분명합니다. 작업 중이거나 다른 브랜치에 있는 디렉토리를 함부로 당기면 오히려 상태를 망가뜨립니다. --ff-only로 제한해 충돌이나 예상치 못한 병합 커밋이 생길 여지를 없앴습니다.

또 하나 솔직하게 적어둘 일이 있습니다. 배포 단계의 외부 API를 다룰 때, 운영에 영향을 줄까 우려해 실제로 호출해보지 못한 부분이 있었습니다. 처음엔 추측으로 호출 방식을 박아두었는데, 그게 영 찜찜했습니다. 그래서 그 PaaS용으로 공개된 MCP 패키지의 npm 소스를 직접 읽어 교차검증했습니다. 그 결과 제가 잘못 알고 있던 것들이 드러났습니다.

  • 배포 트리거는 POST가 아니라 GET 요청이었습니다.
  • 애플리케이션 식별자(uuid)와 배포 식별자(uuid)는 서로 다른 값이었습니다.
  • 완료 상태값은 finished / failed였습니다.

추측으로 박아둔 부분을 공개 소스로 맞춰 보정한 이 과정이, 이번 작업에서 가장 다행스러운 순간이었습니다. 부끄럽지만, 모르는 것을 모른다고 두지 않고 소스로 확인하는 습관이 결국 사슬의 신뢰성을 지킨다는 것을 다시 느꼈습니다.

멱등성과 제외 키워드 가드

자동 트리거를 깔 때 가장 두려운 것은 오발동입니다. 사람이 "리뷰는 나중에", "draft로 두자", "이건 WIP야"라고 했는데도 사슬이 멋대로 굴러가면, 자동화는 신뢰를 잃습니다. 그래서 제외 키워드 가드를 두었습니다.

  • draft / WIP / "초안" 표시가 있으면 자동 트리거를 건너뜁니다.
  • "리뷰는 나중에", "PR만 만들고 멈춰" 같은 자연어 신호도 동등하게 존중합니다.
  • 키워드 탐색 범위는 PR을 만든 턴과 직전 한 턴으로 한정했습니다. 그보다 오래된 맥락까지 끌어오면 옛 지시가 현재를 덮어버리기 때문입니다.

그리고 멱등성. 룰 박스와 hook이 동시에 발동하더라도, 이미 진행 중인 리뷰 루프를 중복으로 띄우지 않도록 했습니다. 자동화에서 "한 번 더 발동해도 무해함"은 옵션이 아니라 전제 조건이라는 생각이 들었습니다.

돌이켜보며 — 1인 개발 자동화에서 배운 것

이 작업을 마치고 나서 가장 크게 남은 교훈은, 검증된 부품을 만드는 일과 그 부품을 자동으로 굴리는 일은 전혀 다른 문제라는 것입니다. 제가 직접 만들어 둔 review-loop도, 머지 게이트도, 배포 단계도 각각은 오래전에 완성돼 있었습니다. 그런데도 사슬은 굴러가지 않았습니다. 마지막 트리거 한 고리가 수동 호출 단계로 남아 있었기 때문입니다.

그 고리를 메우는 과정은 화려한 기능 추가가 아니었습니다. 오히려 책임의 경계를 다시 긋고(게이트는 차단만 한다), 신뢰 경계를 다시 정하고(보고가 아니라 플랫폼을 믿는다), 안전 조건을 까는(ff-only, 제외 키워드, 멱등성) 지루한 작업의 연속이었습니다. 다만 그 지루함이야말로 자동화를 믿고 맡길 수 있게 만드는 토대라는 것을, 이번에 다시 확인했습니다.

1인 개발자에게 자동화는 단순한 시간 절약이 아니라 판단의 외주입니다. 그렇다면 무엇을 외주하고 무엇을 손에 쥘지가 가장 중요한 결정이 됩니다. 결정적인 호출(머지, 배포 트리거)은 박제하되, 충돌의 위험도를 판단하거나 배포를 보류할지 정하는 것처럼 맥락이 필요한 판단은 끝까지 사람과 에이전트의 몫으로 남겨두는 것. 그 경계를 정직하게 긋는 일이, 결국 자동화 사슬을 안심하고 굴리는 유일한 방법이 아닐까 하는 생각이 들었습니다.

Claude CodeAI 에이전트멀티에이전트자동 배포CI/CD1인 개발

관련 글

Claude Code 라우팅 매트릭스를 글로벌 CLAUDE.md 와 SKILL.md 에 박은 회고

Claude Code sub-agent 라우팅 매트릭스를 글로벌 CLAUDE.md 와 review-loop SKILL.md 두 곳에 박은 회고입니다. 같은 매트릭스를 인라인 복제하지 않은 이유와 hook 까지 가지 않은 단계적 결정 근거를 함께 적었습니다.

관련도 91%

Claude Code 라우팅 매트릭스 빈 행 3개를 새 전문 에이전트로 메운 회고

Claude Code 라우팅 매트릭스를 글로벌 CLAUDE.md 와 review-loop SKILL.md 에 박은 직후, 24행을 다시 들여다보니 fallback 으로 흘러갈 수밖에 없던 도메인 세 개가 어렵지 않게 떠올랐습니다. devops-engineer · dba · test-data-verifier 세 전문 에이전트를 더하며 만난 정의 작업의 무게를 정리한 후속편입니다.

관련도 90%

Claude Code 하네스: 1인 개발자가 AI 에이전트 팀을 만드는 법

1인 개발자가 Claude Code로 전문가 AI 팀을 구성한 경험. 에이전트, 스킬, 오케스트레이터로 이루어진 하네스 구조와 모노레포 풀스택 개발 자동화 사례를 공유합니다.

관련도 90%