이 장이 끝나면: 배포된 온티켓에서 실제 장애 하나 — 환불한 주문의 QR로 입장이 되는 버그 — 를 로그와 데이터로 추적해 원인을 찾고, 재발까지 막는다. 장애를 감정이 아니라 타임라인과 근거로 다루는 법, 그리고 고친 뒤 반드시 남기는 포스트모템을 익힌다.
지금까지는 우리가 통제된 환경에서 서비스를 만들었다. 배포 이후는 다르다 — 예상 못 한 입력, 예상 못 한 순서, 예상 못 한 규모가 들어온다. 운영은 살아 있는 서비스를 계속 돌아가게 하고, 문제가 생기면 수습하는 일이다. 개발이 '만드는 것'이라면 운영은 '지키는 것'이다.
이 차이를 조금 더 풀어 보자. 우리가 지금까지 실습한 환경은 우리가 아는 입력만 들어오는 세계였다. 우리는 이벤트를 하나 만들고, 우리가 직접 예매해 보고, 우리가 아는 순서대로 눌렀다. 그런데 배포된 서비스에는 우리가 상상하지 못한 고객이 온다. 결제 창을 띄워 놓고 30분 뒤에 결제하는 사람, 뒤로 가기를 세 번 눌렀다가 다시 주문하는 사람, 같은 QR을 캡처해서 친구에게 보내는 사람, 환불받고 나서 "혹시" 하고 QR을 다시 찍어 보는 사람. 이 모든 조합을 개발 중에 다 겪어 볼 수는 없다. 그래서 운영은 '만들기'의 연장이 아니라, 실제 세계와 처음 마주치는 별개의 국면이다.
여기에 '온티켓'의 사정이 겹친다. 우리는 인터파크 같은 플랫폼 수수료가 아까워서 자사 티켓을 직접 팔기로 했고, 그래서 예매·결제·입장까지 우리 손으로 돌린다. 플랫폼에 얹어 팔던 시절에는 결제 사고나 입장 오류가 나면 플랫폼이 처리했다. 이제는 그 뒤처리도 우리 몫이다. 직접 판다는 것은 직접 지킨다는 것이다. 운영과 장애 대응을 배우는 이유가 여기 있다.
그리고 장애는 반드시 온다. 아무리 잘 만들어도 온다. 좋은 팀과 그렇지 않은 팀의 차이는 '장애가 없다'가 아니라 '장애를 어떻게 다루는가' 다.
이 문장을 가볍게 넘기지 말자. "장애는 반드시 온다"는 것은 비관이 아니라 전제다. 비행기에도 사고가 난다는 전제 위에서 항공 산업은 블랙박스를 달고, 사고 조사 기관을 두고, 조사 결과를 전 세계 항공사가 공유한다. 그 결과 항공기는 가장 안전한 교통수단이 됐다 — 사고를 없애서가 아니라, 사고를 다루는 체계를 만들어서다. 소프트웨어 운영도 같다. 우리가 이 장에서 배우는 것은 '장애를 없애는 법'이 아니라 '장애가 왔을 때 당황하지 않고 처리하는 절차'다. 절차가 있는 팀은 장애를 한 번 겪고 더 강해지고, 절차가 없는 팀은 같은 장애를 얼굴만 바꿔 계속 겪는다.
[짚고 가기] 이 장은 "오늘은 우리가 만든 걸 부수는 날"이라고 생각하면 자세가 달라진다. 지금까지는 '되게 만드는' 실습이었는데, 이 장은 '망가진 걸 고치는' 실습이다. 개발자가 실무에서 보내는 시간의 상당 부분이 사실 이쪽이다 — 새로 만드는 것보다 이미 있는 걸 고치고 지키는 일이 훨씬 많다. 이 점을 알아두면 이 장의 무게가 다르게 느껴진다. 골목 라이브 초과 판매 사고 이력을 떠올려 "그때도 이렇게 대응했어야 했다"로 연결해 보면 이야기가 더 가깝게 와닿는다.
장애가 터지면 사람은 당황한다. "누가 뭘 잘못했지?"부터 나온다. 이것이 가장 나쁜 출발이다. 범인 찾기는 사실을 왜곡하고, 사람들은 정보를 숨기기 시작한다. 장애 대응의 제1원칙은 이것이다.
사람을 탓하지 않고, 사실을 모은다.
왜 범인 찾기가 그렇게 나쁜가. 이유는 심리적이면서 동시에 실용적이다. 장애의 한복판에서 "누구 잘못이냐"가 먼저 나오면, 그 순간 사람들의 목표가 바뀐다. '문제를 해결한다'에서 '나는 아니라는 것을 증명한다'로. 그러면 자기가 만진 부분의 로그를 슬쩍 지우거나, 불리한 사실을 늦게 말하거나, "저는 그때 없었어요" 같은 방어부터 시작한다. 결과적으로 원인 규명에 가장 필요한 정보가 가장 늦게, 가장 왜곡된 채로 모인다. 사람을 탓하지 않는다는 원칙은 착해서가 아니라, 그래야 사실이 빨리 모이기 때문이다. 이것을 업계에서는 개인을 탓하지 않는(blameless) 문화라고 부른다. '아무도 책임 안 진다'가 아니라, 개인을 처벌하는 대신 구조를 고친다는 뜻이다.
무슨 일이, 언제, 어떤 순서로 일어났는가(타임라인). 무엇이 그것을 증명하는가 (로그·데이터). 원인은 대개 한 사람의 실수가 아니라 여러 방어선이 동시에 비어 있던 구조다 — 그 구조를 찾아 메우는 것이 목적이다.
'여러 방어선이 동시에 비어 있던 구조'라는 표현을 그림으로 잡아 두자. 항공 안전에는 스위스 치즈 모델이라는 유명한 비유가 있다. 치즈 한 장에는 구멍이 뚫려 있지만, 여러 장을 겹치면 구멍이 서로 어긋나서 빛이 통과하지 못한다. 사고는 그 구멍들이 우연히 일렬로 늘어섰을 때 일어난다. 온티켓의 이번 장애도 그렇다 — 명세에 구멍이 있었고, 코드는 명세대로 만들어져 같은 자리에 구멍이 있었고, 테스트는 그 경우를 확인하지 않아 구멍을 못 막았고, 입장 게이트 앞의 사람은 초록불만 믿었다. 어느 한 곳이라도 막혀 있었으면 고객은 못 들어갔다. 그러니 "누가 구멍을 뚫었냐"가 아니라 "왜 구멍들이 한 줄로 늘어섰냐, 어디를 막으면 다음엔 어긋나게 할 수 있냐"를 묻는 것이 옳다. 이 관점이 이 장 전체를 관통한다.
[짚고 가기] "사람을 탓하지 마라"를 도덕 훈화로 들으면 힘이 없다. 실용 논리로 이해하는 게 좋다 — 범인을 찾기 시작하는 순간 증거가 숨는다, 그래서 손해다. 스위스 치즈 모델은 그림 한 장이면 충분하다(구멍 뚫린 네모 여러 개를 겹쳐 그리고, 한 줄로 뚫린 경로 하나를 화살표로 이으면 된다). 이 장의 결말(명세→코드→테스트→사람이 다 같은 자리에 비어 있었다)을 미리 염두에 두면, 뒤에서 근본 원인이 드러날 때 "아, 그 치즈구나" 하고 연결된다.
장애 분석은 AI가 크게 돕는 영역이다. 수천 줄 로그에서 패턴을 찾고, 관련 코드와 데이터를 대조하고, 가설을 세우는 일 — AI가 빠르다. 다만 원칙이 있다. 장애 대응 역할에게는 시스템을 바꿀 권한을 주지 않는다. 분석과 수정은 분리한다. 불난 집에서 원인을 조사하는 사람과 불을 끄는 사람이 뒤섞이면, 증거가 사라지고 성급한 수정이 2차 사고를 부른다.
이 '권한 분리'가 왜 중요한지, 조금 더 구체적으로 보자. 우리가 이 장에서 쓰는 incident-responder 서브에이전트는 읽기 계열 도구(파일 읽기, 검색, 명령 실행 같은 조사 도구)만 가지고 있고, 파일을 고치거나 새로 쓰는 권한은 아예 없다. 이것은 우연이 아니라 설계다. 조사하는 역할에게 수정 권한이 있으면, 조사 도중에 "아, 이거네" 하고 손이 먼저 나간다. 그런데 그 성급한 손이 바로 증거를 덮는다. 로그를 지우고, 데이터 상태를 바꾸고, "고쳤으니 됐지"로 넘어가 버린다. 그러면 근본 원인까지 파악하지 못하고, 다음에 같은 장애가 다른 얼굴로 돌아온다.
실제 화재 현장을 떠올리면 정확히 같다. 소방관은 불을 끄고, 화재 조사관은 불을 끄지 않는다. 조사관이 증거를 보존하며 발화 지점을 찾는 동안, 소방관은 현장을 밟고 물을 뿌린다. 이 둘이 한 사람이면 — 발화 지점의 증거가 진화 과정에서 사라진다. 소프트웨어도 똑같아서, 조사자(incident-responder)와 수정자(만드는 역할)를 나눈다. 조사가 근본 원인까지 내려가 "여기가 발화 지점이다"를 확정한 다음, 그 결론을 받아 만드는 역할(백엔드 개발자 등)이 명세와 코드를 고친다.
여기에 사람의 자리가 하나 더 있다. AI가 분석을 아무리 빨리 해도, "이것을 근본 원인으로 확정한다"와 "이 수정안을 승인한다"는 사람이 판단한다. 첫날 익힌 결정·승인·검증이 장애 대응에서도 그대로 살아 있다. AI는 수천 줄 로그를 1분에 훑어 가설 다섯 개를 세우지만, 그중 무엇이 진짜인지, 지금 프로덕션에 어떤 수정을 넣을지는 사람이 결재한다. 장애 상황일수록 이 결재가 더 무겁다 — 급하다고 읽지 않고 승인하면, 급해서 낸 수정이 2차 사고가 된다.
[짚고 가기] "AI에게 장애를 통째로 맡기면 안 되냐"는 궁금증이 들기 쉽다. 답은, 분석은 맡기되 수정 권한은 떼어 놓는 것이다. 화재 조사관 비유로 생각하면 이해가 쉽다. 그리고 이 분리는 앞 장들에서 배운 '역할별 서브에이전트'와 '퍼미션'의 연장선이다. 3일 내내 반복된 원칙(권한은 필요한 만큼만, 위험한 일은 사람이 승인)이 이 장에서 하나로 완성되는 셈이다.
이 장에서 처음, 또는 본격적으로 만나는 용어들이다. 운영 현장에서 매일 쓰는 말이라 실무에서 그대로 통한다. 완벽히 외울 필요는 없고, "이런 게 있구나" 정도로 눈에 익혀 두면 실습하며 손에 붙는다.
서비스가 돌아가며 남기는 기록이다. "언제 누가 무엇을 요청했고 어떤 결과가 나왔는지"가 시간순으로 쌓인다. 장애 분석의 1차 증거다. 단, 로그에는 함정이 있다 — 개인정보가 로그에 남으면 그 자체가 보안 사고다(보안검토자가 점검하던 바로 그 항목이다). 좋은 로그는 필요한 사실은 남기되 전화번호·이메일 같은 것은 가린다.
'로그(log)'라는 단어의 유래가 이 개념을 정확히 설명한다. 원래 log는 통나무다. 옛날 배는 속도를 재려고 통나무 조각에 밧줄을 묶어 바다에 던지고, 그 밧줄이 풀려 나가는 길이로 속도를 계산했다. 그리고 그 측정값을 항해 일지에 적었는데, 그 일지를 logbook이라 불렀다 — 통나무로 잰 기록이라는 뜻이다. 배가 지나온 시각·위치·속도·날씨를 시간순으로 적은 그 항해 일지가, 오늘날 소프트웨어가 남기는 로그의 조상이다. 그래서 로그의 핵심 성질도 항해 일지와 같다. 시간순이고, 사실이고, 나중에 무슨 일이 있었는지 되짚기 위한 것.
로그가 왜 장애 분석의 '1차 증거'인지 생각해 보자. 장애가 났을 때 우리가 가진 것은 대개 불평 한 줄이다("입장이 됐대요"). 이 불평은 감정이고 전언이라 믿을 수 없다. 반면 로그는 서비스 자신이 그때그때 남긴 사실이라, 기억에 의존하지 않는다. "10:03에 이 주문의 입장 요청이 들어왔고, 200(성공)으로 응답했다"는 로그 한 줄은 어떤 증언보다 강하다. 그래서 장애 대응의 첫 동작이 "로그를 본다"다.
로그의 함정도 실무에서 정말 자주 터진다. 개발자가 디버깅하려고 "요청 내용을
통째로 찍자"고 로그를 심으면, 거기에 고객의 전화번호·이메일·주소가 그대로
찍힌다. 이 로그 파일에 접근할 수 있는 사람이 곧 그 개인정보에 접근할 수 있으니,
로그 자체가 개인정보 유출 경로가 된다. 그래서 좋은 로그는 010-****-1234
처럼 마스킹(가리기) 을 한다. 이 장의 실습에서 우리는 장애를 파다가 이
두 번째 문제를 실제로 만나게 된다.
운영 현장의 용어를 가볍게 알아 두자.
| 용어 | 무엇 | 온티켓 예 |
|---|---|---|
| 로그(Logs) | 개별 사건의 기록 | "10:03 주문 ORD-... 입장 처리됨" |
| 메트릭(Metrics) | 수치 지표의 추이 | 분당 주문 수, 에러율 |
| 트레이스(Traces) | 한 요청이 거친 경로 | 예매 요청이 web→api→DB를 거친 흐름 |
온티켓 규모에선 로그가 주력이다. 메트릭·트레이스는 서비스가 커지면 붙인다.
이 세 가지를 묶어 관측성(observability) 이라고 부른다. 어원이 개념을 잘 설명한다. 원래 제어 이론에서 온 말로, "밖에서 관찰할 수 있는 신호만 보고 안에서 무슨 일이 일어나는지 알아낼 수 있는 정도" 를 뜻한다. 서비스는 우리 눈에 안 보이는 상자다. 그 상자에 창문을 몇 개나, 어떤 모양으로 뚫어 두었느냐가 관측성이다. 창문이 없으면 장애가 나도 깜깜하고, 창문이 잘 뚫려 있으면 "아, 저기가 막혔구나"가 보인다.
세 종류를 일상 비유로 구분하면 이렇다.
실무의 순서도 이 비유대로다. 보통 메트릭이 이상 신호를 먼저 울리고(에러율 급증), 로그로 무슨 일인지 확인하고, 복잡하게 얽힌 경우 트레이스로 어느 단계인지 짚는다. 온티켓은 아직 작아서 로그만으로 충분하다. 다만 서비스가 커져 하루 수만 건이 들어오면 로그를 사람이 다 읽을 수 없으니, 그때 메트릭으로 "언제부터 이상해졌나"를 좁히고 그 구간의 로그만 파는 식으로 올라간다. 지금은 "이런 층위가 있다"만 알아 두면 된다.
[짚고 가기] 관측성 3종은 처음에는 이름이 낯설어 겁먹기 쉽다. 하지만 지금 우리가 쓸 건 로그 하나뿐이고, 나머지 둘은 서비스가 커지면 붙이는 것이니 부담 갖지 않아도 된다. 대신 "왜/무엇/어디" 세 질문으로 묶어 두면 기억에 오래 남는다 — 메트릭은 '뭔가 잘못됐다', 로그는 '무슨 일인지', 트레이스는 '어느 구간인지'. 외울 개념이 아니라 실무 감각을 잡는 용도다.
장애를 의도적으로 다시 일으켜 보는 것. "가끔 이상하다"는 신고를 확실한 사실로 바꾸는 방법이다. 재현되면 원인의 절반은 잡힌 것이고, 고친 뒤 같은 재현이 더는 안 되면 수정의 증명이 된다. (테스트를 배울 때 '일부러 망가뜨려' 확인한 것과 같은 정신 — 운영에서도 재현이 열쇠다.)
재현이 왜 그렇게 힘이 센지 두 방향으로 나눠 보면 분명해진다.
첫째, 재현은 모호함을 사실로 바꾼다. "환불한 고객이 입장됐대요"는 전언이다. 몇 건인지, 어떤 조건에서인지, 정말 그런 건지 아무도 확신 못 한다. 그런데 "환불 상태인 주문 ORD-1234의 QR로 입장을 시도했더니 실제로 초록불이 떴다"를 우리 손으로 재현하면, 그 순간 신고는 부정할 수 없는 사실이 된다. 원인을 찾는 사람에게 이것보다 든든한 출발점은 없다.
둘째, 재현은 수정의 증거가 된다. 고쳤다고 말로 하는 것과, "고치기 전에는 재현되던 것이 고친 뒤에는 재현되지 않는다"를 보여 주는 것은 무게가 다르다. 전자는 주장이고 후자는 증명이다. 그래서 장애 대응의 정석은 고치기 전에 반드시 재현부터 성공시키는 것이다. 재현이 안 되면 뭘 고쳐야 하는지도 모르고, 고친 뒤에 나아졌는지도 알 수 없다.
이 정신은 테스트를 배울 때 코드를 일부러 망가뜨려 "이 테스트가 진짜 잡아내는가"를
확인한 것과 정확히 같다. 그때는 코드를 일부러 틀리게 만들어 테스트가 빨간불이
되는지 봤다. 여기서는 장애 상황을 일부러 만들어 버그가 재현되는지 본다. 둘 다
"실제로 일어나는 것을 눈으로 확인한 뒤에만 믿는다" 는 태도다. 이번 장애의
경우, 재현은 곧 "환불 처리한 주문의 QR로 POST /checkins를 실제로 호출해
보는 것"이 된다.
증상이 아니라 진짜 원인. "입장이 잘못됐다"는 증상이고, "입장 확인 코드가 주문의 환불 여부를 안 본다"가 근본 원인이다. 증상만 덮으면 다른 얼굴로 재발한다. "왜?"를 몇 번 반복해 밑바닥까지 내려가는 것이 요령이다.
이 "왜?를 반복한다"에는 이름이 붙어 있다. 5 Why(다섯 번의 왜) 기법이다. 도요타 생산 방식에서 나온 것으로, 문제 앞에서 "왜?"를 대략 다섯 번 파고들면 표면 증상 밑의 진짜 원인에 닿는다는 방법이다. 다섯이라는 숫자가 마법인 것은 아니고, "한 번의 왜로 멈추지 말고 근본까지 내려가라" 는 뜻의 관용이다. 이번 온티켓 장애에 적용하면 이렇게 내려간다.
다섯 번째 '왜'에서 밑바닥이 드러난다. 코드의 버그가 아니라 명세의 구멍이 근본 원인이다. 여기서 초심자가 자주 하는 착각 하나를 짚자. "근본 원인 = 누구 탓"이 아니다. 5 Why의 마지막 답이 "누가 게을렀다"로 끝나면 그건 잘못 파 내려간 것이다. 좋은 5 Why는 사람 대신 구조로 끝난다 — "명세에 이 조건이 빠져 있었고, 그 빠짐을 잡는 방어선이 없었다." 그래야 다음 행동이 '사람 혼내기'가 아니라 '방어선 세우기'가 된다.
그리고 근본 원인을 찾는 것이 왜 중요한지, 증상만 덮었을 때 무슨 일이 나는지도 구체적으로 보자. 만약 이번에 코드만 급히 고쳐 "환불 주문은 입장 거절"을 한 줄 넣고 끝냈다고 하자. 그런데 명세는 그대로 'QR 존재하면 입장'이다. 몇 주 뒤 다른 사람(또는 다른 AI)이 그 명세를 보고 입장 관련 기능을 하나 더 만든다. 명세에는 환불 얘기가 없으니, 그 새 기능에서 똑같은 구멍이 다시 뚫린다. 증상만 덮으면 반드시 이렇게 다른 얼굴로 돌아온다. 근본 원인까지 내려가 명세를 고쳐야, 그 명세를 보는 모든 후속 작업이 자동으로 보호받는다.
[짚고 가기] 5 Why는 직접 한 계단씩 내려가며 적어 보면 효과가 크다. 보통 두 번째 '왜'("코드가 안 봤으니까")에서 멈추기 쉽다. 거기서 "그럼 코드는 왜 안 봤지?"를 한 번 더 밀고 내려가면 명세에 닿고, 순간 "아, 이게 근본이구나" 하는 감각이 온다. API 명세를 쓸 때 "명세의 구멍은 코드가 생기기 전에 메워야 비용이 가장 적게 든다"고 배운 것이 여기서 되살아난다 — 못 메운 구멍이 프로덕션 장애가 되어 돌아온 것이다. 이 연결이 이 과정에서 가장 크게 깨달음을 얻는 순간 중 하나다.
이 장 첫머리에서 원칙으로 말한 것을 용어로 한 번 더 못 박아 두자. 장애 대응에서 분석하는 역할과 수정하는 역할은 나눈다. 우리 팀에서 분석은 incident-responder 서브에이전트가 맡는데, 이 역할은 읽기·검색·조사 도구만 가지고 있고 파일을 바꾸거나 새로 쓰는 권한이 없다. 반대로 수정은 만드는 역할(백엔드 개발자 등)이 맡고, 그들만 파일을 고칠 수 있다.
이 분리를 '권한'의 언어로 이해하면 3일 내내 배운 것과 이어진다. 우리는 계속 "필요한 만큼의 권한만 준다" 는 원칙을 지켜 왔다. 조사만 하는 역할에게 수정 권한은 '필요 이상'이다. 필요 이상의 권한은 사고의 씨앗이다 — 조사 도중에 손이 나가 증거를 덮거나 성급한 수정을 하기 때문이다. 그래서 아예 권한을 주지 않는다. 이것은 사람을 못 믿어서가 아니라, 급한 상황에서 실수할 여지를 구조로 없애는 것이다.
장애가 끝난 뒤 무슨 일이 왜 일어났고 어떻게 막을지를 남기는 기록이다. 범인 기록부가 아니라 재발 방지 설계도다. 좋은 포스트모템은 사람 이름 대신 "어느 방어선이 비어 있었는가"를 적는다. 개발 밖에서도 통한다 — 행사 사고, 매장 클레임, 배송 지연을 같은 형식으로 남기면 조직이 같은 실수를 반복하지 않는다.
'포스트모템(postmortem)'이라는 말의 어원을 알면 성격이 분명해진다. 라틴어로 post(뒤에) + mortem(죽음), 즉 원래는 부검(사후 검시) 을 뜻하는 의학 용어다. 부검의 목적을 생각해 보라. 죽은 사람을 되살리는 것이 아니다 — 왜 죽었는지 정확히 밝혀, 산 사람을 지키는 것이다. 소프트웨어 포스트모템도 똑같다. 이미 지나간 장애를 되돌리는 문서가 아니라, 다음 장애를 막기 위한 문서다. 그래서 부검 소견서에 "이 사람이 게을러서 죽었다"고 쓰지 않듯, 포스트모템에도 "누가 실수해서"를 쓰지 않는다. 원인과 재발 방지를 쓴다.
포스트모템에 반드시 들어가는 뼈대가 있다. 실무에서 쓰는 공통 형식이다.
| 항목 | 무엇을 적나 | 이번 장애의 예 |
|---|---|---|
| 증상 | 밖에서 관찰된 문제 | 환불한 주문의 QR로 입장이 됐다 |
| 타임라인 | 언제 무슨 일이 있었나(시간순) | 배포 → 신고 → 재현 → 원인 확인 → 수정 → 검증 |
| 근본 원인 | 5 Why로 내려간 밑바닥 | 명세가 입장 조건을 'QR 존재'로만 정의 |
| 영향 범위 | 얼마나 번졌나(숫자로) | 환불인데 입장된 주문 N건, 어느 공연 |
| 조치 | 이번에 무엇을 고쳤나 | 명세 수정, 코드 수정, 테스트 추가, 로그 마스킹 |
| 재발 방지 | 다음을 막을 방어선 | 명세 조건·테스트를 상시 방어선으로 심음 |
여기서 핵심 관점은 재발 방지를 '방어선' 관점으로 적는 것이다. "다음엔 조심하겠습니다"는 방어선이 아니다 — 사람의 다짐은 다음 바쁜 날 무너진다. 방어선은 사람의 의지와 무관하게 자동으로 작동하는 장치다. 이번 장애에서 우리가 세우는 방어선은 세 겹이다. (1) 명세에 "유효한 주문의 QR만 입장" 조건을 못 박아, 이 명세를 보는 모든 후속 작업이 보호받게 한다. (2) 그 조건을 검증하는 테스트를 추가해, 환불 QR 입장이 다시 뚫리면 CI가 빨간불을 켜게 한다. (3) 로그 마스킹으로 개인정보 노출 경로를 막는다. 스위스 치즈의 구멍들을 서로 어긋나게 다시 배치하는 것이다.
포스트모템이 개발 밖에서도 통한다는 것을 온티켓 세계의 예로 그려 보자. 승승장구는 원래 공연·행사를 여는 회사다. 지난번 골목 라이브에서 좌석보다 많은 표를 팔아 현장에서 고객을 돌려보낸 초과 판매 사고가 있었다. 만약 그때 포스트모템을 이렇게 남겼다면 — 증상(정원 200석인데 230석 판매), 타임라인 (오픈 → 매진 표시 지연 → 초과 판매 → 현장 항의), 근본 원인(판매 마감과 실시간 재고 반영 사이에 시차가 있었고, 그 시차 동안 팔린 표를 막는 장치가 없었다), 재발 방지(잔여석 0이 되면 판매 버튼을 즉시 잠그는 규칙, 마감 담당자 이중 확인) — 이번 온티켓 예매 시스템을 만들 때 그 교훈이 그대로 설계에 반영됐을 것이다. 포스트모템은 이렇게 한 번의 사고를 조직의 자산으로 바꾸는 장치다.
[짚고 가기] "포스트모템은 반성문이 아니다"를 확실히 기억해 두자. 처음에는 누구나 "죄송합니다, 앞으로 주의하겠습니다" 톤으로 쓰기 쉽다. 그럴 때 "다음에 어떻게 자동으로 막을 건데?"를 스스로 물으면, 다짐이 아니라 장치(명세 조건, 테스트)로 방향이 바뀐다. 부검 어원(왜 죽었는지 밝혀 산 사람을 지킨다)은 짧지만 강하게 각인된다. 골목 라이브 초과 판매 사례를 포스트모템 형식에 끼워 보면, 개발 밖에서도 같은 틀이 통한다는 게 실감난다.
이 장의 절차 — 사실 수집 → 재현 → 근본 원인 → 수정 → 재발 방지 → 포스트모템 — 는 소프트웨어만의 것이 아니다. 승승장구가 실제로 하는 다른 일들에 그대로 얹힌다. 오히려 이 틀이 개발 밖에서 먼저 쓰였고, 소프트웨어가 빌려 온 것에 가깝다.
| 영역 | 장애(증상) | 사실 수집 | 근본 원인 | 재발 방지(방어선) |
|---|---|---|---|---|
| 행사 운영 | 골목 라이브에서 정원 초과 입장 | 판매 기록·입장 카운트 대조 | 마감과 재고 반영 사이 시차 | 잔여석 0이면 판매 즉시 잠금 |
| 매장 클레임 | "받은 상품이 파손됐다" 반복 접수 | 접수 건·배송 경로·포장 방식 확인 | 특정 택배사 구간에서 충격 | 완충 포장 기준 명문화·구간 교체 |
| 배송 지연 | 특정 지역만 배송이 늦다 | 운송장·출고 시각·지역 분포 확인 | 그 지역 물류센터 인력 부족 시간대 | 피크 시간 인력 배치 규칙 추가 |
| 케이터링 | 행사 음식에서 알레르기 반응 | 메뉴·재료·제공 시각 확인 | 성분표에 견과류 누락 | 성분 표기 이중 확인 절차 |
이 표에서 공통점을 짚어 보자. 어느 영역도 "누가 잘못했나"로 시작하지 않는다. 전부 "무슨 일이, 언제, 어떤 순서로"(사실 수집)로 출발하고, "왜"를 밑바닥까지 내려가(근본 원인), 사람의 다짐이 아니라 자동으로 작동하는 규칙(방어선)으로 끝난다. 매장 클레임에서 "포장 담당자가 조심하겠다"가 아니라 "완충 포장 기준을 문서로 못 박고 그 구간 택배사를 바꾼다"로 가는 것 — 이것이 온티켓 명세를 고쳐 후속 작업 전체를 보호하는 것과 같은 발상이다. 그래서 이 장에서 배우는 사고 방식은 개발자가 아니어도 조직에서 문제를 다루는 데 그대로 쓰인다.
배포 다음 날, 온티켓으로 신고가 들어왔다(강의에서는 이 상황을 주어진 장애로 다룬다).
골목 라이브 현장 스태프: "환불받은 고객이 QR로 입장이 됐어요. 분명 취소한 주문인데 게이트에서 초록불이 떴대요. 몇 건인지 모르겠어요."
이것은 돈이 걸린 심각한 장애다 — 환불해 준 티켓으로 입장까지 하면 이중 손해다. 당황 대신 절차로 간다. 사실 수집 → 재현 → 근본 원인 → 수정 → 재발 방지 → 포스트모템.
이 신고 한 줄을 장애 대응의 눈으로 뜯어 보자. 여기에는 사실과 감정과 모름이 섞여 있다. "환불받은 고객", "취소한 주문"은 데이터로 확인할 수 있는 사실 후보다. "초록불이 떴대요"는 현장 스태프가 고객에게 전해 들은 전언이라 아직 확인 전이다. "몇 건인지 모르겠어요"는 솔직한 모름이다. 좋은 장애 대응자는 이 셋을 섞지 않는다. 확인된 사실과, 확인해야 할 전언과, 아직 모르는 숫자를 구분해서 각각을 채워 나간다. 우리가 첫 번째로 할 일이 바로 "몇 건인지" 라는 빈칸을 막연한 불안에서 구체적 목록으로 바꾸는 것이다.
여기서 이 장애가 왜 특별히 중요한지도 한 번 더 짚자. 다른 버그는 대개 "안 되는" 버그다 — 결제가 안 되고, 페이지가 안 뜬다. 고객이 불편하지만 회사가 손해를 보진 않는다. 그런데 이 버그는 "되면 안 되는데 되는" 버그다. 환불로 이미 돈을 돌려준 티켓으로 입장까지 하면, 회사는 표값을 잃고 자리도 잃는다. 이런 '조용히 손해가 새는' 버그는 화면에 에러도 안 뜨고 고객도 신고하지 않아서 (오히려 이득이니까) 늦게 발견된다. 그래서 신고가 들어온 지금, 절차대로 끝까지 파는 것이 중요하다.
우리에겐 단서가 있다. 운영 데이터에는 실제 입장 기록(checkins)과 주문 상태
(orders)가 있고, 게이트 로그(data/logs/app_error.log)도 남아 있다. 이 증거들로 추적한다.
두 개의 데이터를 미리 그려 두자. orders는 주문 대장이다 — 주문번호, 어떤 공연인지, 결제됐는지, 그리고 환불/취소됐는지(상태)가 들어 있다. checkins는 입장 대장이다 — 어느 주문의 QR이 언제 입장 처리됐는지가 찍힌다. 이 둘을 대조하면 질문에 답할 수 있다. "orders에서 상태가 환불인 주문 중에, checkins에 입장 기록이 있는 것이 몇 건인가?" 이 교집합이 바로 신고가 말한 사고의 정체다. 데이터가 있으니 우리는 추측하지 않아도 된다 — 대조하면 사실이 나온다.
[짚고 가기] 신고 문장을 그대로 놓고 "여기서 사실은 무엇이고 전언은 무엇이냐"를 스스로 물어 보면 좋은 출발점이 된다. 대개 "환불됐는데 입장됐다" 전체를 사실로 받아들이기 쉬운데, "초록불이 떴대요"는 아직 확인 전 전언이다. 이 구분을 해 두면 '사실 수집'의 의미가 살아난다. 그리고 orders와 checkins 두 대장을 직접 그려 교집합을 색칠해 보면, 뒤 실습에서 AI가 하는 대조 작업이 무엇인지 미리 그림이 잡힌다.
장애 대응 프롬프트에는 일반 개발 지시와 다른 원칙이 하나 더 있다. 분석 단계에서는 "추정하지 말고 데이터로", "수정하지 말고 원인까지만"을 지시문에 명시하는 것이다. AI는 도우려는 성향이 강해서, 시키지 않으면 조사 도중에 "이렇게 고치면 되겠네요" 하고 손이 나간다. 장애 상황에서 그 성급함이 증거를 덮는다. 그래서 조사 프롬프트에는 울타리(제약) 를 분명히 친다. 아래 사례들을 그 관점으로 읽어 보자.
분석은 시스템을 바꿀 수 없는 역할에게 맡긴다.
incident-responder 서브에이전트에게 맡겨줘. 신고: "환불된 주문의 QR로
입장이 된다." 다음을 사실만 수집해서 타임라인으로 정리해:
1) orders 에서 상태가 '환불'인 주문 목록
2) checkins 에서 그 환불 주문들의 입장 기록이 있는지 대조
3) 관련 로그에서 입장 확인(checkin) 처리 흔적
추정 말고 데이터로. 수정은 하지 마 — 원인 후보까지만.
이 프롬프트가 왜 좋은지 뜯어 보자. 첫째, 역할을 지정했다("incident-responder 에게"). 수정 권한이 없는 역할을 골라, 조사 중에 손이 나가는 것을 구조로 막았다. 둘째, 찾을 것을 번호로 구체화했다. "원인 좀 찾아줘"가 아니라, orders에서 무엇을, checkins에서 무엇을, 로그에서 무엇을 볼지 못 박았다. 셋째, 마지막 두 문장이 울타리다 — "추정 말고 데이터로"는 소설을 못 쓰게 하고, "수정은 하지 마"는 분석과 수정을 분리한다. 이 두 문장이 없으면 AI는 그럴듯한 가설을 사실인 양 말하거나, 조사하다 코드를 고쳐 버린다.
이 지시를 조금 더 강하게 만들고 싶으면, 증거를 인용하라는 요구를 덧붙일 수 있다.
각 발견에는 근거를 함께 붙여줘 — 어느 주문번호인지, checkins의 몇 시 몇 분
기록인지, 로그의 어느 줄인지. "환불인데 입장됨"이라고만 쓰지 말고 그렇게
판단한 데이터 위치를 같이 적어. 결과는 표로: [주문번호 | 공연 | 주문상태 |
입장시각 | 근거].
이렇게 하면 결과가 검증 가능한 사실 목록이 된다. 나중에 사람이 "정말 그런가" 를 데이터에서 직접 다시 확인할 수 있고, 포스트모템의 '영향 범위'에 그대로 들어간다.
방금 찾은 사실을 근거로, 왜 환불 주문이 입장 처리됐는지 근본 원인을 찾아줘.
입장 확인 로직(POST /checkins)이 무엇을 검사하고 무엇을 안 검사하는지,
코드와 명세(API-Spec)를 대조해서. "왜"를 끝까지 내려가.
이 프롬프트의 핵심은 두 가지다. 하나는 "무엇을 검사하고 무엇을 안 검사하는지" 라는 표현이다. 버그는 대개 '한 것'이 아니라 '안 한 것'에서 나온다 — 여기서도 코드가 뭘 잘못한 게 아니라, 주문의 환불 여부를 안 본 것이 문제다. "안 하는 것을 찾아라"라고 지시하면 AI가 누락을 겨냥해 본다. 다른 하나는 "코드와 명세를 대조해서" 다. 코드만 보면 "여기 검사 한 줄 추가하면 되겠네"에서 멈춘다. 명세와 대조해야 "명세에도 그 조건이 없다"는 밑바닥이 드러난다. "왜를 끝까지"는 5 Why를 자연어로 시킨 것이다.
여기서 드러날 것이다 — 입장 확인이 "이 QR에 해당하는 주문이 존재하는가"만 보고, "그 주문이 유효한(환불되지 않은) 상태인가"는 보지 않는다. 그리고 그 뿌리는 더 위에 있다: API 명세 자체에 입장 조건이 'QR 존재'로만 적혀 있었다. 코드는 명세대로 만들어졌고, 명세에 구멍이 있었다. (API 명세를 쓸 때 명세의 구멍을 "코드가 생기기 전에 메워야 비용이 가장 적게 든다"고 한 이유가 여기서 분명해진다 — 못 메운 구멍이 프로덕션 장애가 되어 돌아왔다.)
만약 AI가 "코드에 환불 검사가 빠졌다"에서 멈추면, 한 계단 더 밀어 내리는 지시를 덧붙인다.
거기서 멈추지 말고 한 단계 더. 코드가 환불 여부를 안 본 이유가 뭐야?
명세(docs/API-Spec.md)의 POST /checkins 항목에 입장 조건이 어떻게 적혀
있는지 그대로 인용해서, 코드가 명세를 어긴 건지 아니면 명세를 충실히 따른
건지 판정해줘.
이 추가 지시가 근본 원인을 '코드'에서 '명세'로 옮긴다. 그리고 판정 결과가 "코드는 명세를 충실히 따랐다"로 나오는 순간, 아무도 코드를 잘못 짜지 않았고 명세에 구멍이 있었다는 것이 확정된다 — 사람 탓이 아니라 방어선이 비어 있었다는 이 장의 결론이 데이터로 증명된다.
근본 원인이 명세에 있으니, 코드만 고치면 또 어긋난다. 명세→코드→테스트 순서로 메운다.
근본 원인이 확인됐어. 다음 순서로 고쳐줘:
1) docs/API-Spec.md 의 POST /checkins 조건을 고친다:
"유효한(환불/취소되지 않은) 주문의 QR만 입장 허용. 환불 주문은 409/403 거절"
2) /backend 로 api 코드를 명세대로 수정 — 입장 전에 주문 상태를 검사
3) 이 케이스를 막는 테스트 추가: 환불 주문 QR로 입장 시도 → 거절되어야
4) 로그에 개인정보가 남던 부분이 있으면 마스킹까지
이 프롬프트에서 순서 자체가 방법론이다. 1번(명세)이 2번(코드)보다 앞에 있는 것이 핵심이다. 근본 원인이 명세의 구멍이니, 명세를 먼저 고쳐야 코드가 따를 '옳은 기준'이 생긴다. 명세를 그대로 두고 코드만 고치면, 코드와 명세가 어긋난 채로 남아 다음에 명세를 본 사람이 다시 구멍을 만든다. 3번(테스트)은 이 수정을 상시 방어선으로 바꾼다 — 한 번 고치고 끝이 아니라, 앞으로 누가 이 구멍을 다시 뚫으면 테스트가 빨간불로 잡는다. 4번은 조사 도중 발견한 두 번째 문제 (로그 개인정보 노출)를 같은 수정에 묶어 처리한다.
이번 수정에는 DB 변경이 없다 — orders에 주문 상태 컬럼이 이미 있고, 빠진 것은
그 값을 보라는 조건뿐이었기 때문이다. 만약 DB에 없는 값이 필요한 수정이었다면
순서는 명세 → docs/ERD.md와 스키마 → 코드가 된다. 어느 경우든 코드는 맨 마지막이다.
'409/403'이라는 숫자를 초심자를 위해 풀어 두자. 이것은 HTTP 상태 코드로, 서버가 요청에 "이렇게 됐다"고 붙이는 세 자리 번호다. 200번대는 성공, 400번대는 "요청에 문제가 있어 거절", 500번대는 "서버가 터짐"이다. 403(Forbidden) 은 "권한이 없어 금지", 409(Conflict) 는 "현재 상태와 충돌해서 안 됨"이라는 뜻이다. 환불된 주문의 입장은 "이 주문 상태로는 입장이 안 된다"는 상태 충돌에 가까워서 409가 자연스럽고, 403으로 봐도 무방하다. 중요한 건 200(성공)이 아니라 거절 코드로 응답한다는 것이다 — 게이트가 초록불 대신 빨간불을 켜게.
'약한 지시 → 강한 지시' 대비로 보면 이 수정 프롬프트의 힘이 분명해진다.
| 약한 지시 | 강한 지시 | 무엇을 바꿨나 |
|---|---|---|
| 환불 주문 입장 버그 고쳐줘 | 명세→코드→테스트 순서로 고쳐줘 (위 4단계) | 고칠 '순서'와 '범위'를 명시 |
| 코드에 환불 검사 추가해줘 | 명세부터 조건을 고치고, 코드는 그 명세를 따르게 | 근본 원인(명세)부터 손대게 |
| 됐는지 확인해줘 | 이 케이스를 막는 테스트를 추가해 CI가 잡게 | 일회성 수정을 상시 방어선으로 |
qa 서브에이전트에게: 수정 전후를 비교 검증해줘.
환불 상태인 주문의 QR로 POST /checkins 를 시도했을 때,
수정 전에는 입장 처리(버그 재현), 수정 후에는 거절되는지 실제로 확인해서
판정표로. 그리고 정상 주문의 입장은 여전히 되는지도(회귀 확인).
이 프롬프트가 재현의 두 가지 힘을 그대로 담고 있다. "수정 전에는 입장 처리 (버그 재현)"는 앞에서 말한 모호함을 사실로 바꾸는 부분이고, "수정 후에는 거절"은 수정을 증명하는 부분이다. 여기에 마지막 문장이 중요하다 — "정상 주문의 입장은 여전히 되는지(회귀 확인)". 버그를 고치다 보면 멀쩡하던 것을 망가뜨리기 쉽다. "환불 주문을 막아라"를 너무 세게 적용해서 정상 고객까지 못 들어가게 만들면, 버그 하나 잡으려다 더 큰 사고를 낸다. 그래서 수정은 항상 '막아야 할 것은 막혔는가'와 '되어야 할 것은 여전히 되는가'를 짝으로 확인한다. 이 짝 확인을 회귀 테스트(regression test) 라고 부른다 — 앞으로 걸어가려다 뒷걸음질(regression) 치지 않았는지 보는 것이다.
판정표는 이런 모양이 되면 좋다. 결과가 한눈에 들어온다.
| 케이스 | 수정 전 | 수정 후 | 기대 |
|---|---|---|---|
| 환불 주문 QR로 입장 | 입장됨(버그) | 거절(409/403) | 거절 |
| 정상 주문 QR로 입장 | 입장됨 | 입장됨 | 입장 |
첫 줄이 '입장됨 → 거절'로 바뀌면 버그가 잡힌 것이고, 둘째 줄이 '입장됨 → 입장됨'으로 유지되면 회귀가 없는 것이다. 이 표 한 장이 "고쳤다"는 주장을 증명으로 바꾼다.
[짚고 가기] 프롬프트 네 개(수집·근본원인·수정·검증)는 사실 하나의 흐름이라는 점을 기억해 두면 좋다. 각각 따로 외우는 게 아니라, 장애 대응 절차의 각 단계에 대응한다. 특히 "수정은 하지 마 — 원인 후보까지만"(수집 단계)과 "명세부터"(수정 단계)는 이 장의 두 원칙(분석/수정 분리, 근본 원인부터)을 프롬프트로 구현한 것이다. 이걸 알면 프롬프트를 통째로 외우는 대신 원리로 응용할 수 있다.
이제 시나리오를 실제 절차로 처리한다. 목표는 버그를 고치는 것만이 아니라, 감정이 아니라 근거로, 증상이 아니라 근본 원인으로, 일회성이 아니라 방어선으로 장애를 다루는 몸에 익히는 것이다. 먼저 조사만 하는 역할을 세우고, 그다음 여섯 단계를 순서대로 따라간다.
조사를 시작하기 전에, 조사만 하는 역할부터 세운다. 앞에서 정한 대로 이 역할에는 읽기·검색·조회 도구만 주고, 파일을 고치거나 새로 쓰는 도구는 주지 않는다. 코드 리뷰어·QA·보안검토자를 만들 때와 같은 방식이다 — 보는 역할은 서브에이전트로, 권한은 필요한 만큼만.
.claude/agents/incident-responder.md 로 장애 대응 서브에이전트를 만들어줘.
- description: 장애·오류 신고를 분석할 때 사용. 장애 분석, 원인 추적,
포스트모템 초안 요청이 오면 이 에이전트에 위임
- tools 는 Read, Grep, Glob, Bash 만. Edit·Write 는 절대 넣지 마
(Bash 는 로그 검색·데이터 조회·curl 같은 읽기 목적으로만 쓴다)
- 역할: (주)승승장구 온티켓의 장애 대응 담당. 산출물은 수정이 아니라
근거가 붙은 타임라인과 원인 후보다
- 프로세스:
1. 신고 문장을 사실 / 전언 / 모름으로 가른다
2. 데이터(orders·checkins)와 로그(data/logs/app_error.log)를 대조해
시간순 타임라인을 만든다 — 발견마다 근거(주문번호·시각·로그 줄)를 붙인다
3. 코드(api/)와 명세(docs/API-Spec.md)를 대조해 "무엇을 검사하지 않는가"를 찾는다
4. "왜"를 끝까지 내려가 근본 원인 후보를 보고한다
- 금지: 코드·명세·설정·데이터를 바꾸지 않는다(DB는 조회만, 변경·삭제 명령 금지),
근거 없는 추정 금지, 사람을 탓하는 문장 금지,
수정 방향은 제안까지만(결정과 수정은 사람과 만드는 역할의 몫)
위 프롬프트를 그대로 복사해 입력하면 된다. 다른 내용으로 바꿔서 해 보고 싶다면 자유롭게 바꿔도 좋다.
생성된 .claude/agents/incident-responder.md 를 직접 열어 tools: 줄을 확인한다.
Read, Grep, Glob, Bash 네 개만 있고 Edit·Write가 없어야 한다. 여기에 Edit가
끼어 있으면 조사자가 조사 도중 코드를 고칠 수 있게 되어, '분석과 수정의 분리'가
파일 한 줄에서 깨진다. Bash가 들어 있는 이유는 로그를 검색하고 주문·입장 데이터를
조회해야 하기 때문이다 — 그래서 금지 칸에 "조회만"을 따로 못 박았다. 도구는
권한으로 막고, 도구 안에서의 선은 역할 정의의 문장으로 막는다.
다음 따라하기에서 "그런 서브에이전트를 찾을 수 없다"는 답이 나오면, Claude Code를
한 번 껐다 켠 뒤(/exit 후 다시 claude) 이어 간다. 새로 만든 역할 파일은
시작할 때 읽히기 때문이다.
아래 사실 수집 지시를 실행한다. incident-responder가 orders와 checkins를 대조해 환불인데 입장된 주문을 실제로 찾아내는지 본다. 데이터에는 이런 건들이 실재한다(운영 데이터에 심어져 있다). 몇 건인지, 어느 공연인지 — 막연한 신고가 구체적 목록이 되는 과정을 지켜본다.
incident-responder 서브에이전트에게 맡겨줘. 신고: "환불된 주문의 QR로
입장이 된다." 다음을 사실만 수집해서 타임라인으로 정리해:
1) orders 에서 상태가 '환불'인 주문 목록
2) checkins 에서 그 환불 주문들의 입장 기록이 있는지 대조
3) 관련 로그에서 입장 확인(checkin) 처리 흔적
추정 말고 데이터로. 수정은 하지 마 — 원인 후보까지만.
위 프롬프트를 그대로 복사해 입력하면 된다. 다른 내용으로 바꿔서 해 보고 싶다면 자유롭게 바꿔도 좋다.
여기서 두 가지를 눈으로 확인하자. 첫째, incident-responder가 파일을 고치겠다는 퍼미션을 요청하지 않는다. 이 역할은 읽기·검색만 하므로, 데이터를 조회하고 대조할 뿐 아무것도 바꾸지 않는다. "조사자는 손대지 않는다"는 원칙이 화면에서 실제로 보인다. 둘째, 결과가 "환불인데 입장된 게 있다"는 막연한 문장이 아니라 주문번호가 박힌 목록으로 나오는지 본다. 목록이 나오는 순간, 신고 속의 "몇 건인지 모르겠어요"라는 빈칸이 채워진다 — 이것이 사실 수집의 완성이다.
로그에 입장 처리 흔적이 남아 있는지 본다. 이때 로그에 전화번호·이메일이 그대로 찍혀 있는 것도 발견될 수 있다 — 장애를 파다가 두 번째 문제(개인정보 로그 노출)를 만나는 것이다. 실제 운영에서 흔한 일이다. 발견은 기록해 두고, 수정 목록에 넣는다.
이 단계는 두 가지를 가르친다. 하나는 교차 확인이다. 데이터(checkins)가 "입장됐다"고 말하고, 로그도 "그 시각에 입장 처리했다"고 말하면, 두 개의 독립된 증거가 같은 사실을 가리키는 것이라 확신이 커진다. 서로 다른 출처가 일치할 때 사실은 단단해진다. 다른 하나는 장애 조사 중에 새 문제를 만나는 경험이다. 현실의 조사는 깔끔하게 한 가지만 나오지 않는다. 입장 버그를 파다가 개인정보 로그라는 별개의 사고를 발견한다. 이때 흔들리지 말고 원칙대로 한다 — 지금 당장 고치려 손대지 말고, 발견 목록에 기록했다가 수정 단계에서 함께 처리한다. 조사 중에 손을 대면 증거가 흐트러진다는 원칙이 여기서도 지켜진다.
아래 근본 원인 지시를 실행한다. "코드가 잘못됐다"가 아니라 "명세가 입장 조건을 QR 존재로만 정의했다"는 밑바닥까지 내려가는지 본다. 탓할 사람이 아니라 빈 방어선을 찾는 것 — 이 관점을 직접 경험한다.
방금 찾은 사실을 근거로, 왜 환불 주문이 입장 처리됐는지 근본 원인을 찾아줘.
입장 확인 로직(POST /checkins)이 무엇을 검사하고 무엇을 안 검사하는지,
코드와 명세(API-Spec)를 대조해서. "왜"를 끝까지 내려가.
위 프롬프트를 그대로 복사해 입력하면 된다. 다른 내용으로 바꿔서 해 보고 싶다면 자유롭게 바꿔도 좋다.
AI가 첫 답에서 "코드에 검사가 빠졌다"까지만 가고 멈추면, 아래 추가 지시로 명세까지 밀어 내린다.
거기서 멈추지 말고 한 단계 더. 코드가 환불 여부를 안 본 이유가 뭐야?
명세(docs/API-Spec.md)의 POST /checkins 항목에 입장 조건이 어떻게 적혀
있는지 그대로 인용해서, 코드가 명세를 어긴 건지 아니면 명세를 충실히 따른
건지 판정해줘.
위 프롬프트를 그대로 복사해 입력하면 된다. 다른 내용으로 바꿔서 해 보고 싶다면 자유롭게 바꿔도 좋다. 이 밀어 내리는 경험 자체가 실습의 핵심이다. 근본 원인은 대개 한 번의 '왜'로는 안 나오고, 누군가 한 계단 더 파야 나온다. 그 '한 계단 더'를 학생이 직접 시켜 보게 한다. 그리고 명세를 인용한 판정이 "코드는 명세를 충실히 따랐다"로 나오는 순간, 아무도 잘못 짜지 않았는데 사고가 났다는 이 장의 가장 중요한 통찰이 눈앞에서 확인된다.
아래 수정 지시를 실행한다. 명세→코드→테스트 순서로 메운다. 순서가 중요하다 — 명세를 그대로 두고 코드만 고치면, 다음에 명세를 본 사람(또는 AI)이 다시 구멍을 만든다.
근본 원인이 확인됐어. 다음 순서로 고쳐줘:
1) docs/API-Spec.md 의 POST /checkins 조건을 고친다:
"유효한(환불/취소되지 않은) 주문의 QR만 입장 허용. 환불 주문은 409/403 거절"
2) /backend 로 api 코드를 명세대로 수정 — 입장 전에 주문 상태를 검사
3) 이 케이스를 막는 테스트 추가: 환불 주문 QR로 입장 시도 → 거절되어야
4) 로그에 개인정보가 남던 부분이 있으면 마스킹까지
위 프롬프트를 그대로 복사해 입력하면 된다. 다른 내용으로 바꿔서 해 보고 싶다면 자유롭게 바꿔도 좋다.
수정 단계에서는 만드는 역할이 파일을 고치므로 퍼미션 요청이 다시 등장한다. 앞의 조사 단계(퍼미션 없음)와 대비된다 — 조사자는 손대지 않았고, 수정자는 손대기 전에 사람의 승인을 받는다. 이때 습관적으로 승인하지 말고, 명세가 의도한 대로 고쳐지는지(입장 조건에 '유효한 주문' 문구가 들어갔는지), 코드가 정상 입장까지 막아 버리진 않는지 훑어보고 승인한다. 장애 상황일수록 급하다고 읽지 않고 누르기 쉬운데, 그 성급한 승인이 2차 사고를 부른다는 것을 이 순간에 직접 체감한다. 그리고 로그를 이중 확인할 때 기록해 둔 로그 마스킹도 이 수정에 함께 들어가는지 확인한다.
아래 검증 지시로 수정 전(버그 재현)→수정 후(거절)를 확인하고, 정상 입장은 여전히 되는지(회귀) 본다. 그리고 이 수정을 PR로 올려 CI(테스트+자동 리뷰)를 통과시킨다. 우리가 3일간 세운 관문들이 이 수정을 지켜 주는 것을 본다 — 새 테스트가 CI에서 돌고, 자동 리뷰가 확인하고, 초록불이어야 배포된다.
qa 서브에이전트에게: 수정 전후를 비교 검증해줘.
환불 상태인 주문의 QR로 POST /checkins 를 시도했을 때,
수정 전에는 입장 처리(버그 재현), 수정 후에는 거절되는지 실제로 확인해서
판정표로. 그리고 정상 주문의 입장은 여전히 되는지도(회귀 확인).
위 프롬프트를 그대로 복사해 입력하면 된다. 다른 내용으로 바꿔서 해 보고 싶다면 자유롭게 바꿔도 좋다.
이 순간이 이 과정의 방어선들이 한자리에 모이는 장면이다. 되짚어 보자. 설계 단계에서 쓴 API 명세가 근본 원인을 담는 그릇이 됐고, 이번에 그 명세를 고쳐 후속 작업 전체를 보호했다. 자동화된 테스트가 이 버그를 상시 감시하는 방어선이 됐다. 리뷰·QA·보안 역할이 세운 자동 리뷰가 PR에서 다시 한 번 확인한다. CI가 이 모든 관문을 통과해야만 배포되게 막는다. 처음 이 장애가 났을 때는 이 방어선들이 그 자리에 비어 있었다(스위스 치즈의 구멍들이 한 줄로 늘어섰다). 지금 우리는 그 구멍들을 서로 어긋나게 다시 배치하는 중이다. 버그를 고친 것으로 끝이 아니라, 같은 버그가 다시는 이 관문들을 통과하지 못하게 만드는 것 — 이것이 이 단계의 진짜 목표다.
이 장애의 포스트모템을 docs/postmortem/001-환불주문-입장.md 로 써줘.
형식: 무슨 일이(증상) / 타임라인 / 근본 원인 / 영향 범위(환불인데 입장된
건수) / 조치 / 재발 방지(명세 조건 추가, 테스트 추가, 로그 마스킹).
사람을 탓하는 문장은 넣지 마 — 어느 방어선이 비어 있었는지로 쓴다.
위 프롬프트를 그대로 복사해 입력하면 된다. 다른 내용으로 바꿔서 해 보고 싶다면 자유롭게 바꿔도 좋다.
이 프롬프트의 마지막 문장이 포스트모템의 성격을 결정한다. "사람을 탓하는 문장은 넣지 마 — 어느 방어선이 비어 있었는지로 쓴다." 이 한 줄이 반성문과 재발 방지 설계도를 가른다. AI에게 형식(증상·타임라인·근본 원인·영향 범위·조치·재발 방지)을 번호로 주면, 앞 단계들에서 모은 사실이 그 칸에 그대로 채워진다 — 신고를 사실로 바꾼 단계의 목록이 '영향 범위'로, 5 Why 결과가 '근본 원인'으로, 명세 수정과 재현 증명 단계의 수정이 '조치'와 '재발 방지'로.
작성된 포스트모템을 반드시 소리 내어 읽어 보게 하자. 확인할 것은 하나다 — 어디에도 사람 이름이나 "~했어야 했다" 같은 질책이 없고, 전부 구조와 방어선의 언어로 쓰여 있는가. "환불 처리 담당자가 실수해서"가 아니라 "명세에 입장 조건이 QR 존재로만 정의되어 있었고, 그 조건 누락을 잡는 테스트가 없었다"로. 만약 질책하는 문장이 하나라도 있으면, "그 사람이 없었어도 이 사고가 났을까?"를 물어 방어선의 언어로 바꾼다.
작성된 포스트모템을 읽는다. 이 문서가 다음 장애를 줄인다 — 온티켓의 첫 운영 지식이 저장소에 남았다.
한 걸음 더 나아가면, 이 포스트모템은 온티켓 밖으로도 번진다. 용어 정리에서 본 골목 라이브 초과 판매나 매장 클레임도 같은 형식으로 남길 수 있다. 오대표가 다음 공연을 기획할 때 이 문서철을 펼쳐 "지난번 어디가 비어 있었지"를 확인한다면, 승승장구는 같은 실수를 두 번 하지 않는 조직이 된다. 한 번의 사고를 조직의 자산으로 바꾸는 것 — 포스트모템의 마지막 목적이다.
[짚고 가기] 이 장의 마지막 실습인 만큼, 조사부터 기록까지 여섯 단계가 하나의 이야기로 이어졌음을 되짚어 보면 마무리가 깔끔하다. 막연한 신고(입장 게이트의 신고) → 구체적 목록(신고를 사실로) → 명세의 구멍이라는 근본 원인 → 명세부터 수정 → 방어선 통과(재현으로 증명) → 자산화(포스트모템). 그리고 이 흐름은 3일 내내 배운 모든 것(명세·테스트·리뷰·CI·퍼미션·역할 분리)을 한 번에 소환한다. 그 점을 짚어 보면, 과정 전체가 이 장에서 하나로 묶인다.
.claude/agents/incident-responder.md 가 존재하고 tools 에 Edit·Write가 없다