목차
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 구조
✔️ 구성 요소
- Orchestrator
- 중앙에서 Saga를 조율
- 트랜잭션 순서와 성공/실패 시 보상 트랜잭션 수행 지시
- 서비스 (Participants)
- 각 서비스는 오케스트레이터 지시대로 로컬 트랜잭션 수행
- 성공/실패 결과를 오케스트레이터에 전달
✔️ 흐름 예시
예) 온라인 쇼핑몰 주문 처리
참여 서비스: 주문 서비스, 결제 서비스, 재고 서비스
- 고객이 주문 생성
- 오케스트레이터가 주문 서비스에 "주문 생성" 요청
- 주문 서비스는 주문을 DB에 저장하고 결과를 오케스트레이터에 보고
- 오케스트레이터가 재고 서비스에 "재고 차감" 요청
- 성공하면 오케스트레이터에 보고
- 실패하면 오케스트레이터가 보상 트랜잭션으로 주문 취소 요청
- 오케스트레이터가 결제 서비스에 "결제 진행" 요청
- 실패하면 재고 복원 + 주문 취소 등 보상 트랜잭션 수행
즉, 모든 트랜잭션은 오케스트레이터가 지휘하고, 각 서비스는 그 지시만 수행하며 결과를 보고하는 구조
// 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 흐름까지 처리하도록 만드는 경우
장점
- 구현이 단순
- 배포/서비스 개수 감소
- 작은 프로젝트에서는 오히려 적절할 수 있음
단점 (중요)
- 해당 서비스가 너무 비대해짐
- 해당 서비스가 죽으면 Saga 전체가 멈춤
- MSA의 독립성과 확장성 떨어짐
- 다른 도메인에서 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 패턴)
흐름
- 클라이언트가 오케스트레이터에 POST /orders 요청
- 오케스트레이터가 Saga 시작
- 오케스트레이터 → 주문 서비스에 “create order” command
- 오케스트레이터 → 재고 차감, 결제 등 orchestration
Client → Orchestrator (/orders)
↓
order.create.command
↓
Order Service
왜 이렇게 구성하는가?
- Saga의 전체 생명주기(Lifecycle)를 오케스트레이터가 100% 제어해야 하기 때문
- 주문 서비스가 “단순 도메인 서비스”로 남아있고 비즈니스 워크플로우는 오케스트레이터가 담당
- 다양한 프로세스(쿠폰, 결제, 포인트, 배송 등)가 추가되어도 확장 쉬움
단점
- 오케스트레이터가 사실상 “API Gateway 역할”도 일부 담당하게 됨
- 주문 서비스 단독 배포 시 API가 없어서 테스트 불편
✔️ 패턴 2: “주문 서비스가 시작점 → 오케스트레이터에게 이벤트 전달”
- 주문 생성 API는 주문 서비스에 있음
(이건 Saga “Choreography + 시작점 서비스 주도식” 구조와 가까움)
흐름
- 클라이언트가 주문 서비스에 POST /orders 요청
- 주문 서비스가 로컬 트랜잭션으로 주문 상태를 “PENDING”으로 기록
- 주문 서비스 → Kafka에 “OrderCreatedEvent” 발행 (Saga 시작)
- 오케스트레이터가 이벤트를 듣고 재고 차감/결제 등 orchestration 시작
Client → Order Service (/orders)
↓
OrderCreatedEvent 발행
↓
Orchestrator
장점
- “API는 해당 도메인 서비스의 책임”이라는 DDD 원칙과 잘 맞음
- 주문 서비스만 독립적으로 배포/테스트하기 쉬움
- 외부에서 오케스트레이터를 직접 호출하지 않아도 됨
단점
- 오케스트레이터가 “중간 단계부터 Saga 개입”
- 복잡한 경우 이벤트 흐름 추적이 어렵고 API 경로가 분산됨
'Architecture&Pattern > MSA' 카테고리의 다른 글
| 251022 (수) MSA - Config 서버와 이벤트 드리븐 [개념] (0) | 2025.10.22 |
|---|---|
| 251020 (월) MSA 요청과 응답의 흐름, 서킷 브레이커 (0) | 2025.10.20 |
| 251017 (금) SpringCloud (0) | 2025.10.17 |