11. Release gates — '완료'가 사실이 되는 선
Release gate가 무엇인지, 게이트 없이 '완료'가 부패하는 경로, Auto/Manual gate 분리, 누적 게이트 패턴 실전, lint·build·test 통과 트랙이 42항목 부족했던 날의 교훈.
10편 PRD는 한 문장으로 끝났다. PRD는 도착점을 기술한다.
이 글은 운영자가 어떻게 도착했는지 아는가에 대한 글이다.
Release gates. "완료"를 느낌이 아니라 사실로 바꾸는, 구체적이고 원자 단위이고 기계로 검증 가능한 조건들. 게이트가 없으면 PRD는 바람이다. 있으면 마일 마커가 박힌 길이 되고, 각 마커는 운영자의 자신감 외의 무언가가 검증한다.
0. 출발 전제 — "완료"는 가장 비싼 단어다
7편 검증 루프가 이 주장을 코드 레벨에서 했다. 운영자가 "끝났다"고 느끼는 순간, 보고가 나가기 전에 lint·빌드·테스트가 자동으로 돈다. 그 움직임이 깨지기 쉬운 단어 — "완료" — 를 운영자의 기억에서 시스템 강제로 이동시킨다.
Release gate는 같은 움직임이다. 한 단계 위에서.
코드 레벨에서 "완료"는 이 조각이 컴파일되고 테스트가 통과한다는 뜻이다. Release 레벨에서 "완료"는 시스템이 라이브로 갈 자격이 있다는 뜻이다. 두 단어는 같은 단어가 아니고, 같은 메커니즘으로 강제할 수도 없다.
테스트가 통과해도 privacy policy가 존재한다는 것을 알려주지 않는다. 빌드가 통과해도 OAuth redirect URI가 whitelist에 올라 있는 것을 알려주지 않는다. lint가 깨끗해도 운영자의 실명이 메타데이터 모든 레이어에서 제거됐는지 알려주지 않는다. 그 질문들은 다른 게이트의 것이다.
1. Release gate가 실제로 무엇인가
Release gate는 표다.
표의 각 행은 원자 단위, 외부적으로 검증 가능한 한 가지 조건이다. 각 행에 상태가 있다.
✅ 통과— 검증됨, 현재 이 행이 성립⏳ 진행 중— 작업 중, 아직 성립 안 함🔒 운영자 대기— 사람만 할 수 있는 단계에서 막힘 (Dashboard 클릭, 계정 가입, DNS 변경)❌ 미시작— 알려진 요구, 움직임 없음—해당 없음 — 추측으로 추가된 행이고 상황상 무의미해짐
표는 PRD의 일부다. 별도 문서에 살지 않는다. 이유는 10편 §4와 같다 — 길의 모양이 도착점 문서에 내장되어야 한다. "무엇이 완료로 카운트되는가"를 묻는 사람이 없어진다. 문서가 답한다.
게이트들은 누적이다. 게이트 1은 게이트 0이 통과한 상태를 가정한다. 게이트 9는 0~8이 통과한 상태를 가정한다. 순서는 선호가 아니라 의존이다 — 도메인 없이 서비스를 라이브로 못 띄우듯, 컴플라이언스(게이트 1) 없이 인프라(게이트 2)를 통과할 수 없다.
Release gate는 체크리스트가 아니다. 의도와 실재 사이의 계약이다.
2. Auto-Gate와 Manual-Gate
모든 게이트 행이 같은 종류는 아니다. 두 가지가 있고, 차이가 중요하다.
Auto-Gate 행은 기계가 검증할 수 있는 조건이다. 예:
pnpm build가 0으로 종료pnpm lint가 0으로 종료pnpm test가 0으로 종료grep -r "운영자-실명" .이 아무것도 반환 안 함curl -I https://devalice.jaceclub.com이 200 반환verify:assets스크립트가 15/15 SHA-256 매칭 보고
행이 0 또는 0이 아닌 값으로 종료하는 셸 명령으로 표현될 수 있다면 Auto-Gate 집합에 속한다. 시스템이 매 commit, 매 push, 매일 밤 cron마다 발동할 수 있다 — 운영자가 깨어 있지 않아도.
Manual-Gate 행은 사람만 결정할 수 있는 조건이다. 예:
- "Privacy policy가 두 언어 모두 올바르게 읽힌다"
- "리드가 M2 마일스톤 정의를 승인한다"
- "시드 컨텐츠가 다섯 가지 별개의 사용자 통점을 다룬다"
- "OAuth 스코프가 신뢰 수준에 적절해 보인다"
- "Lighthouse 'Best Practices' 지적이 받아들일 만한 trade-off다"
이 행들은 사람을 기다린다. 게이트에서 자동화될 수 없는 부분이고, 자동화된 척하는 것이 release 사고 대부분의 원천이다.
규율:
| 규율 | 이유 |
|---|---|
| 모든 Auto-Gate 행에는 스크립트가 있고 — CI가 그것을 돌린다 | 아니면 행은 종이에만 있고 실재에는 없다 |
| 모든 Manual-Gate 행은 누가 승인하는지 명시 — "approved"만 쓰지 않는다 | 아니면 아무도 승인하지 않은 채 통과로 카운트된다 |
| Auto와 Manual은 표에서 시각적으로 구분된다 | 운영자가 어느 쪽인지 외우지 않아도 되어야 한다 |
| Manual 승인은 흔적을 남긴다 (commit, comment, PR review) | 기억은 부패한다. commit은 안 한다. |
3. 누적 패턴의 실전
실제 프로젝트 — devalice — 의 실제 모양 게이트 표, 축약형:
게이트 0: M0 출시 (임시 도메인)
- pnpm build 통과 [Auto] ✅
- Vercel preview 배포 [Auto] ✅
- 카테고리 라우트 × 4 [Auto] ✅
- 시드 가이드 × 4 [Manual] ✅ (리드 검토 2026-05-10)
- ESLint clean [Auto] ✅
- Vercel env vars 설정 [Manual] 🔒 (운영자)
게이트 1: 컴플라이언스
- Privacy Policy (ko + en) [Manual] ✅
- 컨텐츠 라이선스 (CC BY) [Auto] ✅ (파일 존재, 헤더 텍스트 매치)
- 코드 라이선스 (MIT) [Auto] ✅
- About 페이지 존재 [Auto] ✅
- 쿠키 안내 (PIPA) [Manual] ✅
게이트 2: 인프라
- 자체 도메인 연결 [Manual] 🔒
- SSL 활성 [Auto] — (게이트 2 행 1에 의존)
- 프로덕션 env 분리 [Manual] 🔒
게이트 3: 보안
- 시크릿 하드코딩 없음 [Auto] ✅ (grep)
- .env.example 완비 [Auto] ✅
- RLS 활성 [Manual] 🔒 (DB push 대기)
- OAuth redirect whitelist [Manual] 🔒
- 입력 검증 [Auto] ✅
- 자산 SHA-256 매칭 [Auto] ✅ (verify:assets 15/15)
...
산문이 못 하는 세 가지를 이 표가 한다.
상태가 한눈에 보인다. 컬럼을 훑는다. ✅ 개수 vs 🔒 개수를 센다. 그림이 즉시 나온다. "우리 어디까지 왔지"를 알기 위해 세 단락 산문을 읽지 않아도 된다.
Blocker가 스스로 드러난다. 게이트 2의 🔒 행은 다음에 일어나야 할 일이다. 표가 운영자·리드·다음 세션에게 무엇을 먼저 봐야 하는지를 말해 준다.
게이트가 스스로 순서를 매긴다. "출시할까?"는 "게이트 N의 마지막 🔒이 해제됐는가?"로 축약된다. 타이밍에 대한 논쟁은 시작되기 전에 끝난다.
4. "완료"가 거짓말이었던 날 — 코드로 강제되지 않은 게이트의 비용
이 이야기는 누군가 "왜 이렇게까지 해야 하느냐"고 물을 때 운영자가 계속 되돌아오는 사례다.
다른 프로젝트 — Track D라고 하자 — 에서 네 개의 sub-phase가 몇 주에 걸쳐 순차적으로 출시됐다. D-0, D-1, D-2, D-3. 각 단계가 마지막에 "success"로 보고됐다. lint 통과. build 통과. cargo test 통과. Lighthouse 점수도 괜찮았다. 팀 리드는 보고를 읽고 다음으로 넘어갔다.
몇 주 뒤, 일상 감사 중에 우리는 실제 결과물이 네 sub-phase에 걸쳐 42항목 부족하다는 것을 발견했다. 버그가 아니다 — 구조적 약속이 누락되어 있었다. spec이 존재할 것이라고 한 UI primitive 4종이 아직 연결되지 않았다. spec이 분해될 것이라고 한 큐 상태 8개가 분해되지 않았다. spec이 삭제될 것이라고 한 파일 7개가 트리에 그대로 있었다. spec이 제거될 것이라고 한 legacy 백엔드 핸들러 9개가 여전히 호출되고 있었다.
어떻게 이런 일이 일어났는가? 모든 보고가 정직했다. 결과물은 컴파일됐다. 테스트는 통과했다. 보고자 — 이 경우 위임받은 에이전트 — 는 무엇이 잘못됐다는 신호를 받을 길이 없었다.
이유는 단순했다. spec의 약속이 산문이었다. 코드로 강제되지 않았다. "spec이 UI primitive 4종을 명시했다 — 트리에 UI primitive 4종이 존재하는가?"를 발동시키는 체크가 없었다. "spec이 파일 X가 삭제됐다고 했다 — 파일 X가 사라졌는가?"를 발동시키는 체크가 없었다. 검증 하니스는 컴파일을 검사했지, 약속을 검사하지 않았다.
해결은 아무도 하기 싫어한 sub-phase였다: D-Catchup. 돌아간다. 각 약속을 트리에 대조한다. 누락된 것을 연결한다. 사라졌어야 할 것을 삭제한다. 그리고 — 이 부분이 오래 가는 변화다 — 이후 모든 위임에 verify:d-N 스크립트를 추가하고, 머지 게이트에 박는다. 다음 네 sub-phase가 거짓말을 하고 싶어도 못 하게.
교훈은 이것이다:
약속은 release gate가 아니다. 코드로 강제된 약속이 release gate다.
D-Catchup 이후, 그 프로젝트의 모든 위임은 같은 PR에 세 가지를 받는다.
- 위임서 — 무엇을, 왜, 어떻게
verify:phase-N.mjs스크립트 — 모든 약속을 순회하고 누락 시 0이 아닌 값으로 종료- CI 규칙 —
pnpm run verify:phase-N이 0으로 종료하지 않으면 머지 차단
위임 단위의 release gate다. PRD의 M0~M10 게이트와 같은 모양, 더 작은 단위에 적용했을 뿐.
5. verify 스크립트의 모양
verify 스크립트는 의도적으로 작고 단순하다. 테스트 프레임워크가 아니다. 문자 그대로 walker다.
spec의 각 행마다 한 블록:
// 행 D-1.3 — 큐는 8개 상태로 분해되어야 함
const queueStates = readQueueStates();
if (queueStates.length !== 8) {
fail("큐가 " + queueStates.length + "개 상태, 8개 기대");
}세 가지 속성이 중요하다.
spec과 1:1. spec의 3번 행은 스크립트의 3번 체크가 된다. spec에 행이 추가되면 스크립트는 체크가 추가되기 전까지 통과하지 않는다. 두 문서가 같이 움직인다.
출력이 영수증. 보고자는 "검증함"이라고 말하지 않는다. raw stdout을 붙인다. verify:d-1 passed 18 / 18. 그 줄이 보고에 없으면 보고가 받아들여지지 않는다.
실패는 시끄럽고 구체적. "큐가 5개 상태, 8개 기대"가 "verify failed"를 이긴다. 운영자는 스크립트를 열지 않고도 무엇이 누락됐는지 알아야 한다.
스크립트는 손으로 쓴다. 생성되지 않는다. 프레임워크가 만들지 않는다. 운영자가 spec을 읽은 결과를 코드로 표현한 것, 머지 게이트 위에 앉아 있는 것이다.
6. Manual-Gate 행 — 정직하게 유지하는 법
Auto-Gate 행은 정직하게 유지하기 쉽다. CI가 체크를 돌리고 통과하거나 실패한다. Manual-Gate 행에서 규율이 드러난다.
운영자가 쓰는 패턴들:
모든 Manual 행은 승인자를 명시. "팀이 승인"이 아니라 역할과 날짜를 명시. Manual ✅ 리드, 2026-05-10. 승인자가 바뀌면 새 승인자가 재승인할 때까지 ⏳로 돌아간다.
Manual 행은 commit 흔적을 남김. ⏳에서 ✅로 행을 뒤집는 commit은 메시지에 승인자 이름과 무엇을 검토했는지 한 줄 메모를 포함. [devAlice] chore(gate-1): privacy policy ko/en — 리드 승인. 6개월 뒤 누군가 "잠깐, 이거 누가 괜찮다고 결정했지?"라고 물으면 git log가 답한다.
Manual 행은 명시적 재검토 주기. M0에서 승인된 privacy policy가 영원히 승인된 것은 아니다. PRD가 재검토 트리거를 명시 — M2 출시 시 재검토 또는 두 분기마다 재검토. 트리거가 발동하면 행은 재확인 전까지 ⏳로 돌아간다.
Manual 행은 "곧 할게"를 상태로 쓰지 않음. 담당자와 마감이 있거나, ❌다. 둘 중 하나.
7. 함정
Release gates는 예측 가능한 방식으로 무너진다.
7.1 커지기만 하고 닫히지 않는 게이트
게이트 3 표가 두 분기 동안 행을 누적하기만 하고, 모든 행이 ✅인 순간이 한 번도 없다. 그건 게이트가 아니라 wish list다. 쪼개라. blocking이 아닌 행들은 "게이트 3 part B"가 되거나 "출시 후 추적"으로 강등된다. 게이트의 목적은 닫히는 것이다.
7.2 체크 없이 통과하는 게이트
한 행이 Auto ✅로 적혀 있는데, 그것을 검사하는 스크립트가 어디에도 없다. 2주 뒤 그 행은 아예 통과하지 않고 있는데 아무도 모른다. 모든 Auto 행은 실행 가능한 체크가 필요하고, 그 체크 이름이 행에 있어야 한다. 체크가 없으면 그 행은 ✅가 아니라 ❌다.
7.3 게이트가 되지 못한 약속
D-Catchup 패턴이다. spec이 X가 참일 것이라고 한다. X가 참인지 검사하는 것이 없다. 보고자는 자기가 검증한 것 — 빌드, 테스트, lint — 에 대해 정직하다. 아무도 검증하지 않은 것이 깨지는 것이다. spec에서 약속할 가치가 있는 것은 게이트로 만들 가치가 있다.
7.4 "구두로 일어난" Manual 승인
한 행이 Manual ✅인 이유는 3주 전 어떤 세션에서 리드가 "괜찮아 보인다"고 말했기 때문이다. git에 기록된 것이 없다. 다음에 누군가 왜 통과인지 물으면 흔적이 없다. 모든 Manual 승인은 commit 또는 PR review를 남긴다. repo에 없으면 일어나지 않은 것이다.
7.5 "프로덕션에서 테스트할게" 게이트
한 행이 pending — 출시 후 검증이라고 되어 있다. 그건 release gate가 아니라 대기 중인 post-mortem이다. 출시 전 검증 가능하거나 (⏳로 옮기고 검증), 출시 기준이 아니거나 (별도 "출시 후 추적" 섹션으로 이동). 둘 중 하나.
8. 한 원칙
Release gates의 전체 모양은 한 줄로 줄어든다.
"운영자의 mind가 '완료'로 다룰 만한 것은 시스템이 먼저 검사하게 한다. 시스템이 검사할 수 없는 것은 누가 했는지 이름을 적는다."
Release gates는 PRD가 바람을 멈추고 계약이 되는 레이어다. Auto-Gate 행은 기계가 강제하는 계약이다. Manual-Gate 행은 흔적을 남긴 명시된 사람이 강제하는 계약이다. 그 조합이, 운영자가 한 조각이 라이브라고 말할 때 의미하는 것이다.
그리고 일단 게이트가 실재하면 — 모든 행이 0으로 종료하는 스크립트거나 git에 기록된 사람 승인이라면 — 일상 작업의 대부분이 운영자가 키보드에 있을 필요가 없어진다. 그게 12편 Ralph loop에 대한 글이다. PRD가 무엇을 만들지 말하고, 게이트가 언제 만들어졌는지 말하고, 루프가 그 사이의 길을 24/7, 운영자가 잠든 동안 스스로 걷는다.