Saga 패턴이란?
Saga 패턴은 여러 서비스에 걸친 하나의 비즈니스 작업을 여러 개의 로컬 트랜잭션으로 나누어 처리하는 분산 트랜잭션 패턴이다.
각 서비스는 자신의 데이터베이스에서 로컬 트랜잭션을 커밋한다. 도중에 작업이 실패하면 앞에서 완료한 작업에 대해 보상 트랜잭션을 실행하거나, 재시도를 통해 남은 작업을 끝까지 완료한다.
주문 생성 → 재고 예약 → 결제 승인 → 배송 요청
Saga를 구성하는 트랜잭션은 역할에 따라 다음 세 종류로 구분할 수 있다.
- Compensable Transaction
- Pivot Transaction
- Retryable Transaction
1. Compensable Transaction
Compensable Transaction은 실패했을 때 반대 작업을 통해 보상할 수 있는 트랜잭션이다.
Saga의 Pivot 이전 단계에 위치하며, 이후 과정에서 문제가 발생하면 해당 작업의 영향을 취소하는 보상 트랜잭션을 실행한다.
주문 생성 ↔ 주문 취소
재고 예약 ↔ 재고 예약 해제
쿠폰 사용 ↔ 쿠폰 복원
포인트 사용 ↔ 포인트 반환
예를 들어 재고 예약까지 성공했지만 결제에 실패했다면 재고 예약을 해제하고 주문을 취소해야 한다.
주문 생성 성공
→ 재고 예약 성공
→ 결제 실패
→ 재고 예약 해제
→ 주문 취소
데이터베이스 롤백과의 차이
보상 트랜잭션은 데이터베이스의 ROLLBACK이 아니다.
각 로컬 트랜잭션은 이미 커밋된 상태이기 때문에 변경 내용을 삭제하는 것이 아니라, 이전 작업을 상쇄하는 새로운 비즈니스 트랜잭션을 실행한다.
예를 들어 결제 취소는 결제 기록을 삭제하는 것이 아니라 상태를 변경하고 환불 내역을 추가하는 작업이 될 수 있다.
PAYMENT_COMPLETED
→ PAYMENT_CANCEL_REQUESTED
→ PAYMENT_CANCELLED
따라서 보상 결과가 실행 전 상태와 완전히 동일하지 않을 수도 있다. 중요한 것은 시스템을 비즈니스적으로 일관된 상태로 만드는 것이다.
2. Pivot Transaction
Pivot Transaction은 Saga의 성공과 실패를 결정하는 기준점이자, 사실상 되돌릴 수 없는 지점이다.
Pivot이 실패하면 이전의 Compensable Transaction들을 보상한다. 반대로 Pivot이 성공하면 Saga는 이전 상태로 돌아가기보다 이후 작업을 끝까지 완료해야 한다.
Compensable Transaction
↓
Pivot Transaction
↓
Retryable Transaction
예를 들어 주문 처리에서 실제 결제 승인을 Pivot으로 설정할 수 있다.
주문 생성 → 재고 예약 → 결제 승인 → 배송 요청
보상 가능 보상 가능 Pivot 재시도
결제 승인 전까지는 주문 취소와 재고 예약 해제로 작업을 되돌릴 수 있다. 결제 승인이 완료된 후에는 단순 롤백보다 배송 요청과 후속 처리를 끝까지 완료하는 방향으로 진행한다.
다만 어떤 작업을 Pivot으로 볼지는 비즈니스에 따라 달라진다.
쇼핑몰 : 결제 승인 또는 출고 확정
은행 송금 : 외부 계좌로 송금 완료
예약 시스템: 최종 예약 확정
물류 시스템: 실제 출고 시작
Pivot은 기술적으로 되돌릴 수 없는 작업일 수도 있고, 비용이나 업무 규칙 때문에 되돌려서는 안 되는 작업일 수도 있다.
3. Retryable Transaction
Retryable Transaction은 Pivot 이후에 실행되며, 일시적으로 실패하더라도 성공할 때까지 재시도할 수 있어야 하는 트랜잭션이다.
Pivot이 이미 성공했다면 이전 단계로 돌아가는 대신 남은 작업을 완료해 Saga를 일관된 최종 상태로 만들어야 한다.
결제 승인 성공
→ 배송 요청 실패
→ 일정 시간 후 재시도
→ 배송 요청 성공
Retryable Transaction은 다음 조건을 만족해야 한다.
- 같은 요청을 반복해도 결과가 동일해야 한다.
- 실행 상태를 저장해야 한다.
- 일시적 장애에 대한 재시도 정책이 있어야 한다.
- 무한 재시도를 방지해야 한다.
- 자동 복구가 불가능하면 운영자에게 알려야 한다.
특히 Retryable Transaction은 반드시 멱등성을 보장해야 한다.
동일한 배송 요청을 두 번 전달
→ 배송이 두 건 생성되면 안 됨
→ 기존 배송 요청 결과를 반환해야 함
멱등성을 위해 sagaId, orderId, requestId와 같은 식별자를 이용해 이미 처리된 요청인지 확인할 수 있다.
전체 실행 흐름
주문 처리 Saga를 세 가지 트랜잭션으로 분류하면 다음과 같다.
[보상 가능한 정상 트랜잭션]
주문 생성
└─ 대응하는 보상 트랜잭션: 주문 취소
↓
[보상 가능한 정상 트랜잭션]
재고 예약
└─ 대응하는 보상 트랜잭션: 재고 예약 해제
↓
[Pivot 트랜잭션]
결제 승인
↓
[재시도 가능한 정상 트랜잭션]
배송 요청
↓
[재시도 가능한 정상 트랜잭션]
주문 완료 처리
Pivot 이전에 실패한 경우
주문 생성 성공
→ 재고 예약 성공
→ 결제 승인 실패
→ 재고 예약 해제
→ 주문 취소
이 경우에는 이전에 완료한 Compensable Transaction의 보상 작업을 실행한다.
Pivot 이후에 실패한 경우
결제 승인 성공
→ 배송 요청 실패
→ 배송 요청 재시도
→ 배송 요청 성공
→ 주문 완료
Pivot이 성공했으므로 이전 작업을 취소하기보다 Retryable Transaction을 재실행해 전체 Saga를 완료한다.
영어정확한 의미주문 예시
| Compensable Transaction | 나중에 보상할 수 있는 정상 트랜잭션 | 주문 생성 |
| Compensating Transaction | 앞에서 성공한 작업을 취소하는 트랜잭션 | 주문 취소 |
| Pivot Transaction | Saga 진행 방향을 결정하는 기준점 | 결제 승인 |
| Retryable Transaction | Pivot 이후 성공할 때까지 재시도할 작업 | 배송 요청 |
Forward Recovery와 Backward Recovery
Saga의 복구 전략은 두 가지 방향으로 이해할 수 있다.
Backward Recovery
이전에 완료한 작업을 보상해 Saga 시작 전과 비슷한 상태로 돌아간다.
주문 생성 → 재고 예약 → 결제 실패
↓
재고 해제 → 주문 취소
주로 Pivot 이전에 실패했을 때 사용한다.
Forward Recovery
이미 성공한 작업은 유지하고, 실패한 작업을 재시도해 Saga를 끝까지 진행한다.
결제 완료 → 배송 요청 실패
↓
배송 요청 재시도
↓
주문 완료
주로 Pivot 이후에 사용한다.
구현할 때 주의할 점
보상 트랜잭션도 실패할 수 있다
보상 작업 역시 일반적인 네트워크 요청이므로 실패할 수 있다. 따라서 보상 처리에도 재시도, 상태 저장, 실패 알림이 필요하다.
모든 작업을 보상할 수 있는 것은 아니다
이메일 발송, 외부 송금, 실제 상품 출고처럼 이미 외부에 영향을 준 작업은 완벽하게 되돌리기 어렵다. 이런 작업은 Pivot이나 Retryable Transaction으로 분류하는 것이 적절한지 검토해야 한다.
중복 실행을 고려해야 한다
메시지가 중복 전달되거나 응답이 유실되면 같은 작업이 여러 번 실행될 수 있다. 정상 작업과 보상 작업 모두 멱등하게 설계해야 한다.
중간 상태가 외부에 노출된다
Saga는 각 트랜잭션을 즉시 커밋하기 때문에 처리 중인 상태가 다른 요청에 조회될 수 있다.
PENDING
→ PROCESSING
→ COMPLETED
또는
PENDING
→ COMPENSATING
→ CANCELLED
따라서 중간 상태를 명시적으로 관리하고, 상태별로 허용되는 작업을 제한해야 한다.
정리
Saga 패턴에서는 트랜잭션의 역할을 다음과 같이 구분한다.
| Compensable | 보상할 수 있는 선행 작업 | 보상 트랜잭션 실행 |
| Pivot | 되돌릴 수 없는 기준점 | 이전 작업 보상 또는 이후 단계 진입 결정 |
| Retryable | Pivot 이후 반드시 완료할 작업 | 성공할 때까지 안전하게 재시도 |
핵심 흐름은 다음과 같다.
Compensable → Pivot → Retryable
Pivot 이전의 실패는 보상 트랜잭션을 통한 Backward Recovery, Pivot 이후의 실패는 재시도를 통한 Forward Recovery로 처리한다.
Saga를 안정적으로 구현하려면 단순히 작업 순서만 연결해서는 안 된다. 각 작업이 보상 가능한지, 어디를 Pivot으로 정할지, Pivot 이후의 작업이 멱등하게 재시도될 수 있는지를 먼저 정의해야 한다.