2023. 2. 22. 20:55ㆍSpring Micro Services/event driven architecture
간단하게 사용자가 주문정보를 전송하고 상품의 재고를 조회 하여서 재고가 부족하거나 재고 조회 서비스 호출 시 오류가 발생하면 주문취소 처리를 하고 재고가 있고 결제 처리 후 배송요청 오류가 발생하면 결제취소, 주문취소를 한다.
처음에 설명한 Saga 패턴의 핵심요소인 이전 로컬 트랜잭션에 의해 변경된 내용을 실행 취소하는 일련의 보상 트랜잭션을 실행한다. 라는 내용을 기억 할 수 있을 것입니다.
주문서비스 시퀀스 다이어그램
주문서비스 Orchestration
주문서비스 Orchestration Command and Event Flow
프로젝트설정
1. Axon Server 설치
https://developer.axoniq.io/download
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 실시간 메세지 처리에 적합하다는 생각이 듭니다.
테스트 준비
- Axon Server 를 먼저 실행 해주어야 합니다.
- Axon Server 설치 경로 에서 java -jar axonserver.jar 실행하여 줍니다.
- 각 서비스별 서버를 실행 합니다.
테스트
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
'Spring Micro Services > event driven architecture' 카테고리의 다른 글
Axon Framework + Axon Server + Saga pattern (0) | 2023.02.22 |
---|