Post

SAGA 패턴 실패 복구

원자성을 포기하고 보상 트랜잭션·오케스트레이션·멱등 키로 실패를 관리하는 SAGA 설계 원칙

SAGA 패턴 실패 복구

이 글의 결론: SAGA는 성공을 연결하는 패턴이 아니라 실패를 감당하기 위해 원자성을 포기하는 설계다.

분산 시스템에서 트랜잭션의 본질은 “모두 성공”이 아니라 “실패했을 때 어디까지 되돌리고, 어디서 멈출 것인가”다. SAGA는 그 결정을 코드와 이벤트로 고정한다.


SAGA가 필요한 이유 결론: 분산 환경에서는 단일 트랜잭션의 원자성을 유지할 수 없다.

  • 여러 서비스/DB에 걸친 ACID 트랜잭션은 현실적으로 불가능하다.
  • 대신 선택지는 둘:

    1. 원자성을 포기하고 최종 일관성을 수용
    2. 실패 시 보상 트랜잭션으로 상태를 수습 SAGA는 (2)를 체계화한 설계다. 즉, 실패를 정상 시나리오로 격상시킨다.

오케스트레이션 vs 코레오그래피 결론: 제어점을 어디에 둘지의 선택이 곧 운영 비용이다.

비교 요약

구분오케스트레이션코레오그래피
제어점중앙(SAGA Orchestrator)분산(각 서비스)
결합도상대적으로 높음낮음
디버깅쉬움(단일 흐름)어려움(흩어진 이벤트)
변경 비용중앙 수정다수 서비스 수정
관측성높음낮음

이 강의의 맥락(주문 중심 수명주기, 실패 시 판단 필요)에서는 오케스트레이션이 합리적이다. 이유는 단순하다. 실패 판단은 중앙에서 내려야 재현·검증이 가능하기 때문이다.


주문–결제 SAGA 결론: 주문 상태 전이는 이벤트의 조합으로만 확정된다.

상태 전이 다이어그램(텍스트)

1
2
3
4
5
6
7
8
9
10
11
12
13
[START]
  |
  v
ORDER_CREATED
  |
  |-- PaymentApproved --> PAYMENT_OK
  |-- PaymentRejected --> ORDER_REJECTED (END)
  |
  v
PAYMENT_OK
  |
  |-- RestaurantAccepted --> ORDER_APPROVED (END)
  |-- RestaurantRejected --> ORDER_REJECTED (COMPENSATE: Refund)
  • 단일 이벤트로 최종 상태를 정하지 않는다.
  • 조합 조건이 충족될 때만 전이한다.
  • 보상은 이미 일어난 사실을 되돌리는 시도다.

보상 트랜잭션 결론: 보상은 롤백이 아니라 반대 방향의 비즈니스 행위다.

  • DB 롤백 ❌
  • 예:

    • 결제 승인 → 환불 요청
    • 레스토랑 예약 → 예약 취소 보상은 항상 성공하지 않을 수 있음을 전제로 설계해야 하며, 실패 시 추가 보상/수동 개입 경로가 필요하다.

실패 시나리오 5개와 대응 결론: 모든 실패는 “판단 + 조치 + 기록”으로 끝나야 한다.

1) 결제 실패

  • 신호: PaymentRejected
  • 판단: 주문 진행 불가
  • 조치: 주문 REJECTED
  • 보상: 없음(선행 성공 없음)

2) 중복 결제 요청

  • 신호: 동일 paymentAttemptId
  • 판단: 이미 처리됨
  • 조치: 무시
  • 보상: 없음
  • 핵심: 멱등 키

3) 메시지 지연

  • 신호: SLA 초과(타이머)
  • 판단: 아직 실패 아님
  • 조치: 대기 또는 재확인 이벤트
  • 보상: 지연 기준 초과 시에만

4) 순서 역전

  • 신호: RestaurantAcceptedPaymentApproved보다 먼저 도착
  • 판단: 조건 미충족
  • 조치: 임시 저장 후 대기
  • 보상: 없음

5) 재처리로 인한 중복 상태 전이

  • 신호: 동일 이벤트 재수신
  • 판단: 이미 전이됨
  • 조치: 무시
  • 보상: 없음
  • 핵심: 상태 머신 검증

테스트의 중요성 결론: 분산에서 버그는 재현이 아니라 시나리오로 잡는다.

  • 단일 테스트로 재현 ❌
  • 해야 할 것:

    • 실패 시나리오별 상태 전이 테스트
    • 이벤트 순서 변경 테스트
    • 중복/재처리 테스트 SAGA 테스트는 “코드 테스트”가 아니라 결정 테스트다.

실패 대응 표 결론: 실패를 분류하지 않으면 대응은 늦어진다.

실패 유형발생 위치탐지 신호대응데이터 영향
결제 실패PaymentRejected 이벤트종료주문 REJECTED
중복 요청Payment동일 키무시없음
메시지 지연KafkaSLA 초과대기/재시도일시 불일치
순서 역전Saga전이 불가버퍼링없음
재처리 중복Saga상태 불일치무시없음

흔한 오해 / 실수 3가지

  1. SAGA를 “정상 흐름 오케스트레이션”으로 이해하는 오해
    • 문제: 성공 시나리오만 코드로 표현, 실패 분기는 로그 처리로 축소
    • 결과: 실제 장애 시 어디서 멈춰야 하는지 판단 불가
    • 교정 기준: SAGA는 실패 복구 설계가 80%다. 정상 흐름은 부수적이다.
  2. 보상 트랜잭션을 DB 롤백처럼 취급
    • 문제: “되돌리면 된다”는 사고
    • 결과: 환불/취소 실패 시 무한 재시도 또는 데이터 불일치
    • 교정 기준: 보상은 반대 방향의 비즈니스 행위이며 실패 가능성을 전제한다.
  3. 중앙 오케스트레이터가 모든 로직을 아는 구조
    • 문제: 결제/레스토랑 내부 정책까지 SAGA가 판단
    • 결과: 오케스트레이터 비대화, 변경 비용 폭증
    • 교정 기준: SAGA는 상태 조합 판단만, 개별 결정은 각 서비스 몫.

대표 실패 시나리오 3가지

  1. 결제 성공 후 레스토랑 거절 → 환불 실패
    • 상황: PaymentApproved → RestaurantRejected → Refund 실패
    • 원인: 보상 실패 시나리오 미정의
    • 결과: 주문 REJECTED지만 결제는 완료 상태
    • 대응: 보상 실패를 별도 상태로 승격 + 수동 개입 큐로 전환
  2. 이벤트 순서 역전으로 SAGA가 잘못 종료되는 경우
    • 상황: RestaurantAccepted가 PaymentApproved보다 먼저 도착
    • 원인: 분산 이벤트 순서 보장 오해
    • 결과: 조건 미충족 상태에서 승인/종료 판단
    • 대응: 상태 머신 기반 “조건 충족 대기” + 임시 저장
  3. 재처리 시 이미 종료된 주문이 다시 전이되는 경우
    • 상황: 오프셋 리셋 후 과거 이벤트 재소비
    • 원인: 상태 전이 멱등성 미검증
    • 결과: 승인→거절 등 불가능한 전이 발생
    • 대응: 현재 상태에서 허용된 전이만 수용(상태 머신 검증)

실무 체크리스트 (SAGA 실패 복구 설계 점검)

  1. 이 SAGA에서 포기한 원자성 범위를 문장으로 설명할 수 있는가
  2. 최종 상태 결정 조건이 코드로 명시돼 있는가
  3. 실패 이벤트가 정상 이벤트만큼 중요하게 다뤄지는가
  4. 보상 트랜잭션이 비즈니스 행위로 정의돼 있는가
  5. 보상 실패 시 다음 단계(대기/수동 개입)가 있는가
  6. 중복 이벤트를 무시할 기준 키가 있는가
  7. 순서 역전을 허용하는 저장/대기 로직이 있는가
  8. 메시지 지연을 실패로 오인하지 않는가
  9. 재처리 시 상태 전이가 안전한가
  10. 오케스트레이터가 도메인 정책을 침범하지 않는가
  11. 실패 시나리오별 로그/메트릭이 분리돼 있는가
  12. 정상 흐름보다 실패 흐름 테스트가 더 많은가

This post is licensed under CC BY 4.0 by the author.