createOrder()
함수createOrder()
함수 앞에 @Transactional
만 붙이면 ACID 트랜잭션이 자동으로 보장됨APPROVAL_PENDING
상태로 생성한다.CREATE_PENDING
상태로 생성한다.AWAITING_ACCEPTANCE
로 변경한다.APPROVED
로 변경한다.APPROVAL_PENDING
상태로 생한다.CREATE_PENDING
으로 생성한다.CREATE_REJECTED
로 변경됨REJECT
로 변경한다.APPROVAL_PENDING
상태로 생성 -> 주문 생성 이벤트 발행CREATE_PENDING
상태로 생성 -> 티켓 생성됨 이벤트 발행PENDING
상태로 생성AWAITING_ACCEPTANCE
로 변경APPROVED
로 변경 -> 주문 승인됨 이벤트를 발행함.APPROVAL_PENDING
상태로 생성 -> 주문 생성 이벤트 발행CREATE_PENDING
상태로 생성 -> 티켓 생성됨 이벤트 발행PENDING
상태로 생성REJECT
로 변경REJECT
로 변경즉, 간단한 사가라면 코레오그래피 방식으로도 충분하지만 복잡한 사가는 오케스트레이션 방식이 적합하다.
사가 오케스트레이터인 CreateOrderSaga
클래스가 비동기 요청/응답을 주고 받으면서 주방 서비스, 소비자 서비스 같은 사가 서비스를 호출하고 그 처리 과정에 따라 커맨드 메시지를 전송. 그리고 이 클래스는 자신의 응답 채널에서 메시지를 읽어 다음 사가 단계를 결정한다.
주문 서비스는 먼저 주문 및 주문 생성 사가 오케스트레이터를 생성한다.
제일 마지막 단계에서 사가 오케스트레이터는 커맨드 메시지를 주문 서비스에 전송한다. 물론 주문 생성 사가가 주문을 직접 업데이트해서 승인 처리해도 되지만, 일관성 차원에서 주문 서비스가 마치 다른 참여자인 것처럼 취급하는 것이다.
APPROVAL_PENDING -> APPROVED
로 바로 상태 전이됨.*_PENDING
상태도 이런 이상 현상을 예방하는 전략 중 하나, 주문 생성 사가처럼 주문을 업데이트 하는 사가는 일단 주문을 *_PENDING
상태로 두고 시작한다. 현재 주문을 사가로 업데이트 하는 중이니 그에 맞게 행동하라고 다른 사가에게 알리는 것.createOrder()
, verifyConsumerDetails()
, createTicket()
은 모두 보상 가능 트랜잭션. 단 verifyConsumerDetails()
은 읽기 전용이라 따로 보상 트랜잭션이 필요 없음.authorizeCreditCard()
는 피봇 트랜잭션. 고객 신용카드가 승인되면 이 사가는 반드시 완료됨 approveTicket()
, approveOrder()
는 피봇 트랜잭션 이후 재시도 가능 트랜잭션Order.state
의 *_PENDING
이 시맨틱 락을 구현한 것. 이 필드를 이용하여 주문에 접근하는 다른 사가에 현재 어떤 사가가 주문을 업데이트 하고 있음을 알림.APPROVAL_PENDING
상태로 주문을 생성하고 마지막 단계(재시도 가능 트랜잭션)은 이 필드를 다시 APPROVED
로 변경한다. 보상 트랜잭션은 이 필드를 REJECTED
로 변경한다.cancelOrder()
를 호출해서 APPROVAL_PENDING
상태의 주문을 취소하려면 어떻게 해야 할까?cancelOrder()
를 실패 처리하고 클라이언트에 나중에 다시 시도하라고 알림. -> 재시도 로직까지 구현해야 하므로 클라이언트가 복잡해짐cancelOrder()
를 블로킹한다.시맨틱 락을 이용해 주문 서비스 및 주문 생성 사가를 설계/구현 하는 예제를 살펴보자
orderServie
, Order
등의 클래스와 주문 생성 사가를 오케스트레이션 하는 createOrderSaga
클래스가 존재한다.orderService
를 호출하여 커맨드 메시지를 처리하는 어댑터 클래스 orderCommanHandlers
가 있다.orderServie
, Order
, orderRepository
에 존재한다. KitchenServiceProxy,
, OrderServiceProxy
)를 거쳐 사가 참여자에게 커맨드 메시지를 전달한다.OrderCommandHandlers
클래스는 사가가 주문 서비스에 전송한 커맨드 메시지를 처리한다.OrderService
클래스는 주문/생성 관리를 담당하는 서비스 API 계층이 호출되는 도메인 서비스. Order
를 생성/수정하고, OrderRepository
를 호출하여 Order
를 저장하며, SagaManager
를 이용하여 CreateOrderSaga
같은 사가를 생성한다. // 예제 4-1 OrderService 클래스와 createOrder() 메서드
@Transactional
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private RestaurantRepository restaurantRepository;
@Autowired
private SagaManager<CreateOrderSagaState> createOrderSagaManager;
@Autowired
private DomainEventPublisher eventPublisher;
public Order createOrder(OrderDetails orderDetails) {
...
ResultWithEvents<Order> orderAndEvents = Order.createOrder(...); // Order 생성
Order order = orderAndEvent.result;
orderRepository.save(order) // DB에 Order 저장
eventPublisher.publish(Order.class, Long.toString(order.getId()), orderAndEvents.events); // 도메인 이벤트 발행
CreateOrderSagaState data = new CreateOrderSagaState(order.getId(), orderDetails); // CreateOrderSaga 생성
CreateOrderSagaManager.create(data, Order.class, order.getId());
return order;
}
}
CreateOrderSaga
를 생성CreateOrderSaga
: 사가의 상태 기계를 정의한 싱글톤 클래스, Create OrderSagaState
로 커맨드 메시지를 생성하고, 사가 참여자 프록시 클래스가 지정한 메시지 채널을 통해 참여하는 서비스에 메시지를 전달한다.CreateOrderSagaState
: 사가의 저장 상태. 커맨드 메시지를 생성함예제 4-2, 4-3 CreateOrderSaga 데피니션
package net.chrisrichardson.ftgo.orderservice.sagas.createorder;
import io.eventuate.tram.sagas.orchestration.SagaDefinition;
import io.eventuate.tram.sagas.simpledsl.SimpleSaga;
import net.chrisrichardson.ftgo.orderservice.sagaparticipants.*;
import net.chrisrichardson.ftgo.kitchenservice.api.CreateTicketReply;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class CreateOrderSaga implements SimpleSaga<CreateOrderSagaState> {
private Logger logger = LoggerFactory.getLogger(getClass());
private SagaDefinition<CreateOrderSagaState> sagaDefinition;
public CreateOrderSaga(OrderServiceProxy orderService, ConsumerServiceProxy consumerService, KitchenServiceProxy kitchenService,
AccountingServiceProxy accountingService) {
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) // 성공 응답을 수신하면 handleCreateTicketReply() 호출
.withCompensation(kitchenService.cancel, CreateOrderSagaState::makeCancelCreateTicketCommand) // 보상 트랜잭션 정의
.step()
.invokeParticipant(accountingService.authorize, CreateOrderSagaState::makeAuthorizeCommand)
.step()
.invokeParticipant(kitchenService.confirmCreate, CreateOrderSagaState::makeConfirmCreateTicketCommand)
.step()
.invokeParticipant(orderService.approve, CreateOrderSagaState::makeApproveOrderCommand)
.build();
}
@Override
public SagaDefinition<CreateOrderSagaState> getSagaDefinition() {
return sagaDefinition; // 사가 데피니션을 반환
}
}
CreateOrderSaga
생성자는 사가 데피니션을 생성하여 sagaDefinition
필드에 세팅. getSagaDefinition()
은 사가 데피니션을 반환하는 메서드.invokeParticipant()
은 포워드 트랜잭션을 정의한 메서드. CreateOrderSagaState.makeCreateTicketCommand()
로 CreateTicket
커맨드 메시지를 생성한 후, KitchenService.create
에 지정된 채널로 보낸다. onReply()
로 호출해서 주방 서비스로부터 성공 응답을 받으면 handleCreateTicketReply()
를 호출한다.withCompensation()
클래스로 보상 트랜잭션을 정의하고, 실패 시 makeCancelCreateTicketCommand()
를 호출한다.// 예제 4-4 CreateOrderSagaState는 사가 인스턴스 상태를 저장한다.
public class CreateOrderSagaState {
private Long orderId;
private OrderDetails orderDetails;
private long ticketId;
public Long getOrderId() {
return orderId;
}
private CreateOrderSagaState() {
}
public CreateOrderSagaState(Long orderId, OrderDetails orderDetails) { // OrderService가 호출하여 CreateOrderSagaState 인스턴스를 생성
this.orderId = orderId;
this.orderDetails = orderDetails;
}
@Override
public boolean equals(Object o) {
return EqualsBuilder.reflectionEquals(this, o);
}
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
를 호출하여 커맨드 메시지를 작성하고, 생성된 메시지를 KitchenServiceProxy
같은 클래스의 끝점으로 전달한다.// 예제 4-5 KitchenServiceProxy는 KitchenService의 커맨드 메시지 끝점을 정의한다.
package net.chrisrichardson.ftgo.orderservice.sagaparticipants;
import io.eventuate.tram.commands.common.Success;
import io.eventuate.tram.sagas.simpledsl.CommandEndpoint;
import io.eventuate.tram.sagas.simpledsl.CommandEndpointBuilder;
import net.chrisrichardson.ftgo.kitchenservice.api.*;
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();
}
sagas.orchestration
패키지는 이 프레임워크에서 가장 복잡함. 사가 기초 인터페이스 SimpleSaga
, 사가 인스턴스를 생성/관리하는 클래스 SagaManager
가 이 패키지에 존재SagaManager
는 사가를 저장하고, 자신이 생성한 커맨드 메시지를 전송하고, 응답 메시지를 구독하고, 사가를 호출하여 응답을 처리한다.orderService
가 사가를 생성할 때 이벤트 순서는 다음과 같다.orderService
는 createOrderSagaState
를 생성한다.orderService
는 SagaManager
를 호출하여 사가 인스턴스를 생성한다.SagaManager
는 사가 데피니션의 첫 번째 단계를 실행한다.createOrderSagaState
를 호출하여 커맨드 메시지를 생성한다.SagaManager
는 커맨드 메시지를 사가 참여자 (소비자 서비스)에 보낸다.SagaManager
는 사가 인스턴스를 DB에 저장한다.SagaManager가 소비자 서비스의 응답을 수신할 때 이벤트 순서는 아래와 같다.
SagaManager
는 DB에서 사가 인스턴스를 조회한다.SagaManager
는 그 다음 사가 데피니션 단계를 실행한다.createOrderSagaState
를 호출하여 커맨드 메시지를 생성한다.SagaManager
는 커맨드 메시지를 사가 참여자 (주방 서비스)에 보낸다.SagaManager
는 사가 인스턴스를 DB에 저장한다.사가 참여 서비스가 실패하면 SagaManager는 보상 트랜잭션을 역순으로 실행한다.
OrderCommandHandlers
클래스에 정의한다.OrderService
를 호출하여 주문 업데이트 후 응답 메시지를 생성한다. SagaCommandDispatcher
는 커맨드 메시지를 적절한 핸들러 메서드에 보내고 응답을 반환하는 클래스다.// 예제 4-6 OrderCommandHandlers 클래스
public class OrderCommandHandlers {
@Autowired
private OrderService 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) {
long orderId = cm.getCommand().getOrderId();
orderService.approveOrder(orderId); // Order를 승인 상태로 변경
return withSuccess(); // 제네릭 성공 메시지 반환
}
public Message rejectOrder(CommandMessage<RejectOrderCommand> cm) {
long orderId = cm.getCommand().getOrderId();
orderService.rejectOrder(orderId); // Order를 거부 상태로 변경
return withSuccess();
}
}
OrderService
를 호출한 후, 응답 메시지를 반환한다.// 예제 4-7 OrderServiceConfiguration은 OrderService의 스프링 빈이 정의된 구성 클래스다
package net.chrisrichardson.ftgo.orderservice.domain;
import io.eventuate.tram.events.publisher.DomainEventPublisher;
import io.eventuate.tram.events.publisher.TramEventsPublisherConfiguration;
import io.eventuate.tram.sagas.orchestration.SagaCommandProducer;
import io.eventuate.tram.sagas.orchestration.SagaManager;
import io.eventuate.tram.sagas.orchestration.SagaManagerImpl;
import io.eventuate.tram.sagas.orchestration.SagaOrchestratorConfiguration;
import io.micrometer.core.instrument.MeterRegistry;
import net.chrisrichardson.ftgo.common.CommonConfiguration;
import net.chrisrichardson.ftgo.orderservice.sagaparticipants.AccountingServiceProxy;
import net.chrisrichardson.ftgo.orderservice.sagaparticipants.ConsumerServiceProxy;
import net.chrisrichardson.ftgo.orderservice.sagaparticipants.KitchenServiceProxy;
import net.chrisrichardson.ftgo.orderservice.sagaparticipants.OrderServiceProxy;
import net.chrisrichardson.ftgo.orderservice.sagas.cancelorder.CancelOrderSaga;
import net.chrisrichardson.ftgo.orderservice.sagas.cancelorder.CancelOrderSagaData;
import net.chrisrichardson.ftgo.orderservice.sagas.createorder.CreateOrderSaga;
import net.chrisrichardson.ftgo.orderservice.sagas.createorder.CreateOrderSagaState;
import net.chrisrichardson.ftgo.orderservice.sagas.reviseorder.ReviseOrderSaga;
import net.chrisrichardson.ftgo.orderservice.sagas.reviseorder.ReviseOrderSagaData;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import java.util.Optional;
@Configuration
@Import({TramEventsPublisherConfiguration.class, SagaOrchestratorConfiguration.class, CommonConfiguration.class})
public class OrderServiceConfiguration {
// TODO move to framework
@Bean
public SagaCommandProducer sagaCommandProducer() {
return new SagaCommandProducer();
}
@Bean
public OrderService orderService(RestaurantRepository restaurantRepository, OrderRepository orderRepository, DomainEventPublisher eventPublisher,
SagaManager<CreateOrderSagaState> createOrderSagaManager,
SagaManager<CancelOrderSagaData> cancelOrderSagaManager ...) {
return new OrderService(orderRepository, eventPublisher, restaurantRepository,
createOrderSagaManager, cancelOrderSagaManager ...);
}
@Bean
public SagaManager<CreateOrderSagaState> createOrderSagaManager(CreateOrderSaga saga) {
return new SagaManagerImpl<>(saga);
}
@Bean
public CreateOrderSaga createOrderSaga(OrderServiceProxy orderService, ConsumerServiceProxy consumerService, KitchenServiceProxy kitchenServiceProxy, AccountingServiceProxy accountingService) {
return new CreateOrderSaga(orderService, consumerService, kitchenServiceProxy, accountingService);
}
@Bean
public SagaManager<CancelOrderSagaData> CancelOrderSagaManager(CancelOrderSaga saga) {
return new SagaManagerImpl<>(saga);
}
@Bean
public SagaCommandDispatcher orderCommandHandlersDispatcher(OrderCommandHandlers orderCommandHandlers) {
return sagaCommandDispatcherFactory.make("orderService", orderCommandHandlers.commandHandlers());
}
@Bean
public KitchenServiceProxy kitchenServiceProxy() {
return new KitchenServiceProxy();
}
@Bean
public OrderServiceProxy orderServiceProxy() {
return new OrderServiceProxy();
}
orderService, creatOrderSaga, orderCommandHandlers, orderCommandHandlersDispatcher
등 다양한 스프링 빈이 저장되어 있고, KitchenServiceProxy, orderServiceProxy
등 프록시 클래스를 가리키는 스프링 빈도 있다.cancleOrder()
는 주문 취소 사가, reviseOrder()
는 주문 변경 사가를 사용한다.반대로 오케스트레이션으로 구현을 했을 경우에는 해당 서비스를 새로 만들고 오케스트레이션에 코드만 추가하면 되죠
마이크로서비스 패턴 (저자: 크리스 리처드슨)