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();
}
}