주문 서비스 예제

2023. 2. 22. 20:55Spring Micro Services/event driven architecture

반응형

간단하게 사용자가 주문정보를 전송하고 상품의 재고를 조회 하여서 재고가 부족하거나 재고 조회 서비스 호출 시 오류가 발생하면 주문취소 처리를 하고 재고가 있고 결제 처리 후 배송요청 오류가 발생하면 결제취소, 주문취소를 한다.

 

처음에 설명한 Saga 패턴의 핵심요소인 이전 로컬 트랜잭션에 의해 변경된 내용을 실행 취소하는 일련의 보상 트랜잭션을 실행한다. 라는 내용을 기억 할 수 있을 것입니다.

 

주문서비스 시퀀스 다이어그램

 

주문서비스 Orchestration

 

주문서비스 Orchestration Command and Event Flow

프로젝트설정

1. Axon Server 설치

https://developer.axoniq.io/download

 

Download - AxonIQ Developer Portal - AxonIQ

The AxonIQ Initializr is a web application that creates an Axon project structure for you. It doesn’t generate any application code, but it will give you a solid project structure, offering either a Maven or a Gradle build specification to suit your ne

developer.axoniq.io

2. Spring Boot 프로젝트 생성

https://start.spring.io/ 사이트에서 order-service 프로젝트를 생성한다.

나머지 프로젝트도 위와 같이 동일하게 생성한다.

common-service의 경우 공통으로 사용하는 command, events만 관리하므로 Dependencies는 Lombok만 추가 해준다.

 

최종 프로젝트 설정 후 IDE화면은 다음과 같다.

 

OrderProcessingSaga.java

프로젝트에서 가장 핵심적인 소스입니다. 주문서비스 전체의 command and flow를 관리하는 소스 입니다. 소스를 구현하다 보면 workflow의 느낌이 많이 들 것입니다.

package com.roopy.orderservice.api.saga;

import com.roopy.commonservice.api.command.*;
import com.roopy.commonservice.api.events.*;
import com.roopy.orderservice.api.events.OrderCreatedEvent;
import com.roopy.orderservice.api.service.InventoryService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.axonframework.commandhandling.gateway.CommandGateway;
import org.axonframework.modelling.saga.EndSaga;
import org.axonframework.modelling.saga.SagaEventHandler;
import org.axonframework.modelling.saga.StartSaga;
import org.axonframework.spring.stereotype.Saga;
import org.springframework.beans.factory.annotation.Autowired;

@Saga
@Slf4j
public class OrderProcessingSaga {

    @Autowired
    private transient CommandGateway commandGateway;

    @Autowired
    private InventoryService inventoryService;

    @StartSaga
    @SagaEventHandler(associationProperty = "orderId")
    private void handle(OrderCreatedEvent event) {
        log.info("[Saga] OrderCreatedEvent in Saga for Order Id : {}", event.getOrderId());
        boolean isValidInventory;
        try {
            isValidInventory = inventoryService.isValidInventory(event);
            
            if (isValidInventory) {
                // 결재처리
                CreatePaymentCommand createPaymentCommand = CreatePaymentCommand.builder()
                        .paymentId(event.getPaymentId())
                        .orderId(event.getOrderId())
                        .totalPaymentAmt(event.getTotalPaymentAmt())
                        .paymentDetails(event.getPaymentDetails())
                        .build();
                commandGateway.sendAndWait(createPaymentCommand);
            } else {
                cancelOrderCommand(event.getOrderId());
            }

        } catch (Exception e) {
            log.error(e.getMessage());
            cancelOrderCommand(event.getOrderId());
        }
    }

    private void cancelOrderCommand(String orderId) {
        CancelOrderCommand cancelOrderCommand = new CancelOrderCommand(orderId);
        commandGateway.sendAndWait(cancelOrderCommand);
    }

    @SagaEventHandler(associationProperty = "orderId")
    private void handle(PaymentProcessedEvent event) {
        log.info("[Saga] PaymentProcessedEvent in Saga for Order Id : {}", event.getOrderId());
        try {
            DeliveryOrderCommand deliveryOrderCommand = DeliveryOrderCommand.builder()
                    .deliveryId(RandomStringUtils.random(15, false, true))
                    .orderId(event.getOrderId())
                    .build();
            log.info(deliveryOrderCommand.toString());
            commandGateway.sendAndWait(deliveryOrderCommand);
        } catch (Exception e) {
            log.error(e.getMessage());
            cancelPaymentCommand(event);
        }
    }

    private void cancelPaymentCommand(PaymentProcessedEvent event) {
        CancelPaymentCommand cancelPaymentCommand = new CancelPaymentCommand(event.getPaymentId(), event.getOrderId());
        commandGateway.sendAndWait(cancelPaymentCommand);
    }

    @SagaEventHandler(associationProperty = "orderId")
    public void handle(OrderDeliveriedEvent event) {
        log.info("[Saga] OrderDeliveriedEvent in Saga for Order Id : {}", event.getOrderId());

        CompleteOrderCommand completeOrderCommand = CompleteOrderCommand.builder()
                .orderId(event.getOrderId()).orderStatus("APPROVED")
                .build();

        commandGateway.sendAndWait(completeOrderCommand);
    }

    @SagaEventHandler(associationProperty = "orderId")
    @EndSaga
    public void handle(OrderCompletedEvent event) {
        log.info("[Saga] OrderCompletedEvent in Saga for Order Id : {}", event.getOrderId());
    }

    @SagaEventHandler(associationProperty = "orderId")
    @EndSaga
    public void handle(OrderCancelledEvent event) {
        log.info("[Saga] OrderCancelledEvent in Saga for Order Id : {}", event.getOrderId());
    }

    @SagaEventHandler(associationProperty = "orderId")
    public void handle(PaymentCancelledEvent event) {
        log.info("[Saga] PaymentCancelledEvent in Saga for order Id : {}", event.getOrderId());
        cancelOrderCommand(event.getOrderId());
    }

}

 

소스 구현시 CommandGateWay Component Injection 을 위해서 @Autowired annotation 적용시 꼭 transient 를 선언해주어야 합니다.

 

다음으로 서비스 전체의 트랜잭션의 시작과 끝을 알려주는 @StartSaga와 @EndSaga annotation 을 설정 해주어야 합니다.

 

처음 구현 시 굉장히 헷갈리는 부분이 많았지만 정해진 규칙에 따라 Command와 Event 설정을 잘해준다면 처음 접하는 개발자들도 쉽게 적응 할 것으로 생각이 듭니다.

 

처음에 Axon Server 대신 Kafka를 사용하는 경우도 많다고 하였습니다. 이 부분의 차이점은 알아보았고 처음으로 MSA Transaction을 공부하시는 개발자라면 Axon Framework + Axon Server를 추천 드립니다.

 

Kafka의 경우는 따로 공부를 해야하고 Kafka 에 대해서 공부하는 시간도 많이 들것입니다. 그리고 Kafka 실시간 메세지 처리에 적합하다는 생각이 듭니다.

 

테스트 준비

  1. Axon Server 를 먼저 실행 해주어야 합니다.
  2. Axon Server 설치 경로 에서 java -jar axonserver.jar 실행하여 줍니다.
  3. 각 서비스별 서버를 실행 합니다.

 

테스트

POSTMAN 설정

 

Body Request 데이터 설정

{
	"orderDetails":[
		{
			"productId":"3820840811",
			"orderSeq":1,
			"qty":1,
			"orderAmt":3800
		},
		{
			"productId":"3820840812",
			"orderSeq":2,
			"qty":2,
			"orderAmt":1200
		}		
	],
	"paymentId":"263547771707652",
	"paymentDetails":[
		{
			"paymentId":"263547771707652",
			"paymentGbcd":"10",
			"paymentAmt":5000
		},
		{
			"paymentId":"263547771707652",
			"paymentGbcd":"20",
			"paymentAmt":2000
		}
		
	]
}

 

정상적인 경우

주문상태는 APPROVED(40), 결재상태는 COMPLETED(30), 배송상태는 REQUESTED(10) 으로 저장

 

재고가 부족한경우

주문상태는 CANCELD(20) 결재, 배송 데이터는 등록이 되지 않습니다.

InventoryProjection.java 에서 아래 코드를 변경 후 inventory-service 재시작 후 테스트를 진행합니다.

InventoryVO inventoryVO = new InventoryVO(query.getProductId(), 0);

 

배송요청중 오류가 발생한 경우

주문상태는 CANCELD(20), 결재상태는 CANCELD(20), 배송 데이터는 등록 되지 않습니다.

OrderProcessingSaga.java 에서 강제로 배송요청 오류를 발생시킨 후 테스트를 진행합니다.

코드 수정 후 order-server 재시작 후 테스트를 진행합니다.

@SagaEventHandler(associationProperty = "orderId")
private void handle(PaymentProcessedEvent event) {
	log.info("[Saga] PaymentProcessedEvent in Saga for Order Id : {}", event.getOrderId());
	try {
		if (true)
			throw new RuntimeException("배송요청중 오류가 발생하였습니다.");

		DeliveryOrderCommand deliveryOrderCommand = DeliveryOrderCommand.builder()
				.deliveryId(RandomStringUtils.random(15, false, true))
				.orderId(event.getOrderId())
				.build();
		log.info(deliveryOrderCommand.toString());
		commandGateway.sendAndWait(deliveryOrderCommand);
	} catch (Exception e) {
		log.error(e.getMessage());
		cancelPaymentCommand(event);
	}
}

소스다운로드

https://github.com/roopy1210/spring-msa-eda-with-axon

 

GitHub - roopy1210/spring-msa-eda-with-axon

Contribute to roopy1210/spring-msa-eda-with-axon development by creating an account on GitHub.

github.com

 

반응형