본문 바로가기

MSA

[MSA] MSA 구조에서의 SAGA 패턴과 분산 트랜잭션

서론

지난 글에서는 모놀리식 서비스를 msa 구조로 바꿔 독립적인 서비스들로 바꾸고, 서비스간 통신을 kafka를 통한 비동기 이벤트 통신으로 하였다.

 

하지만, MSA 환경에서 서비스 간의 데이터 일관성을 보장하는 것은 어려운 문제다.

특히 여러 서비스에 각각 다른 데이터베이스가 존재하며 트랜잭션을 유지해야하는 상황에서 ACID 원칙을 지키는 것은 쉽지 않다.

 

이러한 기술적 문제를 해결하기 위해 SAGA 패턴을 적용하여 Kafka 기반의 분산 트랜잭션을 구현하는 것을 목표로 하여, 트랜잭션 실패시 롤백을 구현해보려고 한다.

 

SAGA 패턴

Saga Pattern은 마이크로 서비스에서 데이터 일관성을 관리하는 방법이다.

각 서비스는 로컬 트랜잭션을 가지고 있으며, 해당 서비스 데이터를 업데이트하며 메시지 또는 이벤트를 발행해서, 다음 단계 트랜잭션을 호출하게 된다.

만약, 해당 프로세스가 실패하게 되면 데이터 정합성을 맞추기 위해 이전 트랜잭션에 대해 보상 트랜잭션을 실행한다.

 

 

SAGA 패턴은 크게 코레오그래피(Choreography) 방식오케스트레이션(Orchestration) 방식, 두 가지로 나뉜다.

 

 

Orchestration 방식

 

Orchestration

 

Orchestration 방식의 Saga 패턴은 트랜잭션 처리를 위해 Saga 인스턴스(Manager)가 별도로 존재한다. 트랜잭션에 관여하는 모든 App은 Manager에 의해 점진적으로 트랜잭션을 수행하며 결과를 Manager에게 전달하게 되고, 비즈니스 로직상 마지막 트랜잭션이 끝나면 Manager를 종료해서 전체 트랜잭션 처리를 종료한다.

만약 중간에 실패하게 되면 Manager에서 보상 트랜잭션을 발동하여 일관성을 유지한다.

 

프로세스 실패 시 보상 트랜잭션 실행

 

 Orchestration 방식의 Saga 패턴은 모든 관리를 Manager가 호출하기 때문에 분산트랜잭션의 중앙 집중화가 이루어진다.

 

장점

  1. 중앙 집중 제어로 흐름 가시성 확보
    • 트랜잭션 상태, 흐름, 보상 트랜잭션까지 모두 Orchestrator에서 관리
    • 모니터링, 디버깅, 로깅이 상대적으로 쉽다
  2. 보상 트랜잭션 처리 용이
    • 실패 시 Orchestrator가 정확한 단계로 되돌아가 보상 트랜잭션을 실행함
  3. 구현과 테스트가 상대적으로 단순
    • 전체 트랜잭션 흐름이 한 곳에 있으므로 유닛 테스트, 시나리오 테스트가 쉬움

 

단점

    1. Orchestrator 자체가 새로운 서비스
      • 별도의 인프라 관리, 배포, 장애 대응 필요
      • 장애 시 전체 트랜잭션에 영향 가능성
    2. 서비스 간 결합도는 낮지만, Orchestrator와 서비스 간 결합도는 높다.
      • 새로운 서비스가 생기면 Orchestrator 로직도 변경해야 함
      • 규모가 커질수록 Manager가 복잡해짐
    3. 단일 장애 지점(Single Point of Failure)
      • Orchestrator가 죽으면 전체 트랜잭션 흐름이 멈출 수 있음
    4. 직렬 흐름으로 인한 병목 가능성
      • 각 서비스가 순차적으로 호출되므로 병렬 처리에 제약이 생기기 쉬움

 

Choreography  방식

 

 

 

Choreography 방식은 중앙 관리자가 없는 이벤트 기반 아키텍처이다.

즉, 서비스끼리 직접적으로 통신하지 않고, kafka/rabitMQ와 같은 이벤트 메시지 큐를 활용해서 통신하는 방식이다.

 

각 서비스가 작업 완료 후 이벤트를 발행하여 다음 서비스를 트리거 하는 구조이다.

 

장점

  1. 구조가 단순함
    • Orchestrator(중앙 관리자) 없이 서비스 간 직접 이벤트로 연결
  2. 빠르게 구성 가능
    • 추가적인 중앙 서비스 없이 Kafka 등 메시지 브로커만 있으면 시작 가능
  3. 서비스 간 느슨한 결합
    • 서비스들은 단지 이벤트를 소비하고 발행할 뿐 서로를 모름 → 확장성과 유지보수 용이

 

단점

  1. 통합 테스트 어려움
    • 전체 흐름을 검증하려면 모든 서비스가 실행되어 있어야 함
  2. 디버깅 복잡
    • 이벤트 흐름이 비동기적이고 분산되어 있어 추적이 어려움
    • 문제가 발생했을 때 어느 단계에서 실패했는지 파악이 어려움
  3. 보상 로직이 서비스마다 분산
    • 예외 상황에 대한 처리(보상 트랜잭션)가 각 서비스에 흩어져 있어 관리가 어려움

 

 

Choreography 방식 선택

cgv 예매 서비스는 workflow가 간단하고, 추가적인  Manager 추가 서비스 구현보다는 kafka를 통해 얻는 이점이 더 크다고 느껴서

Choreography 방식을 택했다. Orchestration 대비 장점은 다음과 같다.

 

  1. 서비스 간 느슨한 결합 -> 확장석,유연성 뛰어남
    • 새로운 서비스가 추가되어도 기존 서비스 로직 변경 필요 없음 → 유지보수 편리
  2. 간단한 Workflow에 적합
    • 단순한 예약, 주문 처리 등 이벤트 흐름이 단방향인 구조에 매우 잘 맞음
  3. 배포 / 유지보수 쉬움
    • 중앙 매니저 필요없이 독립적으로 서비스 배포,유지보수 가능

 

 

구현

movie service와 reservation service에서 좌석 예매를 할 시 좌석은 품절이 되고, reservation service에서 사용자의 예매를 생성하게 된다. 이때 예매에 실패하게 됐을 때의 보상 트랜잭션을 구현해보겠다.

 

 

ReservationService

@Transactional
    public void createReservation(String userName, Long seatId){
        try{

            Reservation reservation= Reservation.builder()
                    .userName(userName)
                    .status(Status.RESERVED)
                    .seatId(seatId)
                    .build();

            reservationRepository.save(reservation);

            log.info("예약 저장 완료: 사용자={}, 좌석ID={}", userName, seatId);
        } catch (Exception e) {
            log.error("예약 요청 오류, 사용자 이름:{}, 좌석 번호:{}", userName,seatId);
            reservationRollbackProducer.sendReservationRollbackEvent(seatId);
        }
    }

 

간단한 프로젝트이기 때문에, 결제와 같은 부가적인 서비스는 존재하지 않는다. 따라서 예매를 생성만 하는 작업이라 실패할 일이 없긴 하지만 오류가 날 시, movie 서비스에게 롤백 이벤트를 발행하게 된다.

 

 

MovieService - ReservationRollbackConsumer

@KafkaListener(topicPattern = "reservation-rollback", groupId = "reservation-rollback", containerFactory = "kafkaListenerContainerFactory")
public void consume(ConsumerRecord<String, ReservationRollbackEvent> record) {
    ReservationRollbackEvent request = record.value();

    log.info("예약 롤백 이벤트 수신: 좌석ID={}",  request.getSeatId());

    seatService.rollbackReservation(request.getSeatId());
}

 

MovieService - SeatService

// 예매 생성 실패 보상 트랜잭션
    @Transactional
    public void rollbackReservation(Long seatId){
        Seat seat=seatRepository.findBySeatIdWithRock(seatId)
                .orElseThrow(() -> new CustomException(StatusCode.SEAT_NOT_EXIST));

        if(seat.getIsReserved())
            seat.rollback();

        log.info("예약 롤백 이벤트 처리: 좌석ID={}", seatId);
    }

 

 

 

마무리

이렇게 보상 트랜잭션을 구현함으로 써, 비동기 간 통신에서 실패 시 롤백을 Kafka로 구현하였다. 하지만 제대로 구조를 설계한건지 위화감이 든다.  지금 구조는 서비스 분리만 해놨지, 여전히 모놀리틱하게 동작하고 있어 msa구조라고 할 수 없다고 생각한다.

 

다음 글에 현재 서비스 흐름의 문제점을 알아보고 제대로 된 msa 구조로 바꿔보도록 하겠다.

'MSA' 카테고리의 다른 글

[MSA] CGV 예매 서비스 MSA로의 변경 - 1  (0) 2025.05.19