본문 바로가기

SAGA 패턴과 Saga Orchestration

@6uiw2025. 11. 27. 00:14
목차
1. SAGA 패턴이란
2.  SAGA의 방식 2가지
3. SAGA Orchestrator

 

 

 

📌 SAGA 패턴이란?

분산 트랜잭션(여러 MSA 서비스가 함께 처리해야 하는 하나의 업무)을 ‘전역 트랜잭션 없이’ 안전하게 처리하기 위한 패턴

 

MSA에서는 하나의 업무를 처리할 때 여러 서비스가 참여한다. 
그런데 서비스마다 DB가 분리되니까 2PC(분산 트랜잭션)가 맞지 않게 되고, 많은 비용과 장애가 발생할 수 있다.
그래서 동기 트랜잭션을 포기하고, 서비스마다 로컬 트랜잭션을 순차적으로 연결하는 방식을 사용하는 것이 SAGA 패턴이다.

한마디로, MSA 환경에서 분산 트랜잭션 없이 작업을 안정적으로 성공시키거나, 실패 시 보상 작업을 수행해 일관성을 맞추는 방식이다.

 

💡SAGA = 이야기라는 뜻 

SAGA는 여러 개의 로컬 트랜잭션으로 이루어진 긴 비즈니스 프로세스를 순차적으로 실행하는 것을

긴 스토리(여러 장으로 구성된 이야기)에 비유해서 Saga(이야기)라는 이름이 붙여짐

 

 

 

📌 보상의 개념 

"SAGA의 보상(compensation)은 롤백(rollback)이 아니다."

 

✔ 보상은 “반대 작업을 새로 실행하는 것”

✔ 롤백은 “DB가 막 실행한 작업을 취소하는 것”

 

 

1. 롤백(Rollback)이란?

트랜잭션 안에서 실행한 작업들을 DB가 원상태로 되돌려주는 것.

  • DB 내부 기능
  • 트랜잭션이 아직 commit되지 않은 상태에서만 가능
  • 자동적이고 원자적
  • ACID 트랜잭션 내에서만 동작
예시)
  update stock set quantity = quantity - 1
  → 문제가 생겨서 rollback
  → quantity가 다시 +1 복구됨 (DB가 자동 처리)

 

 

2. 보상(Compensation)이란?

 

이미 commit된 작업을 "반대로 되돌리는 새로운 트랜잭션"을 실행하는 것.

  • DB가 해주는 게 아니라 애플리케이션이 직접 수행
  • 이전 단계가 이미 커밋되어 있음
  • 네트워크, 다른 서비스 호출, 새로운 비즈니스 로직 필요
  • 원자성이 없음 → 실패 가능

되감기가 아니라 "반대 동작을 새로 실행"하는 것이 바로 보상.

 



📌 SAGA가 필요한 이유

  • MSA에서는 서비스마다 DB가 분리됨
  • 한 번에 전체 DB에 트랜잭션을 걸 수 없음(2PC 불가능)
  • 네트워크 오류, 서비스 장애 등으로 일부만 성공하는 상황이 발생할 수 있음
  • SAGA는 이런 불일치 문제를 해결하기 위해 나온 패턴

 

 

📌 SAGA 패턴에서 오해하기 쉬운 부분

SAGA = 이벤트 기반 패턴이다?
→ 꼭 그런 것은 아니다. Orchestration 방식은 이벤트 없이 REST로도 충분히 구현 가능함.

 

SAGA는 ACID 트랜잭션처럼 완벽한 일관성을 보장한다?
→ 아님.
→ 최종적 일관성(Eventual Consistency)을 제공함.

 

SAGA는 보상 트랜잭션이 있으니 무조건 롤백 가능하다?
→ 보상 트랜잭션도 실패할 수 있다. 따라서 설계가 중요함.

 

 

 


📌 SAGA의 2가지 방식

1) Choreography - 서비스 간 이벤트로 통신

각 서비스가 이벤트를 보고 다음 행동을 알아서 수행하는 방식.

Order Service → “OrderCreatedEvent” 발행
Inventory Service → 이 이벤트를 듣고 재고 차감 → “StockDecreasedEvent” 발행
Payment Service → 이 이벤트를 듣고 결제
...
장점 단점
  • 오케스트레이터 필요 없음 → 단순함
  • 서비스 간 느슨한 결합
  • 이벤트 흐름이 복잡해짐
  • 서비스 간 순서 제어가 어렵고 디버깅이 힘듦

 

2) Orchestration - 중앙에서 트랜잭션 흐름을 조정

 

중앙 조정자(Orchestrator)가 모든 흐름을 제어함.

Orchestrator → Order 생성 요청
Orchestrator → 재고 차감 요청
Orchestrator → 결제 요청
장점 단점
  • 흐름이 명확하고 제어가 쉬움
  • 버그 대응이 쉬움
  • 오케스트레이터가 단일 실패 지점이 될 수 있음
  • 코드가 중앙집중화됨

 

 


📌 Saga Orchestration 구조

✔️ 구성 요소

  1. Orchestrator
    • 중앙에서 Saga를 조율
    • 트랜잭션 순서와 성공/실패 시 보상 트랜잭션 수행 지시
  2. 서비스 (Participants)
    • 각 서비스는 오케스트레이터 지시대로 로컬 트랜잭션 수행
    • 성공/실패 결과를 오케스트레이터에 전달

 

✔️ 흐름 예시

 

예) 온라인 쇼핑몰 주문 처리


참여 서비스: 주문 서비스, 결제 서비스, 재고 서비스

  1. 고객이 주문 생성
  2. 오케스트레이터가 주문 서비스에 "주문 생성" 요청
    • 주문 서비스는 주문을 DB에 저장하고 결과를 오케스트레이터에 보고
  3. 오케스트레이터가 재고 서비스에 "재고 차감" 요청
    • 성공하면 오케스트레이터에 보고
    • 실패하면 오케스트레이터가 보상 트랜잭션으로 주문 취소 요청
  4. 오케스트레이터가 결제 서비스에 "결제 진행" 요청
    • 실패하면 재고 복원 + 주문 취소 등 보상 트랜잭션 수행

즉, 모든 트랜잭션은 오케스트레이터가 지휘하고, 각 서비스는 그 지시만 수행하며 결과를 보고하는 구조

 

// Orchestrator 예시 (Spring Boot)
@Service
public class OrderSagaOrchestrator {

    private final OrderService orderService;
    private final InventoryService inventoryService;
    private final PaymentService paymentService;

    @Transactional
    public void handleOrderSaga(OrderRequest request) {
        try {
            orderService.createOrder(request);
            inventoryService.decreaseStock(request.getItemId(), request.getQuantity());
            paymentService.processPayment(request.getUserId(), request.getAmount());
        } catch (Exception e) {
            // 실패 시 보상 트랜잭션 수행
            paymentService.refund(request.getUserId(), request.getAmount());
            inventoryService.restoreStock(request.getItemId(), request.getQuantity());
            orderService.cancelOrder(request.getOrderId());
        }
    }
}

 

 

 


📌 오케스트레이터는 꼭 별도의 서비스여야 할까?

결론: 반드시 별도 서비스로 분리해야 하는 것은 아님.
하지만 MSA 철학과 운영 관점에서는 별도 서비스로 두는 것이 훨씬 유리

 

 

✔ 1. 오케스트레이터를 “별도 서비스”로 둘 때

장점

1) 도메인 분리 명확

  • 주문 서비스는 주문만
  • 결제 서비스는 결제만
  • 오케스트레이터는 흐름만

   도메인 로직과 Saga 흐름 제어가 섞이지 않아서 유지보수가 편리하다.

2) 오케스트레이터 장애 격리

   오케스트레이터만 죽어도 다른 서비스는 정상 동작
    -> 운영상 이점

3) 스케일링 용이

   트랜잭션 조율(주문 비즈니스 로직)과 이벤트 처리량이 커지면 오케스트레이터만 scale-out 가능.

4) 여러 Saga 흐름을 한 곳에서 관리

   예:

  • 주문 Saga
  • 환불 Saga
  • 배송 Saga
    이런 것들을 하나의 오케스트레이터 서비스에서 통합 관리 가능.

5) 테스트 용이

   오케스트레이션 로직만 따로 단위테스트 및 시나리오 테스트 가능.


✔ 2. 오케스트레이터를 “기존 서비스 내에” 둘 때

예: 주문 서비스 내부에서 Saga 흐름까지 처리하도록 만드는 경우

 

장점

  • 구현이 단순
  • 배포/서비스 개수 감소
  • 작은 프로젝트에서는 오히려 적절할 수 있음

 단점 (중요)

  1. 해당 서비스가 너무 비대해짐
  2. 해당 서비스가 죽으면 Saga 전체가 멈춤
  3. MSA의 독립성과 확장성 떨어짐
  4. 다른 도메인에서 Saga 사용하기 어려움

❗결국 MSA라면 “도메인의 비즈니스 로직”과 “Saga 흐름 제어”는 성격이 완전히 다르기 때문에 섞으면 안 되는 경우가 많음

 

 

 

 

 

 


📌 오케스트레이터가 각 서비스에 명령을 보내는 방식 3가지

✔️ 방식 1: REST API 호출 (가장 기본)

오케스트레이터는 각 서비스의 REST API 엔드포인트를 호출

오케스트레이터 (Spring Boot)

@Service
public class OrderSagaOrchestrator {

    private final RestTemplate restTemplate;

    public void startOrderSaga(OrderRequest req) {
        // 1. 주문 생성 요청
        restTemplate.postForObject("http://order-service/orders", req, Void.class);

        // 2. 재고 차감 요청
        restTemplate.postForObject("http://inventory-service/stocks/decrease", req, Void.class);

        // 3. 결제 요청
        restTemplate.postForObject("http://payment-service/payments", req, Void.class);
    }
}

각 서비스는 API를 제공하는 독립 서비스

단점

  • 요청이 동기식
  • 한 서비스가 느리면 전체 응답이 느려짐
  • 장애에 취약

하지만 구조가 직관적이라 작은 규모에서는 많이 사용


 

✔️ 방식 2: Kafka 같은 메시지 기반 (MSA에서 가장 권장)

오케스트레이터는 각 서비스에게 명령(command) 이벤트를 발행하고,
각 서비스는 자기 토픽을 구독해서 처리한 뒤 결과 이벤트를 다시 발행

 -> 이 방식이 진짜 오케스트레이션 구조

🔎 동작 흐름 예시

오케스트레이터

  • order.create.command 이벤트 발행
  • inventory.decrease.command 이벤트 발행
  • payment.process.command 이벤트 발행

각 서비스

  • 자기에게 온 command 이벤트를 처리
  • 성공/실패 이벤트를 다시 Kafka로 발행

오케스트레이터

  • 그 이벤트를 받고 다음 단계로 진행
  • 실패 이벤트면 보상 트랜잭션 실행

 

🔎 Kafka 기반 예시 코드 (간단)

 

오케스트레이터 → 재고 서비스

kafkaTemplate.send(
	"inventory.decrease.command", new InventoryDecreaseCommand(itemId, qty)
    );

 

재고 서비스 (Consumer)

@KafkaListener(topics = "inventory.decrease.command")
public void decreaseStock(InventoryDecreaseCommand cmd) {
    try {
        inventory.useStock(cmd.itemId(), cmd.qty());
        kafkaTemplate.send("inventory.decrease.success", new SuccessEvent(cmd.itemId()));
    } catch (Exception e) {
        kafkaTemplate.send("inventory.decrease.fail", new FailEvent(cmd.itemId()));
    }
}

 

오케스트레이터 (다음 단계로 진행)

@KafkaListener(topics = "inventory.decrease.success")
public void onInventorySuccess(SuccessEvent event) {
    // 다음 단계로 결제 요청
    kafkaTemplate.send("payment.process.command", new PaymentCommand(...));
}

 


✔ 3. 방식 3: gRPC 호출

REST보다 빠르고 타입 안정적

gRPC란?

Google이 만든 초고속 원격 프로시저 호출(Remote Procedure Call, RPC) 프레임워크
즉, 서비스 간 통신을 엄청 빠르고 효율적으로 해주는 기술

HTTP/REST보다 훨씬 빠르고 효율적이도록 설계된 방식

 

PaymentResponse res = paymentGrpcClient.processPayment(req);

 

 

 


📌 주문 생성 API를 어디에 둬야하는가에 대한 고민

 오케스트레이터”에 둘 수도 있고 “주문 서비스”에 둘 수도 있다. 

 

 

✔️ 패턴 1: “오케스트레이터 → 모든 서비스 지휘” (많이 사용)

  • 주문 생성 API는 오케스트레이터에 있음 (일반적인 Orchestration 패턴)

흐름

  1. 클라이언트가 오케스트레이터에 POST /orders 요청
  2. 오케스트레이터가 Saga 시작
  3. 오케스트레이터 → 주문 서비스에 “create order” command
  4. 오케스트레이터 → 재고 차감, 결제 등 orchestration
Client → Orchestrator (/orders)
            ↓
       order.create.command
            ↓
        Order Service

왜 이렇게 구성하는가?

  • Saga의 전체 생명주기(Lifecycle)를 오케스트레이터가 100% 제어해야 하기 때문
  • 주문 서비스가 “단순 도메인 서비스”로 남아있고 비즈니스 워크플로우는 오케스트레이터가 담당
  • 다양한 프로세스(쿠폰, 결제, 포인트, 배송 등)가 추가되어도 확장 쉬움

단점

  • 오케스트레이터가 사실상 “API Gateway 역할”도 일부 담당하게 됨
  • 주문 서비스 단독 배포 시 API가 없어서 테스트 불편

 

✔️ 패턴 2: “주문 서비스가 시작점 → 오케스트레이터에게 이벤트 전달”

  • 주문 생성 API는 주문 서비스에 있음

(이건 Saga “Choreography + 시작점 서비스 주도식” 구조와 가까움)

흐름

  1. 클라이언트가 주문 서비스에 POST /orders 요청
  2. 주문 서비스가 로컬 트랜잭션으로 주문 상태를 “PENDING”으로 기록
  3. 주문 서비스 → Kafka에 “OrderCreatedEvent” 발행 (Saga 시작)
  4. 오케스트레이터가 이벤트를 듣고 재고 차감/결제 등 orchestration 시작
Client → Order Service (/orders)
            ↓
   OrderCreatedEvent 발행
            ↓
        Orchestrator

 

장점

  • “API는 해당 도메인 서비스의 책임”이라는 DDD 원칙과 잘 맞음
  • 주문 서비스만 독립적으로 배포/테스트하기 쉬움
  • 외부에서 오케스트레이터를 직접 호출하지 않아도 됨

단점

  • 오케스트레이터가 “중간 단계부터 Saga 개입”
  • 복잡한 경우 이벤트 흐름 추적이 어렵고 API 경로가 분산됨

 

6uiw
@6uiw :: LOG.INFO("MING's DEVLOG")

개발을 하면서 공부한 기록을 남깁니다

목차