createOrder()를 예로 설명한다.
@Transactional 어노테이션만으로 ACID 트랜잭션을 보장할 수 있다.APPROVAL_PENDING 상태로 생성CREATE_PENDING 상태로 생성AWAITTING_ACCEPTANCE로 변경APPROVED로 변경| 단계 | 서비스 | 트랜잭션 | 보상 트랜잭션 |
|---|---|---|---|
| 1 | 주문 서비스 | createOrder() | rejectOrder() |
| 2 | 소비자 서비스 | verifyConsumerDetail() | - |
| 3 | 주방 서비스 | createTicket() | rejectTicket() |
| 4 | 회계 서비스 | authorizeCreditCard() | - |
| 5 | 주방 서비스 | approveTicket() | - |
| 6 | 주문 서비스 | approveOrder() | - |
주문 정상 처리 흐름
APPROVAL_PENDING 상태로 생성 → 주문 생성 이벤트 발행PENDING 상태로 생성CREATE_PENDING 상태로 생성 → 티켓 생성됨 이벤트 발행AWAITING_ACCEPTANCE로 변경APPROVED로 변경 → 주문 승인됨 이벤트 발행신용카드 과금에서 실패가 발생하는 흐름
REJECT로 변경REJECT로 변경주문 서비스는 주문 및 주문 생성 사가 오케스트레이터를 생성 후 문제가 없다면 아래와 같이 진행될 것이다.
위 시나리오 마지막 단계에서 사가 오케스트레이터는 주문 승인 커맨드를 주문 서비스(자기 자신)에 전송한다.
*_PENDING 상태는 이상 현상을 예방하는 전략 중 하나다. (시멘틱 락 대책)
APPROVAL_PENDING - 현재 주문을 사가로 업데이트하는 중이니 그에 맞게 행동하라고 다른 사가에게 알리는 것| 단계 | 서비스 | 트랜잭션 | 보상 트랜잭션 |
|---|---|---|---|
| 1 | 주문 서비스 | createOrder() | rejectOrder() |
| 2 | 소비자 서비스 | verifyConsumerDetail() | - |
| 3 | 주방 서비스 | createTicket() | rejectTicket() |
| 4 | 회계 서비스 | authorizeCreditCard() | - |
| 5 | 주방 서비스 | approveTicket() | - |
| 6 | 주문 서비스 | approveOrder() | - |
Order.state의 *_PENDING 상태*_PENDING 상태일 땐 주문 취소 사가를 실패 처리하고 나중에 다시 시도하라고 알리기
debit())과 입금(credit())은 서로 교환적인 작업
OrderService: 주문 생성/관리를 담당하는 서비스 API 계층이 호출하는 도메인 서비스
Order를 생성 수정OrderRepository를 호출하여 Order를 저장SagaManager를 이용하여 CreateOrderSaga 생성
SagaManager는 이벤추에이트 트램 사가 프레임워크에서 기본 제공@Transactional
@RequiredArgsConstructor
public class OrderService {
private final SagaManager<CreateOrderSagaState> createOrderSagaManager;
private final OrderRepository orderRepository;
private final DomainEventPublisher eventPublisher;
public Order createOrder(OrderDetails orderDetails) {
// ...
ResultWithEvents<Order> orderAndEvents = Order.createOrder(...);
Order order = orderAndEvents.result;
orderRepository.save(order);
eventPublisher.publish(
Order.class, Long.toString(order.getId()), orderAndEvents.events
);
CreateOrderSagaState data = new CreateOrderSagaState(order.getId(), orderDetails);
CreateOrderSagaManager.create(data, Order.class, order.getId());
return order;
}
}
SagaManager가 사가 오케스트레이터 인스턴스를 생성
CreateOrderSaga
create(OrderSagaState) 커맨드로 커맨드 메시지를 생성CreateOrderSagaState
public class CreateOrderSaga implements SimpleSaga<CreateOrderSagaState> {
private SagaDefinition<CreateOrderSagaState> sagaDefinition;
public CreateOrderSaga(OrderServiceProxy orderService,
ConsumerServiceProxy consumerService,
KitchenServiceProxy kichenService,
AccountingServiceProxy accountintService) {
this.sagaDefinition =
step()
.withCompensation(orderService.reject, CreateOrderSagaState::makeRejectOrderCommand)
.step()
.invokeParticipant(consumerService.validateOrder, CreateOrderSagaState::makeValidateOrderByConsumerCommand)
.step()
.invokeParticipant(kitchenService.create, CreateOrderSagaState::makeCreateTicketCommand)
.onReply(CreateTicketReply.class, CreateOrderSagaState::handleCreateTicketReply)
.withCompensation(kitchenService.cancel, CreateOrderSagaState::makeCancelTicketCommand)
.step()
.invokeParticipant(accountingService.authorize, CreateOrderSagaState::makeAuthorizeCommand)
.step()
.invokeParticipant(orderService.approve, CreateOrderSagaState::makeApproveOrderCommand)
.build();
}
@Override
public SagaDefinnition<CreateOrderSagaState> getSagaDefinition() {
return sagaDefinition;
}
}
CreateOrderSaga 생성자는 사가 데피니션을 생성하여 sagaDefinition 필드에 세팅한다.invokeParticipant()
onReply()
withCompensation()
CreateOrderSagaState: 사가 인스턴스 상태를 나타낸 클래스
OrderService가 생성public class CreateOrderSagaState {
private Long orderId;
private OrderDetils orderDetails;
private long ticketId;
public Long getOrderId() {
return orderId;
}
private CreateOrderSagaState() {}
public CreateOrderSagaState(Long orderId, OrderDetails orderDetail) {
this.orderId = orderId;
this.orderDetails = orderDetails;
}
CreateTicket makeCreateTicketCommand() { // CreateTicket 커맨드 메시지 생성
return new CreateTicket(getOrderDetails().getRestaurantId(),
getOrderId(), makeTicketDetails(getOrderDetails()));
}
void handleCreateTicketReply(CreateTicketReply reply) { // 새로 만든 티켓 ID 저장
logger.debug("getTicketId {}", reply.getTicketId());
setTicketId(reply.getTicketId());
}
CancelCreateTicket makeCancelCreateTicketCommand() { // CancelCreateTicket 커맨드 메시지 생성
return new CancelCreateTicket(getOrderId());
}
// ...
}
CreateOrderSaga는 CreateOrderSagaState를 호출하여 커맨드 메시지를 생성XXXServiceProxy) 끝점으로 전달KitchenServiceProxy는 주방 서비스의 커맨드 메시지 3개 끝점을 정의한다.
create: 티켓 생성confirmCreate: 생성 확인cancel: 티켓 취소CommandEndpoint마다 지정한다.public class KitchenServiceProxy {
public final CommandEndpoint<CreateTicket> create =
CommandEndpointBuilder
.forCommand(CreateTicket.class)
.withChannel(KitchenServiceChannels.kitchenServiceChannel)
.withReply(CreateTicketReply.class)
.build();
public final CommandEndpoint<ConfirmCreateTicket> confirmCreate =
CommandEndpointBuilder
.forCommand(ConfirmCreateTicket.class)
.withChannel(KitchenServiceChannels.kitchenServiceChannel)
.withReply(Success.class)
.build();
public final CommandEndpoint<CancelCreateTicket> cancel =
CommandEndpointBuilder
.forCommand(CancelCreateTicket.class)
.withChannel(KitchenServiceChannels.kitchenServiceChannel)
.withReply(Success.class)
.build();
}
OrderService가 사가를 생성할 때 이벤트 순서
OrderService가 CreateOrderSagaState 생성OrderService가 SagaManager를 호출해 사가 인스턴스 생성SagaManager는 사가 데피니션의 첫 단계를 실행CreateOrderSagaState를 호출해 커맨드 메시지 생성SagaManager는 커맨드 메시지를 사가 참여자(소비자 서비스)에게 전송SagaManager는 사가 인스턴스를 DB에 저장SagaManager가 소비자 서비스의 응답을 수신할 때 이벤트 순서
SagaManager에 전달SagaManager는 DB에서 사가 인스턴스를 조회SagaManager는 그 다음 사가 데피니션 단계를 실행CreateOrderSagaState를 호출하여 커맨드 메시지를 생성SagaManager는 커맨드 메시지를 사가 참여자(주방 서비스)에게 보냄SagaManager는 업데이트 사가 인스턴스를 DB에 저장SagaManager는 보상 트랜잭션을 역순으로 실행한다.OrderComandHandlers
OrderService를 호출해 주문 업데이트 후 응답 메시지 생성SagaCommandDispatcher
@RequiredArgsConstructor
public class OrderCommandHandlers {
private final OrderService;
public CommandHandlers commandHandlers() {
return SagaCommandHandlersBuilder
.fromChannel("orderService")
.onMessage(ApproveOrderCommand.class, this::approveOrder)
.onMessage(RejectOrderCommand.class, this::rejectOrder)
...
.build();
}
public Message approveOrder(CommandMessage<ApproveOrderCommand> cm) {
OrderService.approveOrder(cm.getCommand().getOrderId());
return withSuccess();
}
public Message rejectOrder(CommandMessage<RejectOrderCommand> cm) {
OrderService.rejectOrder(cm.getCommand().getOrderId());
return withSuccess();
}
}