Java 아키텍처·Spring 용어 사전
MSA보상 트랜잭션 · 오케스트레이션 · 코레오그래피

Saga

서비스마다 로컬 트랜잭션을 이어 붙이고 실패 시 보상으로 되돌리는 패턴. 2PC의 현실적 대안이다.

여러 서비스에 걸친 작업을 각자의 로컬 트랜잭션으로 나누고, 실패하면 보상 작업으로 되돌리는 패턴.

왜 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가지 + 보상 실패까지 곱해진다
  • 각 단계 실패를 강제로 주입하는 테스트를 갖춰야 한다

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 마이크로서비스 — Saga·Outbox·서킷브레이커