15장 인수 가능한 릴리스를 만든다
릴리스 후보는 “제 컴퓨터에서 됨”을 넘어 다른 사람이 인수할 수 있어야 한다.
- 새 체크아웃 설치와 시작 명령
- 환경 변수 목록과 비밀 관리 방법
- 핵심 여정 테스트와 실패 상태 캡처
- DB 변경·백업·복구 절차
- 로그와 대시보드 위치
- 알려진 제한과 다음 작업
- 롤백 조건과 명령
최종 데모에서는 예약 하나를 보류하고 운영자가 확정한다. 같은 요청을 다시 보내도 새 예약이 생기지 않는지 확인한다. 겹치는 시간을 보내 OVERLAP을 본다. 로그에서 요청 ID로 전체 흐름을 찾는다. 마지막으로 새 담당자가 README만 보고 같은 결과를 내는지 확인한다.
완주 체크
cd examples/roomrelay
node --test test/*.test.mjs
세 테스트가 통과하는 것만으로 운영 준비가 끝나지는 않는다. 그러나 독자는 이제 무엇이 빠졌는지 설명할 수 있다. 메모리 배열 대신 DB 트랜잭션이 필요하고, 실제 인증과 부하 테스트가 필요하며, 결제 연결 전 법률·보안 검토가 필요하다는 경계를 안다. 그 경계를 아는 것이 코드 구조대의 출발이자 완주다.
실습 워크북: RoomRelay를 직접 인수하는 15개 미션
이 워크북은 앞의 15장을 같은 프로젝트에서 다시 실행한다. 정답 파일을 먼저 복사하지 않는다. 각 미션의 “완료 증거”를 자기 작업 기록에 남긴다.
미션 1 저장소 인수 봉투 만들기
rescue-notes/00-intake.md를 만들고 런타임, 시작 명령, 외부 연결, 데이터 초기화, 원본 보존 위치를 기록한다. start/booking.mjs의 결함을 아직 고치지 않는다.
완료 증거는 다섯 항목이 채워진 문서와 원본 파일 해시다. 비밀값은 기록하지 않는다. 외부 서비스가 무엇인지 모르면 “없음”이 아니라 “미확인”이라고 쓴다.
미션 2 첫 실패를 한 명령으로 재현하기
Node REPL 또는 작은 스크립트에서 10시~11시 예약 뒤 10시 30분~11시 30분 예약을 넣는다. 두 번째 요청이 통과하는 결과를 rescue-notes/01-overlap.txt에 저장한다.
const rows=[];
reserve(first,rows);
console.log(reserve(partialOverlap,rows));
완료 증거는 조건·행동·기대·현재 결과 네 문장이다. “중복 오류”만 적으면 다시 보완한다.
미션 3 사용자 여정에 쓰기 경계 표시하기
예약 선택부터 알림 초안까지의 흐름을 그린다. 데이터 저장과 외부 전송 앞에 WRITE를, 사람이 약속을 승인하는 곳에 HUMAN GATE를 붙인다. 화면 버튼과 서버 함수가 같은 경계를 가리키는지 대조한다.
미션 4 수정 전에 실패 테스트 만들기
부분 겹침 테스트를 추가하고 수정 전 코드에 실행한다. 테스트가 녹색이면 잘못된 구현을 호출했거나 입력이 실제로 겹치지 않는 것이다. 수정 전 실패 출력과 수정 뒤 성공 출력을 둘 다 보존한다.
node --test examples/roomrelay/test/*.test.mjs
책의 solution을 가져오기 전에 자기 테스트가 같은 규칙을 표현하는지 확인한다.
미션 5 권한 표를 서버 검사로 바꾸기
사용자, 운영자, 관리자의 허용 동작을 표로 만든 뒤 confirm에 승인자 요구가 있는지 확인한다. 빈 승인자로 호출했을 때 상태가 변하지 않아야 한다. 실패 응답 뒤 배열의 예약 상태도 검사한다. 응답만 오류고 데이터는 이미 바뀌는 부분 실패를 놓치지 않는다.
미션 6 DB 제약 설계하기
코드를 DB에 연결하지 않고도 필요한 제약을 SQL 초안으로 적는다.
create unique index booking_request_id_uq on booking(request_id);
alter table booking add constraint booking_status_ck
check (status in ('held','confirmed','cancelled'));
이 SQL을 그대로 운영에 적용하지 않는다. 기존 중복 데이터 조사, 잠금 영향, 되돌리기, DB별 겹침 제약 방법을 작업 계획에 추가해야 완료다.
미션 7 시간 경계표 완성하기
다섯 사례를 표로 만든다: 완전 분리, 끝과 시작이 맞닿음, 부분 겹침, 기존 예약 안에 포함, 기존 예약을 완전히 포함. [start,end) 규칙에서 기대 결과를 먼저 쓰고 테스트한다. 잘못된 날짜와 시작이 종료보다 늦은 사례도 추가한다.
미션 8 결제 웹훅 fixture 설계하기
실제 결제 API 대신 네 JSON 파일을 만든다: 성공, 거절, 동일 이벤트 재전송, 서명 오류. 이벤트 ID와 원본 바이트 해시를 기록하고 중복 이벤트가 새 예약을 만들지 않는 기대를 쓴다. 라이브 키와 실제 카드 번호를 사용하면 실습 실패다.
미션 9 requestId 재생 공격하기
같은 요청을 열 번 호출한다. 배열 길이는 1이고 ID도 같아야 한다. 같은 requestId에 다른 시간 payload를 보내는 사례를 설계한다. 현재 교육용 구현은 payload 충돌 검증이 충분하지 않다는 제한을 발견 기록에 남긴다.
for(let i=0;i<10;i++) reserve(request,rows,{now});
assert.equal(rows.length,1);
미션 10 비밀과 개인정보 탐색하기
rg로 토큰 형태, .env, 이메일, 전화번호를 검색한다. 검색 결과를 그대로 보고서에 붙이지 않는다. 파일·줄 위치와 비밀 종류만 기록하고 실제 값은 마스킹한다. 이미 커밋된 비밀은 삭제만 하지 말고 폐기·재발급 계획을 세운다.
미션 11 실패 화면 세 가지 만들기
RoomRelay 화면에 시간 충돌, 권한 없음, 서버 지연 문구를 작성한다. 각 문구는 무엇이 일어났는지, 데이터가 저장됐는지, 사용자가 다음에 할 일을 알려야 한다. 색을 제거한 화면에서도 상태를 구분할 수 있는지 본다.
미션 12 요청 ID로 사건 재구성하기
가상 로그 다섯 줄을 작성한다: 요청 수신, 검증, 겹침 검사, 보류 저장, 승인. 모든 줄에 같은 requestId를 넣고 개인정보 원문은 빼며 오류 코드는 안정적으로 유지한다. 이 로그만으로 처리 순서와 마지막 성공 지점을 찾을 수 있어야 한다.
미션 13 Codex 작업 계약 실행하기
저장소 루트에 짧은 AGENTS.md를 만들고 테스트 명령, 라이브 외부 행동 금지, 시간 주입, 완료 보고 형식을 적는다. Codex에게 한 결함만 수정하도록 요청한 뒤 diff와 테스트 출력을 직접 본다. 규칙을 읽었다는 답변보다 실제 변경이 규칙을 지켰는지가 증거다.
미션 14 10분 장애 훈련하기
“새 릴리스 뒤 예약 오류율 7%”라는 상황을 주고 타이머를 시작한다. 감지, 쓰기 중단, 릴리스 ID 확인, 이전 버전 전환, 보류 데이터 대조, 사용자 공지 초안을 10분 안에 완성한다. 실제 운영 명령은 실행하지 않고 runbook으로만 연습한다.
미션 15 다른 사람에게 인계하기
새 폴더에서 README만 보고 테스트를 실행할 베타 독자를 정한다. 설명을 추가로 말해 주지 않는다. 독자가 막힌 지점, 잘못 이해한 상태, 실행하지 못한 명령을 기록한다. 그 피드백으로 README와 장의 순서를 고친다.
워크북 최종 채점표
| 항목 | 미달 | 통과 |
|---|---|---|
| 재현 | 증상 설명만 있음 | 입력·기대·현재 결과가 있음 |
| 테스트 | 수정 뒤에만 녹색 | 수정 전 실패와 수정 뒤 성공 확인 |
| 상태 | 화면 문구만 분리 | 서버 도메인에서 held/confirmed 분리 |
| 중복 | 버튼 비활성화 | 서버 requestId와 저장 제약 설계 |
| 시간 | 로컬 문자열 비교 | UTC 인스턴트·지역 표시·경계 테스트 |
| 외부 행동 | 샌드박스 호출 | 기본 fixture, 라이브는 별도 승인 |
| 인계 | “테스트 통과” 보고 | 변경·검증·한계·롤백이 함께 있음 |
일곱 항목이 모두 통과해도 운영 보안 승인은 별도다. 이 채점표는 독자가 책의 기술을 수행했는지 확인하는 학습 기준이다.
부록 A 증상별 첫 질문
| 증상 | 먼저 확인할 것 | 피할 행동 |
|---|---|---|
| 설치 실패 | 런타임·잠금 파일·첫 오류 | 패키지 전체 업그레이드 |
| 로그인 풀림 | 쿠키 속성·프록시·세션 저장소 | 인증 라이브러리 즉시 교체 |
| 예약 중복 | 요청 ID·DB 제약·재시도 | 버튼만 비활성화 |
| 시간 어긋남 | 입력 시간대·저장 형식·표시 지역 | 문자열 잘라 맞추기 |
| 간헐 오류 | 요청 ID별 로그·지연·의존 서비스 | 재현 없이 타임아웃 확대 |
부록 B 출간 전 다시 확인할 자료
- Codex 사용법과
AGENTS.md탐색 규칙은 출간 직전 OpenAI 공식 문서를 다시 확인한다. - 보안 헤더와 쿠키 속성은 배포 환경과 최신 브라우저 기준으로 검토한다.
- 결제 예제는 특정 사업자의 문구를 복제하지 않고 일반적인 포트·어댑터 구조만 사용한다.
부록 C 90분 코드 구조대 워크숍
0~15분: 저장소를 보존한다
현재 브랜치, 마지막 커밋, 변경 파일을 기록한다. Git 저장소가 아니라면 원본 폴더를 읽기 전용 사본으로 보존한다. 운영 데이터베이스에 연결된 환경 변수는 제거하고 fixture 환경에서 시작한다. npm install 같은 명령도 설치 스크립트를 실행할 수 있으므로 package.json과 잠금 파일을 먼저 본다.
산출물은 긴 보고서가 아니다.
원본 위치:
현재 커밋:
실행한 명령:
외부 연결 차단 상태:
되돌리는 방법:
15~30분: 첫 실패를 고정한다
한 번에 명령 하나를 실행하고 표준 출력, 표준 오류, 종료 코드를 보존한다. 두 번째 오류를 고치기 전에 첫 오류가 뒤의 오류를 만든 것은 아닌지 확인한다. 브라우저 문제라면 URL, 화면 크기, 로그인 상태, 클릭 순서, 콘솔의 첫 오류를 함께 적는다.
30~50분: 최소 재현을 만든다
전체 서비스를 띄우지 않고도 도메인 함수를 호출할 수 있다면 작은 테스트로 줄인다. 네트워크가 필요하다면 응답 fixture를 저장하되 토큰과 개인정보를 지운다. 최소 재현은 실제 문제와 같은 이유로 실패해야 한다. 단순히 빨간 테스트를 만드는 것이 목적이 아니다.
50~70분: 한 가설만 수정한다
수정 전 가설을 문장으로 쓴다. 예를 들어 “겹침 검사가 시작 시각의 동일성만 비교하기 때문에 부분 중복을 허용한다.” 수정 뒤에는 해당 테스트만 먼저 실행하고, 통과하면 전체 테스트를 실행한다. 포매터가 전체 파일을 바꿔 diff를 가리지 않도록 한다.
70~90분: 반증하고 인계한다
정상 사례만 확인하지 않는다. 경계가 맞닿는 예약, 완전히 포함되는 예약, 과거 시각, 잘못된 날짜, 같은 요청 재생을 시도한다. 마지막에는 다음 형식으로 인계한다.
수정한 증상:
근본 원인:
바꾼 파일:
수정 전 실패 증거:
수정 뒤 검증:
검증하지 못한 영역:
롤백 방법:
부록 D AI에게 맡길 때 쓰는 네 가지 프롬프트
진단 프롬프트
아직 파일을 수정하지 마. 재현 명령과 첫 실패를 기준으로 가능한 원인을
증거가 강한 순서로 세 개만 제시해. 각 원인을 확인할 읽기 전용 명령과,
그 결과가 가설을 지지하거나 반박하는 조건을 적어 줘.
특성 테스트 프롬프트
현재 사용자에게 보이는 동작을 바꾸지 않고 특성 테스트를 추가해.
시간과 난수는 주입하고 네트워크는 fixture로 대체해.
테스트마다 왜 필요한지 한 줄로 설명하되 구현 세부를 테스트 이름에 넣지 마.
최소 수정 프롬프트
실패 테스트 한 개를 통과시키는 최소 변경만 해.
공개 API, DB 스키마, 의존성 버전은 바꾸지 마.
수정 후 대상 테스트와 전체 테스트를 실행하고 diff에서 예상 밖 변경을 찾아.
네거티브 리뷰 프롬프트
이 변경이 틀렸다고 가정하고 검토해. 인증 우회, 중복 요청, 시간대 경계,
부분 실패, 로그의 개인정보, 롤백 불가능성을 재현 가능한 사례로만 보고해.
근거 없는 일반론과 스타일 의견은 제외해.
AI가 “완료했다”고 말해도 명령 출력과 파일 diff를 직접 확인한다. 도구에 파일 쓰기 권한이 없었거나 테스트가 다른 폴더에서 실행됐을 수 있다.
부록 E 증상별 상세 복구 지도
새로 고침하면 상태가 사라진다
브라우저 메모리에만 있는지, 서버 세션인지, DB에 저장되는지 경계를 추적한다. 쿠키가 보안 속성 때문에 로컬 HTTP에서 전송되지 않는지, 프록시가 호스트와 프로토콜 헤더를 바르게 전달하는지 확인한다. 상태를 급히 localStorage로 옮기면 민감 정보와 동기화 문제가 생길 수 있다.
같은 버튼에서 두 레코드가 생긴다
UI의 이중 클릭, 네트워크 재시도, 서버 타임아웃 뒤 재요청, 작업 큐 재배달을 구분한다. 버튼 비활성화는 사용 경험을 돕지만 서버 멱등성을 대신하지 않는다. 요청 ID, 고유 제약, 처리 결과 저장을 함께 설계한다.
배포는 성공했는데 이전 화면이 보인다
빌드 산출물의 해시, 배포 대상 경로, CDN·브라우저 캐시, 서비스 워커, 여러 인스턴스의 버전을 확인한다. 서버 한 대만 새 버전이면 요청마다 화면이 달라질 수 있다. 화면 하단 또는 응답 헤더에 릴리스 ID를 노출하면 원인을 줄일 수 있다.
오류가 재현되지 않는다
재현되지 않는다고 해결된 것은 아니다. 발생 시각의 요청 ID, 사용자 역할, 입력 범주, 배포 버전, 지역, 브라우저, 의존 서비스 상태를 모은다. 개인정보 원문 대신 필요한 범주와 비식별 식별자를 쓴다. 관측 정보가 없다면 먼저 안전한 로깅을 추가하고 기다리는 것이 무작정 수정하는 것보다 낫다.
테스트는 통과하지만 운영에서 실패한다
테스트가 다루지 않은 경계를 찾는다. 실제 프록시, DB 격리 수준, 시간대 데이터, 파일 권한, 네트워크 제한, 데이터 규모가 대표적이다. 운영 장애를 그대로 복제할 수 없다면 가장 가까운 스테이징 조건과 부하 fixture를 만든다. 테스트 통과는 결론이 아니라 다음 질문을 좁힌 증거다.
부록 F 구조대 용어 카드
- 재현: 같은 조건과 행동으로 같은 실패를 다시 관찰하는 일.
- 특성 테스트: 옳고 그름을 판단하기 전 현재 동작을 기록해 변경을 감지하는 테스트.
- 회귀: 이전에 되던 동작이 변경 뒤 깨지는 현상.
- 멱등성: 같은 요청을 여러 번 처리해도 최종 효과가 한 번과 같게 만드는 성질.
- fixture: 테스트가 반복해 사용하는 고정 입력과 응답 데이터.
- 경계: 인증, 저장, 외부 전송처럼 실패 비용과 책임이 달라지는 지점.
- 카나리: 새 버전을 작은 범위에 먼저 노출해 위험을 관찰하는 배포.
- 롤백: 이전 코드만이 아니라 데이터와 외부 효과까지 안전한 상태로 되돌리는 절차.
- 관측 가능성: 로그·지표·추적으로 내부 상태를 추론할 수 있는 능력.
- 검증 계약: 목표, 제약, 완료 조건, 실행할 확인을 AI와 사람 사이에 명시한 약속.
용어를 외우는 것이 목적은 아니다. 장애 보고에서 “중복이 생겼다”를 “동일 requestId 재생이 멱등하지 않다”로 구체화하면 담당자가 볼 경계가 선명해진다. 반대로 전문 용어가 사용자 안내에 그대로 나오지 않게 한다. 사용자에게는 “같은 요청이 두 번 접수되지 않았습니다”처럼 결과와 다음 행동을 설명한다.
최신 공식 확인처
Codex의 지원 기능과 설정 방식은 바뀔 수 있다. 실제 프로젝트에 적용할 때는 위 공식 문서의 현재 설명과 이 책의 예제를 대조한다.
저작권과 예제 사용
RoomRelay, 조직, 사용자, 예약 데이터는 이 책을 위해 만든 가상 예제다. 코드와 그림은 원고의 교육 목적에 맞춰 새로 작성했다. 독자는 학습과 내부 실험에 예제를 변형할 수 있지만 실제 서비스에 적용하기 전 보안·개인정보·결제·접근성 검토를 별도로 수행해야 한다. Codex, Node.js와 기타 제품명은 각 소유자의 상표이며 이 책은 공식 인증이나 제휴를 주장하지 않는다.