MSA 환경에서 분산 트랜잭션 문제와 Saga 패턴
1. 문제 상황 파악
우리 프로젝트의 현재 상황
현재 우리 프로젝트는 MSA 구조로 다음과 같이 구성되어 있습니다:
1️⃣ 하나의 DB + 논리적 스키마 분리 (현재 개발 환경)
PostgreSQL (logistics)
├── order_schema
├── inventory_schema
└── delivery_schema
- ✅ 별도의 트랜잭션으로 작동
- ❌ Order 서비스가 롤백되어도 Inventory 서비스는 롤백 안 됨
- ❌ DB가 다운되면 모든 서비스 마비
2️⃣ 여러 개의 DB 인스턴스 (운영 환경 목표)
Order DB → Order Service
Inventory DB → Inventory Service
Delivery DB → Delivery Service
- ✅ 별도의 트랜잭션으로 작동
- ❌ Order 서비스가 롤백되어도 Inventory 서비스는 롤백 안 됨
- ✅ 하나의 DB가 다운되어도 다른 서비스는 정상 동작
3️⃣ 하나의 DB + 동일 트랜잭션 (모놀리스)
- ✅ 롤백 시 모든 로직이 함께 롤백
- ❌ MSA가 아님
핵심 문제: 데이터 일관성
현재 주문 생성 플로우는 다음과 같습니다:
주문 요청
→ 상품 조회 API 호출
→ 재고 차감 API 호출
→ 주문 저장
→ 배송 생성 API 호출
(+ 향후 알림, AI 서비스 추가 예정)
문제 시나리오:
1. 재고 차감 성공 ✅ (Inventory DB에 COMMIT)
2. 주문 저장 성공 ✅ (Order DB에 COMMIT)
3. 배송 생성 실패 ❌
결과: 재고는 차감되고 주문은 생성되었는데 배송은 없음
→ 데이터 일관성 깨짐!
이런 분산 트랜잭션 문제를 해결하기 위해 Saga 패턴을 도입합니다.
2. Saga 패턴이란?
Saga 패턴은 MSA 환경에서 분산 트랜잭션의 데이터 일관성을 보장하기 위한 패턴입니다.
핵심 개념
- 각 서비스의 로컬 트랜잭션을 순차적으로 처리
- 각 트랜잭션이 완료되면 다음 트랜잭션을 트리거
- 실패 시 보상 트랜잭션(Compensating Transaction)을 실행
보상 트랜잭션이란?
처음에는 "이전 트랜잭션을 롤백하는 건가?"라고 생각했지만, 그게 아닙니다!
❌ 롤백 (ROLLBACK)
- 같은 트랜잭션 내에서만 가능
- 물리적으로 데이터를 취소
✅ 보상 (Compensation)
- 이미 COMMIT된 트랜잭션을 논리적으로 취소
- 반대 작업을 수행하는 새로운 트랜잭션
예시:
// 정방향 트랜잭션
재고 차감: stock = 10 → 5
// 보상 트랜잭션 (롤백이 아님!)
재고 복구: stock = 5 → 10 (새로운 트랜잭션으로 증가)
3. Saga 패턴의 두 가지 구현 방식
3-1. Choreography-based Saga (코레오그래피)

특징:
- 중앙 컨트롤러 없이 각 서비스가 이벤트를 주고받음
- 각 서비스가 독립적으로 다음 서비스를 트리거
동작 방식:
Order Service: 주문 생성 → OrderCreatedEvent 발행
↓
Inventory Service: 이벤트 수신 → 재고 차감 → InventoryDeductedEvent 발행
↓
Delivery Service: 이벤트 수신 → 배송 생성 → DeliveryCreatedEvent 발행
장점:
- ✅ 구성과 운영이 간편
- ✅ 서비스 간 결합도가 낮음
- ✅ 단일 실패 지점(SPOF)이 없음
단점:
- ❌ 전체 워크플로우 파악이 어려움
- ❌ 서비스 간 순환 종속성 위험
- ❌ 통합 테스트가 복잡함 (모든 서비스를 실행해야 함)
3-2. Orchestration-based Saga (오케스트레이션)

특징:
- 중앙 집중식 컨트롤러(Orchestrator)가 모든 트랜잭션을 관리
- Orchestrator가 각 서비스에 명령을 내리고 상태를 추적
동작 방식:
Orchestrator (예: OrderFacade)
├─ 1. 상품 조회 API 호출
├─ 2. 재고 차감 API 호출
├─ 3. 주문 생성
├─ 4. 배송 생성 API 호출
└─ 실패 시: 보상 트랜잭션 순차 실행
장점:
- ✅ 한 곳에서 전체 플로우 파악 가능
- ✅ 순환 종속성 방지
- ✅ 복잡한 비즈니스 로직 관리 용이
- ✅ 디버깅과 모니터링이 쉬움
단점:
- ❌ Orchestrator가 단일 실패 지점이 될 수 있음
- ❌ 중앙 집중화로 인한 병목 가능성
- ❌ Orchestrator 구현 복잡도 증가
4. 우리 팀의 선택: Orchestration
- 튜터님이 Choreography-based Saga 패턴을 추천해주었기 때문에 우리는 청개구리여서 반대로 선택하였습니다.
- 여기까지 읽어보신다면 소소한 재미를 위해 Joke 한번 넣었습니다.
왜 Orchestration을 선택했는가?
현재 우리 코드를 보면 이미 Orchestration 패턴의 기초가 구현되어 있습니다:
@Component
public class OrderFacade {
@Transactional
public Order createOrder(CreateOrderCommand command) {
// 1. 상품 정보 조회
Map<UUID, GetProductResponse> productInfoMap = fetchProductInfos(command);
// 2. 주문 생성
Order order = orderService.createOrder(command);
// 3. 재고 차감
inventoryIntegrationService.deductInventory(...);
// 4. 배송 생성
deliveryIntegrationService.createDelivery(...);
return order;
}
}
OrderFacade가 이미 중앙 조율자 역할을 수행하고 있습니다!
선택 기준
기준 Choreography Orchestration 우리 선택
| 제어 방식 | 이벤트 체인 | 명시적 순서 제어 | ✅ 명시적 |
| 전체 플로우 파악 | 여러 서비스 코드 확인 필요 | 한 곳에서 파악 가능 | ✅ 한 곳 |
| 현재 구조 | 이벤트 기반 필요 | Facade 이미 존재 | ✅ Facade 있음 |
| 서비스 개수 | 많을수록 유리 | 적을수록 유리 | ✅ 3~5개 예상 |
| 비즈니스 로직 | 단순 | 복잡 | ✅ 복잡 |
결론
- 이미 OrderFacade가 순서를 제어하고 있음
- 한 곳에서 전체 플로우를 파악할 수 있어 유지보수 용이
- 서비스 개수가 적음 (Order, Inventory, Delivery + α)
- 주문 생성은 순서가 중요하고 복잡한 비즈니스 로직
따라서 Orchestration-based Saga 패턴을 선택했습니다!
5. 핵심 포인트 정리
Saga 패턴 구현 시 주의사항
1. 멱등성 보장 (Idempotency)
같은 요청이 여러 번 와도 결과가 동일해야 합니다.
// 나쁜 예
public void deductInventory(UUID productId, int quantity) {
product.decreaseStock(quantity); // 여러 번 호출하면 계속 차감!
}
// 좋은 예
public void deductInventory(String idempotencyKey, UUID productId, int quantity) {
if (alreadyProcessed(idempotencyKey)) {
return; // 이미 처리됨
}
product.decreaseStock(quantity);
saveIdempotencyKey(idempotencyKey);
}
2. 보상 트랜잭션은 역순으로
정방향과 반대 순서로 실행합니다.
정방향: 주문 생성 → 재고 차감 → 배송 생성
보상: 배송 취소 → 재고 복구 → 주문 취소
3. 보상 트랜잭션도 실패할 수 있음
보상 트랜잭션 실패에 대비한 재시도 메커니즘이 필요합니다.
try {
restoreInventory();
} catch (Exception e) {
// 재시도 큐에 추가
retryQueue.add(new RetryTask("restoreInventory", orderId));
// 운영팀에 알림
alertService.send("보상 트랜잭션 실패", orderId);
}
4. 타임아웃 설정
각 단계마다 적절한 타임아웃을 설정해 무한 대기를 방지합니다.
@FeignClient(
name = "inventory-service",
configuration = FeignConfig.class
)
public interface InventoryClient {
@PostMapping("/v1/inventories/deduct")
@Timeout(value = 5000) // 5초 타임아웃
DeductInventoryResponse deductInventory(@RequestBody DeductInventoryRequest request);
}
6. 향후 확장 계획
Kafka 도입 시
현재 동기 방식에서 비동기 메시징으로 전환 가능합니다:
// 현재: 동기 방식 (HTTP/Feign)
inventoryClient.deductInventory(request);
// 향후: Kafka 비동기 방식
kafkaProducer.send("inventory.deduct", request);
// 결과는 이벤트로 수신
@KafkaListener(topics = "inventory.deducted")
public void onInventoryDeducted(InventoryDeductedEvent event) {
// 다음 단계 진행
}
장점:
- Orchestrator는 유지하되, 통신 방식만 변경
- 각 서비스의 독립성 향상
- 비동기 처리로 성능 개선
알림/AI 서비스 추가
새로운 서비스가 추가되어도 Orchestrator에 단계만 추가하면 됩니다:
public Order createOrder(CreateOrderCommand command) {
try {
// 기존 로직
order = orderService.createOrder(command);
inventoryIntegrationService.deductInventory(...);
deliveryIntegrationService.createDelivery(...);
// 5. 알림 발송 (신규)
notificationService.sendOrderConfirmation(order);
// 6. AI 분석 (신규)
aiService.analyzeOrder(order);
} catch (Exception e) {
// 보상 트랜잭션에도 추가
compensate(order, ...);
}
}
7. 결론
MSA 환경에서는 분산 트랜잭션 문제가 필연적으로 발생합니다. 우리 팀은:
- 문제 인식: 서비스 간 트랜잭션 롤백 불가능
- 해결 방안: Saga 패턴 도입
- 구현 방식: Orchestration-based Saga
- 선택 이유:
- 현재 구조(OrderFacade)와의 정합성
- 전체 플로우의 가시성
- 복잡한 비즈니스 로직 관리 용이
- 유지보수 편의성
Saga 패턴은 완벽한 원자성을 보장하지는 못하지만, 최종 일관성(Eventual Consistency)을 통해 분산 시스템의 데이터 일관성을 유지합니다.
핵심 요약
| 문제 | MSA에서 분산 트랜잭션 롤백 불가 |
| 해결책 | Saga 패턴 (보상 트랜잭션) |
| 방식 | Orchestration (중앙 제어) |
| 장점 | 가시성, 유지보수성, 명확한 플로우 |
| 주의사항 | 멱등성, 역순 보상, 재시도 |
8. 참고 자료
- Microsoft Azure - Saga Pattern
- https://joobly.tistory.com/69#google_vignette
- https://sangyunpark99.tistory.com/entry/Saga-Pattern사가-패턴
'👍코드 회고' 카테고리의 다른 글
| Outbox와 DLQ 아키텍처 설계 (0) | 2025.12.01 |
|---|---|
| Saga 패턴2 (0) | 2025.11.27 |
| N+1 (10.17) (0) | 2025.10.17 |
| 리팩토링 vs 오버 엔지니어링 (10.16) (0) | 2025.10.16 |
| 장바구니 코드 속 store_id는 어디에 둘까? (10.15) (0) | 2025.10.15 |