doksam.com
블로그Claude Code 실전 가이드 5편

Claude Code 실전 가이드 5편 — 이슈에서 배포까지 자율주행

2026-08-04claude-code자동화ci-cd워크플로

Claude Code 실전 가이드 마지막 5편입니다. 1편에서 시작하기와 CLAUDE.md, 2편에서 컨텍스트 관리, 3편에서 서브에이전트와 병렬 처리, 4편에서 MCP 로 인프라를 도구화하는 이야기를 했습니다. 이번 편은 그 넷을 한 루프로 묶어서, 이슈 하나가 등록되는 순간부터 실제로 배포되어 URL 로 확인되는 순간까지를 얼마나 사람 개입 없이 돌릴 수 있는지 — 그리고 어디서는 반드시 사람이 서명해야 하는지를 정리합니다.

실제로 도는 루프

제가 지금 쓰는 루프는 대략 이렇습니다.

  1. GitLab 이슈 등록 (4편에서 다룬 GitLab MCP, 또는 직접 웹에서)
  2. 정식 클론이 아니라 별도 git worktree 를 파서 이슈 번호가 박힌 브랜치 생성
  3. 구현
  4. 타입체크·빌드 로컬 검증
  5. 커밋 (이슈 번호 참조)
  6. MR 생성, 본문에 Closes #N 포함
  7. CI 파이프라인을 끝까지 폴링
  8. 통과하면 squash 머지
  9. 머지 후 배포 파이프라인이 따로 있으면 그것도 끝까지 폴링
  10. 배포된 실물에서 방금 머지한 커밋이 서비스되고 있는지 확인
  11. 워크트리 정리 (worktree remove + 로컬 브랜치 삭제)
  12. 완료 알림 (알림 큐에 insert, 텔레그램으로 도착)

이 열두 단계 중 사람이 미리 승인해야 하는 건 사실 처음 한 번뿐입니다. "이 이슈 해도 됨" 이라는 신호가 떨어지면, 그 뒤로는 실패해서 되돌아오지 않는 한 사람이 다시 개입할 지점이 없습니다. 그런데 이 마지막 문장이 이번 편의 핵심입니다 — "실패해서 되돌아오지 않는 한" 을 보장하는 게 자율주행에서 제일 어려운 부분입니다.

사람이 서명해야 하는 지점

몇 번 사고를 겪고 나서 정리한 경계입니다.

시작 승인은 사람 — 이슈를 만드는 것과 그 이슈에 착수하는 것은 별개입니다. 이슈 등록까지는 자동으로 해도 되지만, "이거 지금 진행해" 라는 명시적 신호(번호 지정이든 "ㄱㄱ" 든) 없이 바로 코드를 건드리기 시작하면 안 됩니다. 특히 범위가 처음 이슈보다 커질 것 같으면(파일 몇 개 고칠 줄 알았는데 스키마까지 건드려야 한다든가) 거기서 한 번 더 확인받는 게 낫습니다. 자동 진행 자체가 문제가 아니라, 스코프가 조용히 불어나는 걸 사람이 모르는 채로 넘어가는 게 문제입니다.

파괴적 작업은 항상 사람git push --force, git reset --hard, 프로덕션 DB 의 삭제성 쿼리, 서비스 강제 재시작 같은 건 루프 안에 있어도 자동 실행 대상이 아닙니다. 되돌릴 수 없는 조작과 되돌릴 수 있는 조작을 구분해서, 후자만 자율주행 범위에 넣는 게 기준입니다.

prod 인프라 변경은 정확한 명령을 사람에게 넘기는 게 기본 — 셀프호스팅 환경이다 보니 "배포" 라는 단어 안에 실제로는 서버 설정 변경, 리버스프록시 룰 수정처럼 되돌리기 까다로운 작업이 섞여 있을 수 있습니다. 애플리케이션 코드 배포 파이프라인은 CI 가 검증하고 자동으로 흘러가도 되지만, 그 파이프라인 자체를 바꾸는 일(새 서비스 추가, nginx 설정, systemd 유닛 변경)은 별도 확인 대상으로 뗍니다.

맡겨도 되는 지점

반대로 실패해도 되돌리기 쉽고, 검증 가능한 구간은 사람이 매번 지켜볼 필요가 없습니다.

  • 브랜치 생성, 커밋, MR 생성 — 전부 로컬/원격에서 되돌리기 쉬운 조작입니다.
  • 타입체크·빌드·린트 — 실패해도 그냥 실패 상태로 남을 뿐 아무것도 망가뜨리지 않습니다.
  • CI 파이프라인 대기 — 폴링 자체는 읽기 전용 조회입니다.
  • squash 머지 — MR 은 되돌릴 수 있고(revert), 머지 커밋 하나로 이력이 깔끔하게 남습니다.
  • 워크트리 정리 — 로컬 작업 디렉터리 정리는 원격에 아무 영향이 없습니다.
  • 완료 알림 큐 insert — 알림 테이블에 행 하나 넣는 것뿐이라 위험이 없습니다.

이 구분의 기준은 결국 하나입니다. "잘못됐을 때 원상복구가 쉬운가." 쉬우면 자동으로 흘러가게 두고, 어려우면 그 앞에서 멈춥니다.

자율주행이 무너지는 전형적 패턴

몇 번 겪어본 실패 패턴들입니다.

검증 없이 완료 보고

CI 큐에 파이프라인이 등록된 걸 보고 "MR 올렸습니다" 라고 끝내버리는 패턴입니다. 파이프라인은 등록된 것과 통과한 것이 다른데, 등록만 확인하고 손을 떼면 그 뒤에 타입 에러나 테스트 실패로 막혀 있어도 아무도 모릅니다. CI 는 트리거한 뒤 끝까지 폴링해서 성공 또는 실패를 실제로 확인해야 합니다. 특히 squash 머지 이후 별도 배포 파이프라인이 있는 구조라면, MR 의 head 커밋 SHA 로 폴링하면 안 됩니다. squash 머지가 끝나면 기본 브랜치에는 새로운 커밋 SHA 가 하나 생기고, 배포 파이프라인은 그 SHA 를 기준으로 돌기 때문에 head SHA 로만 계속 지켜보면 검증 단계만 성공으로 보이고 배포는 영원히 안 뜹니다.

폴링 방식 자체도 한 번 데인 적이 있습니다. 상태 조회 명령을 매번 새 프로세스로 띄우는 셸 루프는 중간에 한 번 실패하면 거기서 끊깁니다. 조회와 종료 조건 판정을 한 프로세스 안에서 도는 스크립트로 만들어두는 편이 안정적이었습니다. 그 스크립트를 백그라운드로 돌릴 때도 셸에서 완전히 떼어내면(disown) 세션 쪽에서 종료를 감지하지 못해 "다 됐다" 는 신호가 영영 안 옵니다. 폴링은 붙잡아두고 끝을 봐야 하는 작업입니다.

머지됐다고 이슈가 자동으로 닫혔다고 단정

MR 본문에 Closes #N 을 넣어두면 머지 시점에 이슈가 자동으로 닫힙니다. 제 셀프호스팅 GitLab 에서도 실제로 자동 클로즈가 동작하는 걸 확인했습니다. 문제는 이 동작이 트래커 설정과 머지 방식에 따라 달라질 수 있다는 점이고, 한 번 확인한 결과를 그 뒤로 계속 사실로 들고 다니는 게 위험하다는 겁니다. 그래서 루프에는 "머지 직후 이슈 상태를 조회한다" 를 그대로 남겨뒀습니다. 닫혀 있으면 거기서 끝이고, 열려 있으면 그 자리에서 코멘트를 남기고 수동으로 닫습니다. 확인 한 번에 드는 비용은 조회 한 번인데, 안 하면 처리된 이슈가 열린 채로 조용히 쌓입니다.

브랜치 확인 없이 main 에 커밋

세션 도중 어떤 자동화 절차가 기본 브랜치로 checkout+pull 을 해버리면, 작업 브랜치를 만들어놓고도 커밋이 기본 브랜치 위에 얹힙니다. 3편에서 다룬 worktree 격리를 지키면 이 사고는 구조적으로 안 생기지만, 루프 관점에서 중요한 건 그 다음입니다. 자율주행 루프는 "커밋 → MR → CI" 가 한 호흡으로 이어지기 때문에, 커밋 대상이 틀렸다는 사실이 CI 가 엉뚱한 브랜치에서 돌기 시작한 뒤에야 드러납니다. 그래서 커밋 직전에 현재 브랜치를 한 번 확인하는 단계를 루프 안에 넣어뒀습니다. 사람이 지켜보는 작업이면 눈에 띄었을 실수가, 무인 구간에서는 몇 단계 뒤에야 드러난다는 게 자율주행의 대가입니다.

SHA 가 다른 걸 보고 유실됐다고 단정

squash 머지를 하면 작업 브랜치의 커밋들이 기본 브랜치에서는 새 커밋 하나로 합쳐집니다. 이때 원래 커밋 SHA 는 기본 브랜치 이력에 존재하지 않으니, SHA 비교나 git log A..B 만 보고 "커밋이 사라졌다" 고 판단하면 멀쩡한 머지를 되돌리는 사고로 이어집니다. SHA 가 다르다는 건 SHA 가 다르다는 뜻일 뿐이고, 확인해야 하는 건 내용입니다. git show --stat <머지커밋> 으로 파일 목록을 보고, 의심되면 실제 파일 내용을 열어서 변경분이 들어갔는지 확인하는 게 맞습니다.

낡은 전제를 확인 없이 사용

2편에서 영속 메모리가 낡는다는 이야기를 했는데, 무인 루프에서는 그 대가가 더 큽니다. "이 레포의 배포는 이 브랜치에서 돈다", "이 서비스는 이 포트를 쓴다" 같은 전제가 하나 어긋나면, 사람이 중간에 화면을 보고 있지 않으니 그 전제 위에서 몇 단계가 더 진행된 뒤에야 틀렸다는 게 드러납니다. 루프를 돌리기 전에 이번에 쓸 전제 한두 개(배포 대상 브랜치, 확인할 엔드포인트)는 파일이나 설정으로 한 번 확인하고 들어가는 편이 결국 빠릅니다.

실물 확인이 마지막 관문인 이유

CI 가 통과하고 배포 파이프라인도 성공으로 끝났다고 해서 작업이 끝난 게 아닙니다. 캐시가 안 갱신됐거나, 리버스프록시가 이전 컨테이너를 계속 물고 있거나, 정적 자산 경로가 어긋나 있는 경우가 실제로 있었습니다. 파이프라인의 성공 상태는 "배포 절차가 끝까지 실행됐다" 는 증거이지 "지금 서비스되는 게 내 변경분이다" 는 증거가 아닙니다.

그래서 루프의 마지막 검증은 배포된 실물에서 직접 얻은 증거로 둡니다. 사람이라면 브라우저로 열어보고 끝낼 일이지만, 무인 루프에는 눈으로 보는 단계가 없으니 기계가 판정할 수 있는 형태여야 합니다. 다음 절이 그걸 어떻게 만들어뒀는지에 대한 이야기입니다.

실사례 — 이 블로그 레포의 한 바퀴

지금 이 글이 올라와 있는 레포가 그 루프를 그대로 씁니다. GitLab CI 는 스테이지가 둘뿐입니다. validate 에서 타입체크·빌드·테스트를 돌리고, 기본 브랜치에 푸시가 들어왔을 때만 deploy 가 붙습니다. 배포 스크립트는 운영 호스트의 러너 위에서 컨테이너 이미지를 빌드하고, Quadlet 유닛을 설치한 뒤 사용자 systemd 서비스를 재시작하고, 마지막에 헬스 엔드포인트가 200 을 돌려줄 때까지 기다립니다. 여기까지 실패하면 배포 잡 자체가 실패로 끝나서 루프가 그 자리에 멈춥니다.

무인 루프에서 실제로 도움이 된 건 이미지 빌드 인자로 커밋 SHA 를 넣고, 애플리케이션이 그걸 헬스 응답에 그대로 노출하게 만든 부분입니다.

curl -s http://127.0.0.1:<포트>/api/health
# {"status":"ok","uptime":41.6,"version":"001fb9a"}

version 이 방금 머지된 커밋 SHA 와 같으면, 지금 트래픽을 받고 있는 컨테이너가 내 변경분으로 빌드된 것이라는 게 한 줄로 증명됩니다. "파이프라인이 초록색이다" 와는 증명력이 다릅니다. 배포 스크립트 자체가 이 엔드포인트를 재시도하며 기다리기 때문에, 컨테이너가 뜨다 말았거나 이전 이미지가 그대로 살아 있는 상황은 잡 실패로 드러납니다.

실제 작업 하나가 이 루프를 도는 모습은 이렇습니다. 기술스택 글에 레포 전체 언어 분포를 덧붙이는 이슈(#13)를 등록하고, feat/13-blog-tech-stack-repos 브랜치를 워크트리로 따서 글을 고치고, 커밋 제목에 이슈 번호를 박고, MR 을 올려 validate 통과를 확인한 뒤 squash 머지합니다. 머지되면 기본 브랜치에서 deploy 가 돌고, 헬스 응답의 version 이 새 SHA 로 바뀐 걸 확인하고, 그 자리에서 워크트리와 로컬 브랜치를 지우고, 알림 큐에 완료 레코드를 넣습니다. 사람이 개입한 지점은 맨 처음 "이거 진행" 한 번입니다.

알림은 나중에 읽어도 이해되게 쓴다

루프의 마지막 단계인 알림은 사소해 보이지만 실제로 제일 자주 손본 부분입니다. 초기에는 "배포 완료" 같은 한 줄만 큐에 넣었는데, 몇 시간 뒤 텔레그램에서 그 알림을 보면 무슨 작업이었는지 알 수가 없었습니다. 그 세션의 대화 맥락이 없는 상태에서 읽히는 문장이라는 걸 감안하지 않은 겁니다.

지금은 알림 하나에 무슨 작업이었는지(제목), 어느 레포의 어느 이슈·MR 이었고 결과가 무엇인지, 남은 후속 조치가 있는지를 줄로 나눠 담습니다. 약어나 내부 코드명은 풀어서 씁니다. 무인으로 도는 루프일수록 사람이 보는 창구는 이 알림 하나뿐이라, 여기서 요약이 부실하면 자동화의 결과를 사람이 확인할 방법이 사라집니다.

시리즈를 관통하는 원칙

다섯 편을 쓰면서 반복해서 확인한 건 결국 하나입니다. 에이전트에게 넘겨도 되는 일과 사람이 서명해야 하는 일의 경계는 "되돌릴 수 있는가" 로 갈린다는 것입니다.

  • CLAUDE.md(1편)는 그 경계와 규율을 매 세션 처음부터 다시 설명하지 않기 위한 장치입니다.
  • 컨텍스트 관리(2편)는 그 경계를 판단할 때 필요한 정보가 실제로 에이전트 눈앞에 있게 만드는 일입니다.
  • 서브에이전트와 병렬 처리(3편)는 독립적인 일을 동시에 처리해 속도를 내되, 서로 다른 작업이 같은 자리를 밟지 않게 격리하는 문제입니다.
  • MCP(4편)는 에이전트가 추측이 아니라 실제 도구 호출로 사실을 확인하게 만드는 통로입니다.
  • 이번 5편의 자율주행 루프는 그 넷을 다 갖췄을 때 비로소 "이슈에서 배포까지" 를 사람 개입 최소로 돌릴 수 있다는 걸 보여주는 자리이면서, 동시에 그 자동화가 무너지는 지점들 — 검증 생략, 상태 오판, 낡은 가정 — 을 어디서 끊어야 하는지 정리하는 자리였습니다.

자율주행이라는 말이 주는 인상과 달리, 실제로 안정적으로 도는 루프는 "사람 없이 다 한다" 가 아니라 "사람이 봐야 할 지점만 정확히 남기고 나머지를 걷어낸다" 에 가깝습니다. 그 경계를 코드와 CLAUDE.md 에 명시적으로 박아두는 일이, 다섯 편에 걸쳐 결국 하고 싶었던 이야기입니다.

이 시리즈의 다른 글

시리즈 전체