MSA Phase 5. Transaction

devty·2023년 9월 12일
0

MSA

목록 보기
6/14

서론

Transaction 이란?

  • Transaction은 일련의 작업 단위를 의미한다.
  • 데이터베이스 관리 시스템(DBMS)에서 상태를 변환시키는 하나의 논리적 기능을 수행하기 위한 작업의 단위 또는 한꺼번에 수행되어야할 일련의 연산들을 의미한다.

Transaction 특징은?

  1. 원자성(Atomicity)
    • 트랜잭션이 데이터베이스에 모두 반영되던지, 아니면 전혀 반영 되지 않아야 한다.
    • 트랜잭션 내의 모든 명령은 반드시 완벽히 수행되어야 하며, 모두가 완벽히 수행되지 않고 어느 하나라도 오류가 발생하면 트랜잭션 전부가 취소되어야 한다.
  2. 일관성(Consistency)
    • 트랜잭션의 작업 처리 결과가 항상 일관성이 있어야 한다.
    • 트랜잭션 전과 후에 데이터의 무결성 규칙이 유지되어야 한다.
  3. 고립성(Isolation)
    • 각 트랜잭션이 독립적으로 실행되어야 함을 나타냅니다.
    • 여러 트랜잭션이 동시에 실행되더라도, 각 트랜잭션은 서로에게 영향을 주지 않아야 한다.
  4. 영속성(Durability)
    • 트랜잭션이 성공적으로 완료되었을 경우, 결과는 영구적으로 반영되어야 한다.

모놀로식에서 Transaction 관리

  1. 비즈니스 로직에 트랙잭션(데이터베이스에 상태 변화)이 필요한 작업을 수행한다. → 트랙잭션 시작
  2. 트랜잭션 안에서 여러개의 쿼리나 명령이 실행되며, 중간에 어떤한 오류도 발생하지 않아야한다.
  3. 모든 연산이 성공적으로 완료되면 트랜잭션은 커밋 상태가 된다.
    • 커밋은 트랙잭션 내의 모든 변경사항이 데이터베이스에 영구적으로 저장되는 것을 의미한다.
  4. 만약 트랜잭션 중간에 오류가 발생하거나 어떤 이유로 트랙잭션을 취소해야하는 경우 롤백이 발생한다.
    • 롤백은 트랙잭션 내에서 일어난 모든 변경사항을 취소하고, 데이터 베이스를 트랙잭션 시작 전으로 돌리는 과정이다.
  • 이러한 모든 Flow가 1PC(1-Phase Commit)이라고 한다.
  • 모놀로식 아키텍처에서는 데이터베이스 연결과 트랙잭션 관리가 일반적으로 단일 서비스 내에서 이루어지기 때문에 ACID 속성을 유지하는 것이 비교적 간단하다.

트랙잭션이 포함 된 SQL

  • 트랙잭션이 포함되지 않은 SQL이다.
    INSERT INTO users (username, password, phone_number) VALUES ('유저이름', '비밀번호123', '010-1234-5678');
    • 딱 보기에도 우리가 아는 그 SQL 문법이다.
  • 트랙잭션이 포함된 SQL이다.
    START TRANSACTION;
    
    INSERT INTO users (username, password, phone_number) VALUES ('taeyun', 'password', '010-1234-5678');
    
    IF (SELECT COUNT(*) FROM users WHERE username = 'taeyun') > 0 THEN
        ROLLBACK;
    ELSE
        COMMIT;
    END IF;
    • START TRANSACTION; → 트랙잭션을 시작하겠다는 뜻이다.
    • IF (SELECT COUNT(*) FROM users WHERE username = 'taeyun') > 0 THEN → 아이디에 대한 중복을 체크하는 로직이다.
    • ROLLBACK; → 위 로직이 실패시 트랙잭션을 취소하고 START TRANSACTION 실행 전 상태로 롤백한다.
    • COMMIT; → 위 로직이 성공시 트랙잭션을 DB에 반영한다.

본론

MSA에서 Transaction 필요한 이유

  • MSA에서는 여러 서비스가 존재하는데 각 서비스별로 각각의 고유의 DB를 가지고 있다.
    • MSA가 지향하는 점으로 각 서비스엔 각각의 DB를 가지는걸 지향한다.
  • 두개 이상의 서비스에서 DB 상태 변화가 일어날 때 하나 이상의 서비스에서 오류가 발생한다면 두 서비스 모두 롤백을 해줘야하는데 서비스가 다르기에 자체적으로 ACID를 지원하지 않는다.
  • 따라서 이런 부분을 해소해주기위해 MSA에서 새로운 Transaction 방식을 채택해야한다.

MSA에서 Transaction 관리

  • 이번 내용은 서론이 조금 길었는데 앞에 내용을 이해해야지 MSA에서 Transaction 관리를 어떻게 하는지 파악하기가 더 쉽다!
  • 그리고 진짜로 MSA에 여러 경험이 있는건 아니지만 Transaction 관리하는게 제일 어려웠던 것 같다…
  • 다시 돌아와 MSA에서 Transaction 관리로는 여러 방법이 있다.
    1. 2PC(2-Phase Commit)
    2. Saga Pattern
      • Ochestration Pattern
      • Choregraphy Pattern
  • 위 몇가지 방법을 이론적으로 먼저 소개하겠다.

2PC(2-Phase Commit)

  • 2PC(2-Phase Commit or N-Phase Commit)는 분산 시스템에서 트랜잭션을 관리하는 알고리즘이다.
  • 1 단계 : 준비단계(Prepare Phase)
    1. 코디네이터(Coordinator)는 모든 참여자에게 트랜잭션을 준비하고 커밋할 준비가 되었는지 묻는 Request-To-Prepare 메시지를 보냅니다.
    2. 참여자(Participant = Managers)는 트랜잭션을 준비하고, 커밋할 준비가 되면 Yes로 응답하고, 그렇지 않으면 No로 응답합니다. Yes로 응답한 참여자는 트랜잭션을 롤백하지 않고 잠시 대기합니다.
  • 2 단계 : 커밋 단계 (Commit Phase)
    1. 코디네이터(Coordinator)는 모든 참여자(Participant = Managers)로부터 Yes 응답을 받으면, 모든 참여자에게 Commit 메시지를 보내 트랜잭션을 완료하도록 합니다.
    2. 만약 어떤 참여자라도 No로 응답하거나 응답이 없다면, 코디네이터(Coordinator)는 모든 참여자(Participant = Managers)에게 Rollback 메시지를 보내 트랜잭션을 취소합니다.
  • 1 단계인 준비단계가 필요한 이유
    • 모놀리식에서는 어차피 본인들의 인스턴스를 공유 → 트랜잭션 적용하려는 DB가 트랜잭션이 가능한 상태지인지 알아야할 필요 X → 전체(1개)의 어플리케이션이 1개의 DB를 사용하기 때문이다.
    • MSA에서는 인스턴스 분리로 인해 대상 DB가 트랜잭션이 가능한 상태인지 미리 확인해야한다. → 전체(여러개)의 어플리케이션이 각 고유의 DB를 사용하기 때문이다.

2PC(2-Phase Commit) 장점, 단점

  • 장점
    1. 모든 참여자가 트랜잭션에 대해 합의하므로 데이터 일관성이 유지된다.
    2. 여러 서비스 또는 데이터베이스에서 동작하는 분산 트랜잭션을 관리할 수 있다.
  • 단점
    1. 모든 참여자가 커밋을 완료할 때까지 대기해야 하므로 시스템 전반의 성능이 저하됩니다.
    2. 알고리즘이 복잡하여 구현 및 유지 관리가 어려울 수 있습니다.
    3. 참여자 중 하나가 실패하거나 응답이 느리면, 다른 모든 참여자가 해당 참여자의 응답을 기다리는 동안 데드락이 발생할 수 있습니다.
    4. 참여자들은 트랜잭션의 최종 커밋/롤백이 결정될 때까지 자원을 보유해야 합니다. 이는 시스템에 부하를 줄 수 있습니다.
    5. NoSQL에서는 지원하지 않는다.

Saga Pattern

  • 드디어 왔다. MSA에서 제일 어려운 트랙잭션 관리 중에서도 Saga Pattern이다.
  • Saga 패턴이란 마이크로서비스들끼리 이벤트를 주고 받아 특정 마이크로서비스에서의 작업이 실패하면 이전까지의 작업이 완료된 마이크서비스들에게 보상(complemetary) 이벤트를 소싱함으로써 분산 환경에서 원자성(atomicity)을 보장하는 패턴입니다.
  • Saga에는 두가지 패턴이 존재하는데 위에서 설명했듯이 Choreography, Orchestration 두가지가 존재한다.

Choreography-based Saga

  • 분산 트랜잭션을 책임지는 중계자(Saga Manager)가 존재하지 않는 방식이다.
    1. 로컬 트랜잭션을 처리하고 다음 서비스에게 이벤트 전달한다.
    2. 성공, 실패를 큐로 응답으로 넣어준다.
    3. 실패 시 보상 트랜잭션 발행한다.
  • 장점
    1. 각 서비스가 독립적으로 작동하므로 시스템이 더 분산적이다.
    2. 각 서비스가 자체적으로 단계를 처리하므로, 개별 서비스 변경이 더 쉽다.
    3. 서비스간에 느슨한 결합이 유지되므로, 한 서비스의 변경이 다른 서비스에 큰 영향을 미치지 않는다.
  • 단점
    1. 트랜잭션이 많은 서비스를 포함하는 경우, 전체 흐름을 이해하거나 디버깅하기가 어려울 수 있다.
    2. 각 서비스가 독립적으로 작동하므로, 전체 트랜잭션의 일관성을 유지하는 것이 어려울 수 있다.
    3. 오류가 발생하면, 이미 완료된 단계를 롤백하는 로직이 복잡해질 수 있다.

Orchestration-based Saga

  • 하나의 orchestrator가 전체 트랜잭션의 흐름을 관리합니다.
  • Orchestration는 각 서비스를 순차적으로 호출하고, 오류가 발생하면 적절한 롤백 로직을 호출하여 트랜잭션을 복구합니다.
    1. 트랜젝션에 관여하는 모든 서비스는 중계자에 의해서 점진적으로 트랜잭션을 수행하며 결과를 중계자에게 전달한다.
    2. 그렇게 진행하다 마지막 트랜잭션이 끝나게되면 중계자를 종료하면서 전체 트랜잭션 처리를 종료한다.
    3. 실패 시 보상 트랜잭션 발행한다.
  • 장점
    1. 중앙 중계자가 있으므로 전체 트랜잭션 흐름이 명확하고 일관됩니다.
    2. 오류 처리와 롤백 로직이 중앙에 있으므로, 오류 관리가 더 간결하고 효과적일 수 있습니다.
    3. 전체 흐름이 한 곳에서 관리되므로, 코드 유지보수가 더 용이할 수 있습니다.
  • 단점
    1. 중앙 중계자가 시스템의 복잡성을 증가시킬 수 있습니다.
    2. 중앙 조정자는 서비스 간의 결합도를 높일 수 있으며, 시스템의 유연성을 제한할 수 있습니다.

Choreography-based Saga vs Orchestration-based Saga

비교 요소Choreography-based SagaOrchestration-based Saga
시스템 복잡성높음 (시스템이 확장됨에 따라 복잡성이 증가)중간 (중앙 중계자가 복잡성을 관리)
오류 처리 및 롤백복잡 (각 서비스가 자체 롤백을 관리)단순 (중앙 중계자가 롤백 전략을 관리)
결합도낮음 (서비스간 느슨한 결합)높음 (중앙 중계자가 의한 강한 결합)
추적성낮음 (전체 흐름 추적이 어려움)높음 (중앙 중계자를 통한 명확한 트랜잭션 추적)
유지보수 및 관리어려움 (변경 및 확장이 복잡함)용이 (중앙 중계자를 통해 쉽게 관리 및 확장 가능)
비즈니스 로직 집중화낮음 (비즈니스 로직이 분산됨)높음 (중앙 중계자에 비즈니스 로직 집중)
  • 위 표를 보면 알 수 있듯이 두 패턴에 대해 장단점이 뚜렷하다.
  • 두 패턴은 어떤 프로젝트에서 사용하는게 나은지는 프로젝트의 특성에 따라 나뉠수 있다고 생각한다.
  • 내가 생각했을 땐 Choreography-based Saga가 결합도 측면에서 조금 더 MSA가 지향하는 방식이 아닐까 하지만 단지 결합도 측면만 봐서는 안 될 것 같다.
  • 왜냐하면 유지보수 및 관리적인 측면에서 서비스가 커지면 커질수록 Orchestration-based Saga는 개발 리소스가 적게 들 것이다.
  • 또한, 추적성 측면에서 중앙 중계자를 통해 명확한 트랙잭션이 추적이 가능하고 제일 크다고 느낀점은 바로 오류 처리 및 롤백 측면이다.
    • 해당 부분은 Orchestration Saga가 직접 롤백을 관리하기에 각각의 서비스를 자체적으로 롤백하지 않아도 돼서 이 부분이 제일 맘에 들었다.
  • 따라서, 나는 Orchestration-based Saga로 구현하기로 마음 먹었다!!

비즈니스 로직

  1. 주문을 한다. → 주문 서비스
  2. 재고 수량을 감소한다. → 재고 서비스
  3. 주문 상태를 변경(완료)한다. → 주문 서비스

해당 비즈니스 로직에는 여러 커멘드와 이벤트들이 존재한다. 하나씩 파헤쳐보겠다.

  • 주문을 한다. → 주문 서비스
    • 주문을 생성에 대한 커멘드 → CreateOrderCommand
    • 주문이 생성 되었음에 대한 이벤트 → OrderCreatedEvent
  • 재고 수량을 감소한다. → 재고 서비스
    • 재고 수량 감소에 대한 커멘드 → ReduceStockCommand
      • 재고 수량이 있을 경우 → StockReducedEvent
      • 재고 수량이 없을 경우 → StockSoldOutEvent
  • 주문 상태를 변경한다. → 주문 서비스
    • 주문 상태 변경에 대한 커맨드(재고 수량에 유무에 따라 커멘드 변경) → CompleteOrderCommand, CancelOrderCommand
      • 주문 상태가 완료되었을 경우 → OrderCompletedEvent → End Saga
      • 주문 상태가 취소되었을 경우 → OrderCancelledEvent → End Saga
💡 Command의 시제와 Event의 시제가 다른 이유?
  • 커맨드 (Command): 시스템에 특정 작업을 수행하도록 지시하는 객체입니다. 그래서, 명령어는 일반적으로 동사로 표현됩니다.
    • 예: "CreateOrder" (주문 생성).
  • 이벤트 (Event): 시스템 내에서 어떤 일이 발생했음을 나타내는 객체입니다. 이러한 객체는 일반적으로 과거 시제의 동사로 표현됩니다, 왜냐하면 이미 일어난 사건을 나타내기 때문입니다.
    • 예: "OrderCreated" (주문이 생성되었음).

코드

  • build.gradle → Order Service
    dependencies {
        // Axon Framework
        implementation 'org.axonframework:axon-spring-boot-starter:4.8.0' // 최신 버전에 따라 업데이트 필요
    }
    • dependencies에 axon framework을 추가해준다.
  • RegisterOrderService.java → Order Service
    public class RegisterOrderService implements RegisterOrderUseCase {
    
        private final SaveOrderPort saveOrderPort;
    
        @Autowired
        private CommandGateway commandGateway;
    
        @Override
        public void registerOrder(CreateOrderCommand command) {
            Order order = Order.builder()
                    .receiverName(command.getReceiverName())
                    .receiverPhone(command.getReceiverPhone())
                    .receiverAddress(command.getReceiverAddress())
                    .userId(command.getUserId())
                    .orderStatus(OrderStatus.ORDER_CREATED)
                    .build();
    
            Order saveOrder = saveOrderPort.saveOrder(order);
    
            command.setOrderId(saveOrder.getOrderId());
            commandGateway.sendAndWait(command);
        }
    }
    • CommandGateway → Axon Framework의 인터페이스이다. 이 인터페이스는 커맨드를 발행하고 결과를 반환하는 메서드를 제공한다.
    • command.setOrderId(saveOrder.getOrderId()) → 이벤트 또는 커맨드 처리를 위해 주문 id가 필요하기 때문에 넣어주었다.
    • commandGateway.sendAndWait(command) → axon에서 관리하는 커멘드 버스로 보내준다. 그리고 해당 커멘드를 구독하고 있는 곳으로 통신이 된다.
  • OrderAggregate.java → Order Service
    @Aggregate
    @NoArgsConstructor
    public class OrderAggregate {
    
        @AggregateIdentifier
        private Long orderId;
    
        private OrderStatus orderStatus; // 주문 상태 (ex: CREATED, COMPLETED, CANCELLED)
        private List<OrderCreatedEvent.OrderItemInfo> orderItems;
    
        @CommandHandler
        public OrderAggregate(CreateOrderCommand command) {
            this.orderId = command.getOrderId();
            apply(new OrderCreatedEvent(command.getOrderId(), command.getOrderItemInfos()));
        }
    
        @EventSourcingHandler
        public void on(OrderCreatedEvent event) {
            this.orderId = event.getOrderId();
            this.orderItems = event.getOrderItems();
            this.orderStatus = OrderStatus.ORDER_CREATED;
        }
    
        @EventSourcingHandler
        public void on(StockReducedEvent event) {
            this.orderId = event.getOrderId();
        }
    
        @EventSourcingHandler
        public void on(StockSoldOutEvent event) {
            this.orderId = event.getOrderId();
            this.orderStatus = OrderStatus.ORDER_CANCEL;
        }
    }
    • @Aggregate → DDD(Domain-Driven Design)에서 한 덩어리의 일관성 경계를 구성하며 해당 경계 내에서의 데이터 변경을 관리합니다.
    • @AggregateIdentifier → 해당 어노테이션이 붙은 필드가 Aggregate의 유니크 식별자임을 나타낸다. 이 식별자를 통해 여러 커맨드와 이벤트가 해당 Aggregate와 연결된다.
    • OrderAggregate(CreateOrderCommand command) → CreateOrderCommand를 처리하는 메소드이고 내부적으로 OrderCreatedEvent를 발생시킨다.
      • 이 메소드를 아래와 같이 on(CreateOrderCommand command)로 사용할 수 없는 이유?
        • OrderAggregate(CreateOrderCommand command)는 Aggregate의 초기 상태를 생성하는데 사용되며, Axon 프레임워크가 이를 특별하게 처리한다. 생성자 내에서는 Aggregate의 식별자를 초기화하고 첫 번째 이벤트를 발생시킨다.
        • 따라서 on(CreateOrderCommand command) 이렇게 사용하게 되면 Aggregate의 생성자가 아니게 되므로, Aggregate의 생성 및 초기화 과정에서 문제가 생길수 있다.
    • on(OrderCreatedEvent event) → OrderCreatedEvent를 처리하는 핸들러이다.
      • 주문이 시작됨을 알려주기 위해 this.orderStatus = OrderStatus.ORDER_CREATED;로 넣어주었다.
    • on(StockReducedEvent event) → StockReducedEvent를 처리하는 핸들러이다.
    • on(StockSoldOutEvent event) → StockSoldOutEvent를 처리하는 핸들러이다.
      • 재고가 알려주기 위해 this.orderStatus = OrderStatus.ORDER_CANCEL;로 넣어주었다.
  • OrderManagementSaga.java → Order Service
    @Saga
    @Slf4j
    public class OrderManagementSaga {
    
        @Autowired
        private transient CommandGateway commandGateway;
    
        @StartSaga
        @SagaEventHandler(associationProperty = "orderId")
        public void handle(OrderCreatedEvent event) {
            log.info("OrderCreatedEvent received for Order ID: " + event.getOrderId() + ". Reducing stock for order items.");
    
            List<ReduceStockCommand.OrderItem> items = event.getOrderItems().stream()
                    .map(orderItemInfo -> new ReduceStockCommand.OrderItem(orderItemInfo.getProductId(), orderItemInfo.getCount()))
                    .collect(Collectors.toList());
    
            // 주문 생성 후 재고 감소 커맨드 전송
            commandGateway.send(new ReduceStockCommand(event.getOrderId(), items));
        }
    
        @SagaEventHandler(associationProperty = "orderId")
        public void handle(StockReducedEvent event) {
            log.info("Stock successfully reduced for Order ID: " + event.getOrderId() + ". Completing the order.");
            // 재고가 성공적으로 줄어들면 주문 완료 커맨드 전송
            commandGateway.send(new CompleteOrderCommand(event.getOrderId ()));
        }
    
        @SagaEventHandler(associationProperty = "orderId")
        public void handle(StockSoldOutEvent event) {
            log.info("Stock fail sold out for Order ID: " + event.getOrderId() + ". Cancelling the order.");
            // 재고 부족 시 주문 취소 커맨드 전송
            commandGateway.send(new CancelOrderCommand(event.getOrderId()));
        }
    
        @EndSaga // saga에 생명주기가 끝났음을 나타냄.
        @SagaEventHandler(associationProperty = "orderId")
        public void handle(OrderCompletedEvent event) {
            log.info("Order with ID: " + event.getOrderId() + " has been successfully completed.");
        }
    
        @EndSaga // saga에 생명주기가 끝났음을 나타냄.
        @SagaEventHandler(associationProperty = "orderId")
        public void handle(OrderCancelledEvent event) {
            log.info("Order with ID: " + event.getOrderId() + " has been cancel completed.");
        }
    }
    • CommandGateway → Axon Framework의 인터페이스이다. 이 인터페이스는 커맨드를 발행하고 결과를 반환하는 메서드를 제공한다.
    • @SagaEventHandler(associationProperty = "orderId") → 해당 메서드가 사가 이벤트 핸들러로 동작하며, 이벤트 객체의 orderId 속성을 기반으로 사가 인스턴스와 연결한다.
    • handle(OrderCreatedEvent event) → 주문이 생성되면, 이 핸들러가 호출되어 재고 감소 커맨드를 전송한다.
    • handle(StockReducedEvent event) → 재고가 성공적으로 감소하면, 이 핸들러가 호출되어 주문 완료 커맨드를 전송한다.
    • handle(StockSoldOutEvent event) → 재고가 부족하면, 이 핸들러가 호출되어 주문 취소 커맨드를 전송한다.
    • handle(OrderCompletedEvent event)handle(OrderCancelledEvent event) → 주문이 완료되거나 취소되면, 각각의 이벤트 핸들러가 호출되어 사가의 생명주기를 종료한다.
  • ReduceStockCommand.java → Common Moudle
    @Getter
    @AllArgsConstructor
    public class ReduceStockCommand {
    
        @TargetAggregateIdentifier
        private Long orderId;
    
        private List<OrderItem> items;
    
        @Data
        @AllArgsConstructor
        public static class OrderItem {
            private Long productId;
            private Integer count;
        }
    
    }
    • 여기는 중점적으로 봐야할 부분이 있다.
    • ReduceStockCommand는 Order-service에서 Orchestration Saga를 통해 ReduceStockCommand를 보내게 된다.
    • 근데 ReduceStockCommand는 Stock-service에서 StockCommandHandler를 통해 관리가 되므로 Order-service, Stock-service 두개 서비스 모두에서 사용이 된다.
    • 이러한 경우에는 두개 서비스에서 사용하기에 공통 모듈로 빼는 것이 좋다고 판단이 들어 Common Moudle에 넣어주게되었다.
    • @TargetAggregateIdentifier → 어떤 Aggregate 인스턴스가 이 커맨드를 처리해야 하는지를 Axon Framework에 알려주는 역할을 한다. 이를 통해, Axon Framework는 커맨드를 올바른 Aggregate 인스턴스로 라우팅할 수 있다.
      • 위 코드중 OrderAggregate에서 생성자(OrderAggregate(CreateOrderCommand command))를 통해 Aggregate 인스턴스가 생성된다.
  • StockCommandHandler.java
    @Component
    @AllArgsConstructor
    public class StockCommandHandler {
    
        @Autowired
        private final EventGateway eventGateway;
    
        private final ReduceStockHandlerUseCase reduceStockHandlerUseCase;
    
        @CommandHandler
        public void handle(ReduceStockCommand command) {
            try {
    						boolean allSuccess = true;
                for (ReduceStockCommand.OrderItem orderItem : command.getItems()) {
                    try {
                        // 재고 감소 로직
                        reduceStockHandlerUseCase.reduceStock(orderItem);
                    } catch (RuntimeException e) {
                        allSuccess = false;
                        log.info("재고 감소 실패: " + orderItem.getProductId());
                        // 다른 처리 로직 (예: 실패한 상품에 대한 정보 저장)을 여기에 추가할 수 있습니다.
                        break;
                    }
                }
    
                if (allSuccess) {
                    // 재고 감소에 성공하면 StockReducedEvent 발행
                    eventGateway.publish(new StockReducedEvent(command.getOrderId()));
                } else {
                    // 재고 부족시 StockSoldOutEvent 발행
                    eventGateway.publish(new StockSoldOutEvent(command.getOrderId()));
                }
            } catch (RuntimeException e) {
                log.error("Unexpected error", e);
            }
        }
    }
    • OrderManagementSaga(Order-service)에서 commandGateway.send(new ReduceStockCommand(event.getOrderId(), items)) 이 코드가 실행 됐을 때 StockCommandHandler(Stock-service)에서 handle(ReduceStockCommand command)메소드가 실행이 된다.
    • handle(ReduceStockCommand command) → 재고 감소에 대한 비즈니스 로직을 처리 한다.
    • 재고 감소 로직을 수행중에 재고가 있을경우와, 재고가 없을 경우에 이벤트가 다르다.
      • 재고 감소에 성공하면 StockReducedEvent발행
      • 재고 부족하여 실패하면 StockSoldOutEvent발행
    • StockReducedEvent발행되면 OrderManagementSaga에서 handle(StockReducedEvent event)메소드가 실행된다.
    • StockSoldOutEvent발행되면 OrderManagementSaga에서 handle(StockSoldOutEvent event)메소드가 실행된다.
  • OrderCommandHandler.java
    @Component
    @AllArgsConstructor
    public class OrderCommandHandler {
    
        @Autowired
        private EventGateway eventGateway;
    
        private final RegisterOrderUseCase registerOrderUseCase;
        private final CancelOrderUseCase cancelOrderUseCase;
    
        @CommandHandler
        public void handle(CompleteOrderCommand command) {
            registerOrderUseCase.completeOrder(command.getOrderId());
            eventGateway.publish(new OrderCompletedEvent(command.getOrderId()));
            log.info("Order completed with ID: " + command.getOrderId());
        }
    
        @CommandHandler
        public void handle(CancelOrderCommand command) {
            cancelOrderUseCase.CancelOrder(command.getOrderId());
            eventGateway.publish(new OrderCancelledEvent(command.getOrderId()));
            log.info("Order cancel with ID: " + command.getOrderId());
        }
    
    }
    • 바로 위에서 hadle로 처리 된 StockReducedEvent, StockSoldOutEvent는 각각CompleteOrderCommand, CancelOrderCommand를 발행시킨다.
    • handle(CompleteOrderCommand command) → 주문 완료에 대한 비즈니스 로직을 처리한다.
      • 로직이 잘 처리 됐다면 주문 완료에 대한 이벤트를 발행시킨다.
      • 발행된 OrderCompletedEventOrderManagementSaga에서 EndSaga를 통해 Saga 인스턴스를 종료한다.
    • handle(CancelOrderCommand command) → 주문 취소에 대한 비즈니스 로직을 처리한다.
      • 로직이 잘 처리 됐다면 주문 취소에 대한 이벤트를 발행시킨다.
      • 발행된 OrderCancelledEventOrderManagementSaga에서 EndSaga를 통해 Saga 인스턴스를 종료한다.

Flow Chart

  • 위에 코드로 설명했던 부분을 한번 읽고 이 Saga Flow를 보면 이해가 편할 것 이다.
  • 하나의 플로우마다 해당 파일이 어떤 서비스에 존재하는지와 파일 명을 적어두었다.

결론

후기

  • 모놀로식 아키텍처에서는 기본적으로 @Transaction만 사용해도 DB가 1개 여서 롤백(=보상)을 딱히 생각하지 않았다.
    • 그 이유는 DB에서 ACID를 지원해주기 때문이었다.
  • 하지만 마이크로소프트 아키텍처에서는 서비스별로 각각의 DB를 사용하기에 전체적인 ACID를 관리하는 것이 매우 어려웠다.
  • 배워야하는 이론들도 어려웠지만 역시나..구현이 정말 어려웠던 것 같다.
    • 생각해야할 케이스들이 너무 많았다. → 성공에 대한 이벤트, 실패에 대한 이벤트
  • 그래도 이제 슬슬 트랙잭션까지 마무리 해서 곧 MSA 챕터도 끝나지 않을까 싶다.
  • 앞으로 작성할 부분은 대충 구상해봤는데, 4개정도 될 것 같다.
    1. DevOps
    2. Monitoring
    3. TDD
    4. WorkShop Presentation
  • 이 중에서 이미 4번은 사내 워크샵때 발표한 내용이므로 거의 정리가 되었고, DevOps도 이미 인프라 환경을 다 구축해둔터라 정리만 하면 될 것 이다.
  • 나머지 Monitoring, TDD 부분만 구현하고 정리하면 MSA도 끝이다!
profile
지나가는 개발자

0개의 댓글