MSA 분산 트랜잭션의 악몽 해결: Saga 패턴(Saga Pattern) 실무 구현과 롤백 전략

모놀리식 시스템을 마이크로서비스 아키텍처(MSA)로 분리하는 순간, 개발자는 가장 큰 악몽과 마주하게 됩니다. 바로 '데이터 정합성(Data Consistency)'의 붕괴입니다. 주문 서비스에는 데이터가 들어갔는데, 재고 서비스에서 에러가 발생했다면? 이미 커밋(Commit)된 주문 데이터는 좀비 데이터가 됩니다. 이 글에서는 ACID 트랜잭션을 보장할 수 없는 분산 환경에서, 2PC(Two-Phase Commit)의 성능 제약을 극복하고 데이터 일관성을 유지하는 Saga 패턴의 실무적 구현 전략을 다룹니다.

1. 2PC(Two-Phase Commit)는 왜 실패하는가?

과거에는 XA 트랜잭션이나 2PC를 사용하여 분산 데이터베이스 간의 원자성(Atomicity)을 강제로 보장하려 했습니다. 하지만 최근의 고가용성 시스템에서 이 방식은 치명적인 단점을 가집니다.

2PC의 문제점: 코디네이터(Coordinator)가 응답을 받을 때까지 모든 참여 서비스가 락(Lock)을 잡고 대기해야 합니다. 이는 처리량(Throughput)을 급격히 저하시키며, 데드락(Deadlock)의 주원인이 됩니다. O(N)의 복잡도가 서비스 수에 따라 기하급수적으로 증가합니다.

따라서 현대적인 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)입니다.

시나리오: 사용자가 주문(Order)을 생성하면 -> 결제(Payment)가 시도되고 -> 재고(Inventory)가 차감됩니다. 만약 재고 부족으로 실패하면, 결제를 취소하고 주문을 '취소' 상태로 되돌려야 합니다.
// 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개 이상 / 복잡한 로직
Best Practice: 초기 MSA 전환 단계에서는 Choreography 방식으로 시작하여 복잡도를 낮추십시오. 서비스 간의 이벤트 흐름이 5단계를 넘어가거나, 조건 분기가 복잡해지는 시점에 Orchestration(예: Axon Server, Camunda 등) 도입을 고려해야 합니다.

Conclusion

MSA 환경에서 완벽한 트랜잭션은 존재하지 않습니다. 우리는 '일관성'과 '가용성' 사이에서 트레이드오프를 결정해야 합니다. Saga 패턴은 이 난제를 해결하는 가장 강력한 도구이지만, 보상 트랜잭션 설계가 누락된다면 데이터 오염이라는 더 큰 재앙을 불러옵니다. 비즈니스 로직을 설계할 때 "성공하는 로직"보다 "실패했을 때 되돌리는 로직"을 먼저 고민하는 역발상이 필요합니다.

Post a Comment