Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

65장. 실전 2 — 장애 티켓 하나로 문제 해결하기

64장에서 결제 재시도를 배포했다.

배포 이틀 뒤, 새벽 두 시.

[알림] PG 중복 결제 의심 건 감지 — 4건

64장 끝에서 “놓친 것이 하나 있다” 고 했다.
그것이 여기서 드러난다.


02:14 — 상황 판단 (사람)

알림을 보고 가장 먼저 하는 일은
Agent를 부르는 것이 아니다.

1. 지금도 발생 중인가
2. 몇 건인가
3. 돈이 실제로 이중 청구됐는가

대시보드에서 확인했다.

중복 결제 의심: 4건
전부 최근 3시간 내
지금도 발생 중 ⚠️

사람이 결정한 것 ① — 즉시 완화.

결제 재시도 피처 플래그 off

56장에서 말한 대로 완화가 먼저다.

플래그를 끄는 것은 Agent에게 맡기지 않는다.
59장의 목록에 있는 항목이다.


02:19 — 조사 위임

완화가 됐으니 이제 조사다.

incident-responder 로 조사해줘.

증상:
- PG 중복 결제 의심 4건, 최근 3시간
- 어제 결제 재시도 기능 배포 (PAY-2841)
- 재시도 플래그는 방금 off 함

수정은 하지 마. 원인 가설과 확인 방법만.

56장에서 만든 읽기 전용 Agent다.
toolsEdit 이 없다.

내가 이 사이에 한 일은
고객센터에 상황 공유였다.

🔥 조사와 공지가 병렬로 진행됐다.

혼자 온콜을 서면서 둘을 동시에 하기 어려운데,
위임하면 가능해진다.


02:31 — 조사 결과

## 현재 상태
플래그 off 이후 추가 발생 없음.

## 확인한 사실
- 4건 모두 PG 타임아웃 후 재시도된 건 (payment_logs)
- 4건 모두 멱등키가 원 요청과 다름 ⚠️
- PaymentPgClient 는 멱등키를 그대로 전달함 (PaymentPgClient.kt:38)
- 그런데 재시도는 PaymentService.pay() 에 걸려 있음
  (PaymentService.kt:44 @Retryable)

## 가설 (유력한 순)
1. 재시도 지점이 PaymentService 라서 PayRequest 가
   새로 생성되며 멱등키도 새로 발급된다
   → 맞다면 재시도 요청의 키가 매번 달라야 함. 4건 모두 그렇다
2. PG 쪽 멱등성 시간창 문제 (가능성 낮음)
3. 클라이언트 중복 요청 (로그상 단일 요청)

## 확인하지 못한 것
- 정산 배치 영향 (배치는 03:00 실행 예정)

## 권장 조치
즉시: 플래그 off 유지 (완료)
근본: 재시도를 PgClient 레벨로 이동

⚠️ 1번 가설이 정확했다.

64장의 계획서에는 래퍼 클래스에 재시도를 붙이기로 되어 있었는데,
구현에서 PaymentService 레벨에 붙었다.

계획과 구현이 어긋난 것이다.


02:35 — 확인하지 못한 것 처리 (사람)

보고서 마지막 항목이 눈에 걸렸다.

정산 배치가 03:00 에 실행 예정

25분 뒤다.

사람이 결정한 것 ② — 배치 일시 중단.

🔥 19장에서 확인하지 못한 것 절을
반드시 쓰라고 한 이유가 여기서 나온다.

이 항목이 없었으면
25분 뒤에 두 번째 장애가 났을 수 있다.


09:00 — 재현 테스트

급한 불은 껐다. 수정은 아침에 한다.
56장의 순서대로다.

어젯밤 장애를 재현하는 테스트를 먼저 만들어줘.
PG 타임아웃 → 재시도 시 멱등키가 원 요청과 같아야 한다.
지금은 실패해야 정상이야.
@Test
fun `재시도 시 멱등키는 원 요청과 동일하다`() {
    mockPg.enqueue(timeout())
    mockPg.enqueue(success())

    paymentService.pay(request)

    val keys = mockPg.recordedRequests.map { it.idempotencyKey }
    assertThat(keys.distinct()).hasSize(1)   // 실패: 2
}
> ./gradlew test --tests '*PaymentRetryTest'
  재시도 시 멱등키는 원 요청과 동일하다  FAILED
    expected size: 1 but was: 2

원인이 확정됐다.


09:20 — 수정

재시도 지점을 PaymentPgClient.request() 로 옮겨줘.
계획서(@tasks/PAY-2841.md)의 원래 설계대로.

PaymentService 의 @Retryable 은 제거해줘.

수정 후 테스트가 통과했다.


09:40 — 왜 통과했는가

62장의 질문이다.

"누가 잘못했는가" 가 아니라 "왜 통과했는가"

세 층에서 놓쳤다.

놓친 이유
테스트멱등키 동일성을 검증하지 않았다
계획 대조Review에서 “계획대로인가” 를 형식적으로 봤다
아키텍처재시도 위치를 강제하는 규칙이 없었다

⚠️ 첫 번째가 핵심이다.

64장의 완료 조건에 이런 항목이 있었다.

- 같은 멱등키로 재시도해도 결제 1건 테스트

이 테스트는 통과했다.

같은 멱등키를 명시적으로 넘겼을 때
결제가 1건인지만 봤기 때문이다.

재시도가 멱등키를 유지하는지는 검증하지 않았다.

🔥 30장에서 말한 그대로다.

실패 주입 테스트가 없으면
복원력 코드는 검증된 적 없는 코드다.

테스트는 있었지만 주입 지점이 틀렸다.


10:10 — 하네스를 고친다

세 가지를 추가했다.

1. 테스트 (23장)

@Test fun `재시도 시 멱등키는 원 요청과 동일하다`()
@Test fun `재시도는 PgClient 레벨에서만 일어난다`()

2. Skill 체크리스트 (48장)

# .claude/skills/add-external-call/SKILL.md
- [ ] 재시도가 붙은 지점이 요청 객체 생성 지점보다 안쪽인가
      (밖에 붙으면 멱등키가 매번 새로 생성된다 — PAY-2841 장애)

3. 아키텍처 테스트 (42장)

@Test
fun `@Retryable 은 Infrastructure 계층에만 붙는다`() { ... }

⚠️ 세 번째가 가장 강하다.

문장이 아니라 조건이 됐다.

같은 실수가 다시 나오면 CI에서 막힌다.


10:30 — 기록

48장의 장애 분석 Skill 7번 항목이다.

# tasks/incident-2026-08-16.md

## 타임라인
02:14 알림 → 플래그 off   02:19 조사 위임
02:31 원인 확정          02:35 정산 배치 중단
09:40 근본 수정          10:10 하네스 개선 3건

## 원인
재시도가 PaymentService 레벨에 붙어 재시도마다 PayRequest 가
새로 생성되고 멱등키도 새로 발급됨. 계획서 설계와 구현이 어긋남.

## 왜 통과했는가
1. 멱등성 테스트가 "같은 키를 주면 1건" 만 검증
2. Review에서 계획 대조가 형식적이었음
3. 재시도 위치를 강제하는 규칙 없음

## 조치
즉시: 플래그 off, 배치 중단 / 근본: 재시도 지점 이동
하네스: 테스트 2건, Skill 항목 1개, 아키텍처 테스트 1건

## 확인하지 못한 것
- 중복 결제 4건의 환불 처리 (CS팀 진행 중)
- 정산 배치 재실행 시 영향 (내일 확인)

19장의 형식 그대로다.

⚠️ 마지막 절을 비우지 않았다.

급하게 처리한 장애일수록
미확인 항목이 많다.


정리

장애 인지 → 완화        5분
조사 (위임)             12분
2차 장애 예방           그 사이 사람이
근본 수정               다음 날 오전
하네스 개선             30분

사람이 결정한 것은 둘이었다.

① 즉시 플래그 off
② 정산 배치 중단

둘 다 되돌릴 수 없거나
지금 당장 해야 하는 일이었다.

🔥 그 사이 조사는 Agent가 했다.

56장에서 말한 병렬이
실제로 새벽 두 시에 이렇게 작동한다.


이 장의 핵심

  • 알림을 받고 가장 먼저 하는 일은 Agent 호출이 아니라 완화다
  • 플래그를 끄는 것은 사람이 한다 — 되돌릴 수 없는 실행이다
  • 조사를 위임하면 사람이 공지와 판단에 집중할 수 있다
  • 확인하지 못한 것 절이 25분 뒤의 2차 장애를 막았다
  • 계획서와 구현이 어긋난 것을 조사 과정에서 발견했다
  • “같은 키를 주면 1건” 테스트는 통과했지만 주입 지점이 틀렸다
  • 실패 주입 테스트가 있어도 지점이 틀리면 검증되지 않은 코드다
  • 질문은 “누가 잘못했는가” 가 아니라 “왜 통과했는가” 다
  • 하네스 개선 세 건 중 아키텍처 테스트가 가장 강하다 — 조건이 됐다
  • 급한 장애일수록 미확인 항목을 반드시 남긴다