← wiki

SAGA — 여러 서비스에 걸친 일을 어떻게 '한 번에' 처리할까

주문을 한 번 넣으면 뒤에서 여러 일이 같이 일어난다. 주문이 기록되고, 결제가 빠지고, 재고가 줄어든다. 이 셋은 전부 되거나 전부 안 되어야 한다. 결제는 됐는데 재고가 모자라 주문이 안 잡히면, 돈만 빠진 사고가 된다.

DB가 하나였을 땐 이게 공짜였다. 데이터베이스의 트랜잭션이 "이 작업들을 하나로 묶어, 중간에 하나라도 실패하면 전부 없던 일로 되돌린다(롤백)"를 보장해 줬기 때문이다. 그런데 마이크로서비스로 가면서 주문·결제·재고가 각자 다른 서비스, 다른 DB로 쪼개졌다. 이제 하나의 트랜잭션으로 셋을 묶을 수 없다. SAGA는 이 "여러 서비스에 걸친 일"을 작은 로컬 트랜잭션들의 연쇄로 바꾸고, 중간에 실패하면 앞서 성공한 것들을 **보상(되돌리기)**해 일관성을 맞추는 패턴이다.

주문 생성로컬 TX
결제 처리로컬 TX
재고 차감로컬 TX
실패 시보상
이 글의 예시

주문 → 결제 → 재고 차감 세 단계가 각각 다른 서비스/DB에서 일어나는 주문 처리 시나리오를 따라간다. 이 중 한 단계가 실패했을 때 앞 단계를 어떻게 되돌리는지가 핵심이다.


1. 분산되면 깨지는 '전부 아니면 전무'

먼저 왜 기존 트랜잭션으로 안 되는지를 분명히 하자. 단일 DB 트랜잭션이 주던 보장이 무엇이었고, 쪼개지면 그게 왜 사라지는지를 보면 SAGA의 동기가 잡힌다.

  1. 1
    단일 DB: 트랜잭션이 다 묶어줬다

    begin → 작업들 → commit, 중간 실패는 rollback.

    한 DB 안에서는 여러 작업을 하나로 묶어, 실패하면 전부 없던 일로 되돌릴 수 있었다. 원자성(atomicity) — 전부 되거나 전부 안 되거나 — 이 공짜로 따라왔다.

  2. 2
    분산: 서비스마다 DB가 따로다

    주문 DB·결제 DB·재고 DB가 서로 다른 곳에 있다.

    한 트랜잭션은 한 DB 안에서만 유효하다. 서로 다른 서비스의 DB 작업을 하나의 트랜잭션으로 묶을 수 없으니, "결제는 커밋됐는데 재고는 실패"가 각각 따로 확정돼 버린다.

  3. 3
    2단계 커밋(2PC)은 무겁다

    분산 트랜잭션 표준은 있지만 비용이 크다.

    여러 DB를 한 트랜잭션처럼 묶는 2PC 방식이 있지만, 모든 참여자를 잠그고 합의를 기다려 느리고, 한 곳만 멈춰도 전체가 묶인다. MSA에선 잘 안 쓰고, 대신 SAGA로 느슨하게 푼다.

핵심은 이거다. 분산 환경에선 "전부 한 번에 되돌리기"가 불가능하니, 되돌리는 행위 자체를 별도의 작업으로 정의해 두자는 것. 그게 보상 트랜잭션이다.


2. 보상 트랜잭션: 되돌리기를 직접 정의한다

SAGA의 핵심은 각 단계마다 **그걸 되돌리는 짝(보상 트랜잭션)**을 준비해 두는 것이다. 자동 롤백이 없으니, "결제 취소", "재고 복원"처럼 되돌리는 동작을 명시적으로 만들어 둔다. 중간에 실패하면, 이미 성공한 단계들을 역순으로 보상해 간다.

주문 처리(코디네이터)각 서비스
1. 주문 생성

성공

2. 결제 처리

성공

3. 재고 차감 실패!

재고 부족

보상: 결제 취소

2번을 되돌림

보상: 주문 취소

1번을 되돌림 → 일관성 회복

  1. 1
    정상 경로 — 앞으로 한 단계씩

    각 단계가 자기 DB에 로컬 트랜잭션으로 커밋한다.

    한 단계가 끝나면 그 결과는 이미 확정된다. 다음 단계로 넘어가며, 각 단계는 독립적으로 자기 DB에 커밋한다. 전체를 한꺼번에 잡고 있지 않다.

  2. 2
    실패 경로 — 뒤로 보상하며 되감기

    실패 지점부터 성공했던 단계들을 역순으로 되돌린다.

    재고 차감이 실패하면, 이미 커밋된 결제·주문을 되돌리는 보상 동작을 차례로 실행한다. "결제 취소"는 진짜 환불일 수도, "취소 표시"일 수도 있다 — 도메인에 맞게 정의한다.

보상은 '완벽한 롤백'이 아니다

DB 롤백은 흔적 없이 되돌리지만, 보상은 새로운 반대 작업이라 흔적이 남는다. 결제했다 환불하면 이미 알림이 나갔을 수 있고, 카드사엔 두 건이 찍힌다. 그래서 보상은 "없던 일로"가 아니라 "되돌리는 행위를 추가로 한 것"임을 전제로 설계해야 한다.


3. 누가 흐름을 지휘하나 — 두 가지 방식

각 단계와 보상을 정의했다면, 다음 질문은 "이 순서를 누가 끌고 가느냐"다. 크게 한 명이 지휘하는 방식과, 각자 이벤트로 이어 달리는 방식이 있다.

오케스트레이션 (지휘자형)
  • ·중앙 코디네이터가 단계를 순서대로 호출
  • ·흐름이 한곳에 모여 있어 이해·디버깅 쉬움
  • ·실패 시 보상 순서도 코디네이터가 관리
  • ·코디네이터가 복잡해지고 결합점이 될 수 있음
코레오그래피 (이벤트 릴레이형)
  • ·각 서비스가 이벤트를 듣고 다음 일을 수행
  • ·중앙 지휘자 없이 느슨하게 연결됨
  • ·서비스 추가가 쉽지만 전체 흐름 파악이 어려움
  • ·이벤트가 얽히면 추적·보상 순서가 까다로움
이벤트로 연결한다면 메시지 브로커가 필요하다

코레오그래피는 "결제 완료" 같은 이벤트를 서비스들이 주고받으며 진행한다. 이 이벤트를 안정적으로 전달하려면 메시지 큐/브로커(Kafka 등) 가 바탕에 깔린다. 또 이벤트와 DB 커밋이 둘 다 되거나 둘 다 안 되게 하려면 아웃박스(outbox) 같은 기법이 따라붙는다. SAGA가 메시징 글과 자주 함께 등장하는 이유다.

SAGA는 '최종적 일관성'이다

SAGA가 도는 동안엔 "결제는 됐는데 재고는 아직"인 중간 상태가 잠깐 존재한다. 즉시 일관(strong consistency)이 아니라, 보상까지 끝나야 맞는 최종적 일관성(eventual consistency)이다. 그래서 사용자에겐 "처리 중" 상태를 보여주고, 중간 상태를 가정한 화면·재시도를 설계해야 한다.

정리하면 SAGA는 "여러 DB에 걸친 하나의 일"을, 작은 로컬 트랜잭션의 연쇄 + 실패 시 보상으로 풀어내는 패턴이다. 단일 트랜잭션의 즉시 원자성을 포기하는 대신, 서비스를 느슨하게 유지하며 최종적 일관성을 얻는다. 그 대가로 보상 동작을 일일이 정의하고, 중간 상태를 견디는 설계를 떠안는다.


한 장 요약

  1. 1
    문제

    DB가 쪼개지면 '전부 되거나 전부 안 되거나'를 한 트랜잭션으로 못 묶는다.

  2. 2
    발상

    큰 일을 작은 로컬 트랜잭션의 연쇄로 바꾸고, 각 단계의 되돌리기를 미리 정의.

  3. 3
    보상

    중간 실패 시 성공한 단계들을 역순으로 보상(취소·복원)해 일관성 회복.

  4. 4
    보상의 한계

    DB 롤백과 달리 흔적이 남는 새 작업 — '없던 일로'가 아님을 전제로 설계.

  5. 5
    지휘 방식

    중앙 코디네이터(오케스트레이션) vs 이벤트 릴레이(코레오그래피).

  6. 6
    성질

    즉시 일관이 아니라 최종적 일관성 — 중간 상태와 '처리 중'을 견뎌야 한다.