SAGA 패턴 실패 복구
원자성을 포기하고 보상 트랜잭션·오케스트레이션·멱등 키로 실패를 관리하는 SAGA 설계 원칙
Posted
By okorion
SAGA 패턴 실패 복구
이 글의 결론: SAGA는 성공을 연결하는 패턴이 아니라 실패를 감당하기 위해 원자성을 포기하는 설계다.
분산 시스템에서 트랜잭션의 본질은 “모두 성공”이 아니라 “실패했을 때 어디까지 되돌리고, 어디서 멈출 것인가”다. SAGA는 그 결정을 코드와 이벤트로 고정한다.
SAGA가 필요한 이유 결론: 분산 환경에서는 단일 트랜잭션의 원자성을 유지할 수 없다.
- 여러 서비스/DB에 걸친 ACID 트랜잭션은 현실적으로 불가능하다.
대신 선택지는 둘:
- 원자성을 포기하고 최종 일관성을 수용
- 실패 시 보상 트랜잭션으로 상태를 수습 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) 순서 역전
- 신호:
RestaurantAccepted가PaymentApproved보다 먼저 도착 - 판단: 조건 미충족
- 조치: 임시 저장 후 대기
- 보상: 없음
5) 재처리로 인한 중복 상태 전이
- 신호: 동일 이벤트 재수신
- 판단: 이미 전이됨
- 조치: 무시
- 보상: 없음
- 핵심: 상태 머신 검증
테스트의 중요성 결론: 분산에서 버그는 재현이 아니라 시나리오로 잡는다.
- 단일 테스트로 재현 ❌
해야 할 것:
- 실패 시나리오별 상태 전이 테스트
- 이벤트 순서 변경 테스트
- 중복/재처리 테스트 SAGA 테스트는 “코드 테스트”가 아니라 결정 테스트다.
실패 대응 표 결론: 실패를 분류하지 않으면 대응은 늦어진다.
| 실패 유형 | 발생 위치 | 탐지 신호 | 대응 | 데이터 영향 |
|---|---|---|---|---|
| 결제 실패 | Payment | Rejected 이벤트 | 종료 | 주문 REJECTED |
| 중복 요청 | Payment | 동일 키 | 무시 | 없음 |
| 메시지 지연 | Kafka | SLA 초과 | 대기/재시도 | 일시 불일치 |
| 순서 역전 | Saga | 전이 불가 | 버퍼링 | 없음 |
| 재처리 중복 | Saga | 상태 불일치 | 무시 | 없음 |
흔한 오해 / 실수 3가지
- SAGA를 “정상 흐름 오케스트레이션”으로 이해하는 오해
- 문제: 성공 시나리오만 코드로 표현, 실패 분기는 로그 처리로 축소
- 결과: 실제 장애 시 어디서 멈춰야 하는지 판단 불가
- 교정 기준: SAGA는 실패 복구 설계가 80%다. 정상 흐름은 부수적이다.
- 보상 트랜잭션을 DB 롤백처럼 취급
- 문제: “되돌리면 된다”는 사고
- 결과: 환불/취소 실패 시 무한 재시도 또는 데이터 불일치
- 교정 기준: 보상은 반대 방향의 비즈니스 행위이며 실패 가능성을 전제한다.
- 중앙 오케스트레이터가 모든 로직을 아는 구조
- 문제: 결제/레스토랑 내부 정책까지 SAGA가 판단
- 결과: 오케스트레이터 비대화, 변경 비용 폭증
- 교정 기준: SAGA는 상태 조합 판단만, 개별 결정은 각 서비스 몫.
대표 실패 시나리오 3가지
- 결제 성공 후 레스토랑 거절 → 환불 실패
- 상황: PaymentApproved → RestaurantRejected → Refund 실패
- 원인: 보상 실패 시나리오 미정의
- 결과: 주문 REJECTED지만 결제는 완료 상태
- 대응: 보상 실패를 별도 상태로 승격 + 수동 개입 큐로 전환
- 이벤트 순서 역전으로 SAGA가 잘못 종료되는 경우
- 상황: RestaurantAccepted가 PaymentApproved보다 먼저 도착
- 원인: 분산 이벤트 순서 보장 오해
- 결과: 조건 미충족 상태에서 승인/종료 판단
- 대응: 상태 머신 기반 “조건 충족 대기” + 임시 저장
- 재처리 시 이미 종료된 주문이 다시 전이되는 경우
- 상황: 오프셋 리셋 후 과거 이벤트 재소비
- 원인: 상태 전이 멱등성 미검증
- 결과: 승인→거절 등 불가능한 전이 발생
- 대응: 현재 상태에서 허용된 전이만 수용(상태 머신 검증)
실무 체크리스트 (SAGA 실패 복구 설계 점검)
- 이 SAGA에서 포기한 원자성 범위를 문장으로 설명할 수 있는가
- 최종 상태 결정 조건이 코드로 명시돼 있는가
- 실패 이벤트가 정상 이벤트만큼 중요하게 다뤄지는가
- 보상 트랜잭션이 비즈니스 행위로 정의돼 있는가
- 보상 실패 시 다음 단계(대기/수동 개입)가 있는가
- 중복 이벤트를 무시할 기준 키가 있는가
- 순서 역전을 허용하는 저장/대기 로직이 있는가
- 메시지 지연을 실패로 오인하지 않는가
- 재처리 시 상태 전이가 안전한가
- 오케스트레이터가 도메인 정책을 침범하지 않는가
- 실패 시나리오별 로그/메트릭이 분리돼 있는가
- 정상 흐름보다 실패 흐름 테스트가 더 많은가
This post is licensed under CC BY 4.0 by the author.
