TIL

04 트랜잭션 관리: 사가

4.1 마이크로서비스 아키텍처에서의 트랜잭션 관리

4.1.1 분산 트랜잭션의 필요성

4.1.2 분산 트랜잭션의 문제점

4.1.3 데이터 일관성 유지: 사가 패턴

예제: 주문 생성 사가

사가는 보상 트랜잭션으로 변경분을 롤백한다.

단계 서비스 트랜잭션 보상 트랜잭션
1 주문 서비스 createOrder() rejectOrder()
2 소비자 서비스 verifyConsumerDetail() -
3 주방 서비스 createTicket() rejectTicket()
4 회계 서비스 authorizeCreditCard() -
5 주방 서비스 approveTicket() -
6 주문 서비스 approveOrder() -

4.2 사가 편성

4.2.1 코레오그래피 사가

주문 생성 사가 구현: 코레오그래피 스타일

확실한 이벤트 기반 통신

코레오그래피 사가의 장단점

4.2.2 오케스트레이션 사가

주문 생성 사가 구현: 오케스트레이션 스타일

사가 오케스트레이터를 상태 기계로 모델링

사가 오케스트레이션과 트랜잭셔널 메시징

오케스트레이션 사가의 장단점

4.3 비격리 문제 처리

4.3.1 비정상 개요

4.3.2 비격리 대책

사가의 구조

단계 서비스 트랜잭션 보상 트랜잭션
1 주문 서비스 createOrder() rejectOrder()
2 소비자 서비스 verifyConsumerDetail() -
3 주방 서비스 createTicket() rejectTicket()
4 회계 서비스 authorizeCreditCard() -
5 주방 서비스 approveTicket() -
6 주문 서비스 approveOrder() -

대책: 시멘틱 락

대책: 교환적 업데이트

대책: 비관적 관점

대책: 값 다시 읽기

대책: 버전 파일

대책: 값에 의한

4.4 주문 서비스 및 주문 생성 사가 설계

4.4.1 OrderService 클래스

@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;
    }
}

4.4.2 주문 생성 사가 구현

CreateOrderSaga 오케스트레이터

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

CreateOrderSagaState 클래스

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

    // ...
}

KitchenServiceProxy

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

이벤추에이트 트램 사가 프레임워크

4.4.3 OrderCommandHandlers 클래스

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