여러 서비스에 걸친 작업을 각자의 로컬 트랜잭션으로 나누고, 실패하면 보상 작업으로 되돌리는 패턴.
왜 2PC를 안 쓰나
2PC(2단계 커밋)
- ① 조정자가 전원에게 "커밋 가능?" 을 묻고
- ② 전원이 동의하면 커밋 명령
문제: 조정자가 1·2단계 사이에 죽으면
- 참여자들이 락을 쥔 채 무한정 기다린다 (블로킹)
- 한 서비스의 장애가 다른 서비스를 얼려 버린다
MSA에서 2PC를 피하는 것이 기본인 이유다.
Saga의 방식
주문 생성 → 결제 승인 → 재고 차감 → 배송 요청
↓ 실패
결제 취소 ← 주문 취소 (역순으로 보상)
각 단계가 독립된 로컬 트랜잭션이라 락을 오래 쥐지 않는다.
대가 — 중간 상태가 보인다
원자성이 없다
- "결제는 됐는데 재고가 없어 곧 취소됨" 같은 상태를 사용자가 볼 수 있다
- 그걸 UX 로 설계해야 한다 ("처리 중" 상태를 노출한다)
보상은 취소가 아니다
계좌에서 100원을 뺐다 ❌ 롤백 — 이미 커밋됐다. 그런 건 없다 ✅ 보상 — 100원을 다시 넣는 '새 트랜잭션' 을 실행한다
메일을 보냈다 → 보상이 불가능하다
- 되돌릴 수 없는 작업은 Saga 의 맨 뒤에 배치한다
두 가지 조율 방식
오케스트레이션 (중앙 조율)
- 조율자가 각 단계를 순서대로 호출하고 실패 시 보상을 부른다
- ✅ 흐름이 한곳에 보인다 · 디버깅이 쉽다
- ❌ 조율자가 모든 서비스를 알아야 한다
코레오그래피 (이벤트 연쇄)
- 각 서비스가 이벤트를 듣고 다음 이벤트를 낸다
- ✅ 결합이 낮다
- ❌ 전체 흐름이 어디에도 안 적혀 있다 · 추적이 어렵다
단계가 4개를 넘으면 오케스트레이션이 유지보수에 유리하다는 것이 일반적 경험칙이다.
보상이 실패하면
-
① 보상은 반드시 멱등하게 만든다 (여러 번 실행돼도 결과가 같게)
-
② 재시도한다
-
③ 그래도 실패하면 사람이 개입할 큐로 보낸다
-
"자동으로 완벽하게 되돌린다" 는 환상이다 — 개입 경로를 반드시 설계한다
면접 함정
- ❌ "Saga는 ACID를 보장한다" → 원자성을 포기하고 최종 일관성을 택한 것이다.
- ❌ "Saga를 쓰면 분산 트랜잭션이 해결된다" → 보상 로직·중간 상태·멱등성이라는 새 복잡도를 얻는다.
상태를 어디에 두나
-- 오케스트레이션이면 진행 상태를 저장해야 재시작 후 이어갈 수 있다
CREATE TABLE saga_instance (
id BIGINT PRIMARY KEY,
saga_type VARCHAR(50),
current_step VARCHAR(50), -- PAYMENT_DONE · STOCK_DONE ...
status VARCHAR(20), -- RUNNING · COMPENSATING · DONE · FAILED
payload JSONB,
updated_at TIMESTAMPTZ
);
조율자가 죽어도 이어갈 수 있어야 Saga가 완성된다. 메모리에만 두면 재시작 시 중간 상태가 영영 미완으로 남는다.
타임아웃도 설계에 넣는다
-
결제 서비스가 응답하지 않는다 — 성공인지 실패인지 모른다
-
① 조회 API 로 실제 상태를 확인한다 (권장)
-
② 타임아웃 후 보상한다 — 단, 실제로는 성공했을 수 있다
- 보상이 멱등해야 하는 이유가 여기 있다
테스트가 어렵다
정상 흐름보다 '실패 경로 × 단계 수' 가 훨씬 많다
- 4단계면 실패 시나리오가 최소 4가지 + 보상 실패까지 곱해진다
- 각 단계 실패를 강제로 주입하는 테스트를 갖춰야 한다