모놀리식 시스템을 마이크로서비스 아키텍처(MSA)로 분리하는 순간, 개발자는 가장 큰 악몽과 마주하게 됩니다. 바로 '데이터 정합성(Data Consistency)'의 붕괴입니다. 주문 서비스에는 데이터가 들어갔는데, 재고 서비스에서 에러가 발생했다면? 이미 커밋(Commit)된 주문 데이터는 좀비 데이터가 됩니다. 이 글에서는 ACID 트랜잭션을 보장할 수 없는 분산 환경에서, 2PC(Two-Phase Commit)의 성능 제약을 극복하고 데이터 일관성을 유지하는 Saga 패턴의 실무적 구현 전략을 다룹니다.
1. 2PC(Two-Phase Commit)는 왜 실패하는가?
과거에는 XA 트랜잭션이나 2PC를 사용하여 분산 데이터베이스 간의 원자성(Atomicity)을 강제로 보장하려 했습니다. 하지만 최근의 고가용성 시스템에서 이 방식은 치명적인 단점을 가집니다.
따라서 현대적인 MSA에서는 결과적 일관성(Eventual Consistency)을 목표로 하는 Saga 패턴을 표준으로 채택합니다. Saga는 긴 트랜잭션을 여러 개의 짧은 '로컬 트랜잭션'으로 쪼개고, 실패 시 보상 트랜잭션(Compensating Transaction)을 실행하여 데이터를 원상복구합니다.
2. Choreography vs Orchestration: 아키텍처 결정
Saga 패턴을 구현하는 방식은 크게 두 가지로 나뉩니다. 비즈니스 복잡도에 따라 선택 전략이 달라져야 합니다.
A. 코레오그래피(Choreography) - 이벤트 기반
중앙 제어 장치 없이, 각 서비스가 이벤트를 발행하고 구독하는 방식입니다.
- 장점: 구현이 간단하고 서비스 간 결합도가 낮습니다. RabbitMQ나 Kafka만 있으면 시작할 수 있습니다.
- 단점: 비즈니스 로직이 분산되어 있어, 전체 흐름을 파악하기 어렵습니다. 순환 의존성(Cyclic Dependency) 발생 위험이 큽니다.
B. 오케스트레이션(Orchestration) - 중앙 제어
별도의 'Saga Orchestrator' 서비스가 전체 프로세스를 지휘합니다.
- 장점: 트랜잭션의 상태 관리가 명확하며, 복잡한 롤백 시나리오를 제어하기 쉽습니다.
- 단점: 오케스트레이터 자체가 병목이 될 수 있으며, 인프라 복잡도가 증가합니다.
3. Spring Boot와 Kafka를 활용한 보상 트랜잭션 구현
실무에서 가장 많이 사용되는 Choreography 기반의 구현 예제입니다. 핵심은 OrderService가 실패 이벤트를 감지했을 때 실행하는 보상 로직(Compensating Logic)입니다.
// OrderService: 이벤트를 수신하여 보상 트랜잭션 수행
@Service
@RequiredArgsConstructor
public class OrderSagaListener {
private final OrderRepository orderRepository;
private final PaymentGateway paymentGateway;
@KafkaListener(topics = "inventory-failure-topic", groupId = "order-group")
@Transactional
public void handleInventoryFailure(InventoryFailedEvent event) {
// 1. 실패한 주문 조회
Order order = orderRepository.findById(event.getOrderId())
.orElseThrow(() -> new OrderNotFoundException(event.getOrderId()));
// 2. [보상 트랜잭션] 주문 상태를 FAILED로 변경 (롤백 개념)
order.setStatus(OrderStatus.FAILED);
orderRepository.save(order);
// 3. [보상 트랜잭션] 이미 결제된 건이 있다면 환불 처리 요청
if (event.isPaymentCompleted()) {
paymentGateway.refund(order.getPaymentId());
}
log.error("Order {} failed due to inventory. Compensating transaction executed.", order.getId());
}
}
위 코드에서 가장 중요한 점은 @KafkaListener 내부에서 발생하는 예외 처리입니다. 보상 트랜잭션마저 실패할 경우(예: DB 다운), 반드시 Dead Letter Queue (DLQ)로 메시지를 보내 수동 처리나 재시도 메커니즘을 확보해야 합니다.
4. 패턴별 성능 및 복잡도 비교
팀의 규모와 비즈니스 성숙도에 따라 어떤 전략을 선택해야 할지 명확한 기준이 필요합니다.
| 비교 항목 | 2PC (Legacy) | Saga (Choreography) | Saga (Orchestration) |
|---|---|---|---|
| 데이터 일관성 | 강력함 (Immediate) | 결과적 (Eventual) | 결과적 (Eventual) |
| 결합도 | 매우 높음 | 낮음 (Loose) | 중간 (오케스트레이터 의존) |
| 구현 난이도 | 낮음 (프레임워크 지원) | 중간 | 높음 (상태 머신 필요) |
| 확장성 | 나쁨 (Locking) | 매우 좋음 | 좋음 |
| 추천 시나리오 | 금융 핵심 코어(제한적) | 서비스 2~4개 연동 | 서비스 5개 이상 / 복잡한 로직 |
Conclusion
MSA 환경에서 완벽한 트랜잭션은 존재하지 않습니다. 우리는 '일관성'과 '가용성' 사이에서 트레이드오프를 결정해야 합니다. Saga 패턴은 이 난제를 해결하는 가장 강력한 도구이지만, 보상 트랜잭션 설계가 누락된다면 데이터 오염이라는 더 큰 재앙을 불러옵니다. 비즈니스 로직을 설계할 때 "성공하는 로직"보다 "실패했을 때 되돌리는 로직"을 먼저 고민하는 역발상이 필요합니다.
Post a Comment