INSERT INTO users (username, password, phone_number) VALUES ('유저이름', '비밀번호123', '010-1234-5678');
START TRANSACTION;
INSERT INTO users (username, password, phone_number) VALUES ('taeyun', 'password', '010-1234-5678');
IF (SELECT COUNT(*) FROM users WHERE username = 'taeyun') > 0 THEN
ROLLBACK;
ELSE
COMMIT;
END IF;
START TRANSACTION;
→ 트랙잭션을 시작하겠다는 뜻이다.IF (SELECT COUNT(*) FROM users WHERE username = 'taeyun') > 0 THEN
→ 아이디에 대한 중복을 체크하는 로직이다.ROLLBACK;
→ 위 로직이 실패시 트랙잭션을 취소하고 START TRANSACTION 실행 전 상태로 롤백한다.COMMIT;
→ 위 로직이 성공시 트랙잭션을 DB에 반영한다.비교 요소 | Choreography-based Saga | Orchestration-based Saga |
---|---|---|
시스템 복잡성 | 높음 (시스템이 확장됨에 따라 복잡성이 증가) | 중간 (중앙 중계자가 복잡성을 관리) |
오류 처리 및 롤백 | 복잡 (각 서비스가 자체 롤백을 관리) | 단순 (중앙 중계자가 롤백 전략을 관리) |
결합도 | 낮음 (서비스간 느슨한 결합) | 높음 (중앙 중계자가 의한 강한 결합) |
추적성 | 낮음 (전체 흐름 추적이 어려움) | 높음 (중앙 중계자를 통한 명확한 트랜잭션 추적) |
유지보수 및 관리 | 어려움 (변경 및 확장이 복잡함) | 용이 (중앙 중계자를 통해 쉽게 관리 및 확장 가능) |
비즈니스 로직 집중화 | 낮음 (비즈니스 로직이 분산됨) | 높음 (중앙 중계자에 비즈니스 로직 집중) |
해당 비즈니스 로직에는 여러 커멘드와 이벤트들이 존재한다. 하나씩 파헤쳐보겠다.
CreateOrderCommand
OrderCreatedEvent
ReduceStockCommand
StockReducedEvent
StockSoldOutEvent
CompleteOrderCommand
, CancelOrderCommand
OrderCompletedEvent
→ End SagaOrderCancelledEvent
→ End Sagadependencies {
// Axon Framework
implementation 'org.axonframework:axon-spring-boot-starter:4.8.0' // 최신 버전에 따라 업데이트 필요
}
public class RegisterOrderService implements RegisterOrderUseCase {
private final SaveOrderPort saveOrderPort;
@Autowired
private CommandGateway commandGateway;
@Override
public void registerOrder(CreateOrderCommand command) {
Order order = Order.builder()
.receiverName(command.getReceiverName())
.receiverPhone(command.getReceiverPhone())
.receiverAddress(command.getReceiverAddress())
.userId(command.getUserId())
.orderStatus(OrderStatus.ORDER_CREATED)
.build();
Order saveOrder = saveOrderPort.saveOrder(order);
command.setOrderId(saveOrder.getOrderId());
commandGateway.sendAndWait(command);
}
}
CommandGateway
→ Axon Framework의 인터페이스이다. 이 인터페이스는 커맨드를 발행하고 결과를 반환하는 메서드를 제공한다.command.setOrderId(saveOrder.getOrderId())
→ 이벤트 또는 커맨드 처리를 위해 주문 id가 필요하기 때문에 넣어주었다.commandGateway.sendAndWait(command)
→ axon에서 관리하는 커멘드 버스로 보내준다. 그리고 해당 커멘드를 구독하고 있는 곳으로 통신이 된다.@Aggregate
@NoArgsConstructor
public class OrderAggregate {
@AggregateIdentifier
private Long orderId;
private OrderStatus orderStatus; // 주문 상태 (ex: CREATED, COMPLETED, CANCELLED)
private List<OrderCreatedEvent.OrderItemInfo> orderItems;
@CommandHandler
public OrderAggregate(CreateOrderCommand command) {
this.orderId = command.getOrderId();
apply(new OrderCreatedEvent(command.getOrderId(), command.getOrderItemInfos()));
}
@EventSourcingHandler
public void on(OrderCreatedEvent event) {
this.orderId = event.getOrderId();
this.orderItems = event.getOrderItems();
this.orderStatus = OrderStatus.ORDER_CREATED;
}
@EventSourcingHandler
public void on(StockReducedEvent event) {
this.orderId = event.getOrderId();
}
@EventSourcingHandler
public void on(StockSoldOutEvent event) {
this.orderId = event.getOrderId();
this.orderStatus = OrderStatus.ORDER_CANCEL;
}
}
@Aggregate
→ DDD(Domain-Driven Design)에서 한 덩어리의 일관성 경계를 구성하며 해당 경계 내에서의 데이터 변경을 관리합니다.@AggregateIdentifier
→ 해당 어노테이션이 붙은 필드가 Aggregate의 유니크 식별자임을 나타낸다. 이 식별자를 통해 여러 커맨드와 이벤트가 해당 Aggregate와 연결된다.OrderAggregate(CreateOrderCommand command)
→ CreateOrderCommand를 처리하는 메소드이고 내부적으로 OrderCreatedEvent를 발생시킨다.on(CreateOrderCommand command)
로 사용할 수 없는 이유?OrderAggregate(CreateOrderCommand command)
는 Aggregate의 초기 상태를 생성하는데 사용되며, Axon 프레임워크가 이를 특별하게 처리한다. 생성자 내에서는 Aggregate의 식별자를 초기화하고 첫 번째 이벤트를 발생시킨다.on(CreateOrderCommand command)
이렇게 사용하게 되면 Aggregate의 생성자가 아니게 되므로, Aggregate의 생성 및 초기화 과정에서 문제가 생길수 있다.on(OrderCreatedEvent event)
→ OrderCreatedEvent를 처리하는 핸들러이다.this.orderStatus = OrderStatus.ORDER_CREATED;
로 넣어주었다.on(StockReducedEvent event)
→ StockReducedEvent를 처리하는 핸들러이다.on(StockSoldOutEvent event)
→ StockSoldOutEvent를 처리하는 핸들러이다.this.orderStatus = OrderStatus.ORDER_CANCEL;
로 넣어주었다.@Saga
@Slf4j
public class OrderManagementSaga {
@Autowired
private transient CommandGateway commandGateway;
@StartSaga
@SagaEventHandler(associationProperty = "orderId")
public void handle(OrderCreatedEvent event) {
log.info("OrderCreatedEvent received for Order ID: " + event.getOrderId() + ". Reducing stock for order items.");
List<ReduceStockCommand.OrderItem> items = event.getOrderItems().stream()
.map(orderItemInfo -> new ReduceStockCommand.OrderItem(orderItemInfo.getProductId(), orderItemInfo.getCount()))
.collect(Collectors.toList());
// 주문 생성 후 재고 감소 커맨드 전송
commandGateway.send(new ReduceStockCommand(event.getOrderId(), items));
}
@SagaEventHandler(associationProperty = "orderId")
public void handle(StockReducedEvent event) {
log.info("Stock successfully reduced for Order ID: " + event.getOrderId() + ". Completing the order.");
// 재고가 성공적으로 줄어들면 주문 완료 커맨드 전송
commandGateway.send(new CompleteOrderCommand(event.getOrderId ()));
}
@SagaEventHandler(associationProperty = "orderId")
public void handle(StockSoldOutEvent event) {
log.info("Stock fail sold out for Order ID: " + event.getOrderId() + ". Cancelling the order.");
// 재고 부족 시 주문 취소 커맨드 전송
commandGateway.send(new CancelOrderCommand(event.getOrderId()));
}
@EndSaga // saga에 생명주기가 끝났음을 나타냄.
@SagaEventHandler(associationProperty = "orderId")
public void handle(OrderCompletedEvent event) {
log.info("Order with ID: " + event.getOrderId() + " has been successfully completed.");
}
@EndSaga // saga에 생명주기가 끝났음을 나타냄.
@SagaEventHandler(associationProperty = "orderId")
public void handle(OrderCancelledEvent event) {
log.info("Order with ID: " + event.getOrderId() + " has been cancel completed.");
}
}
CommandGateway
→ Axon Framework의 인터페이스이다. 이 인터페이스는 커맨드를 발행하고 결과를 반환하는 메서드를 제공한다.@SagaEventHandler(associationProperty = "orderId")
→ 해당 메서드가 사가 이벤트 핸들러로 동작하며, 이벤트 객체의 orderId 속성을 기반으로 사가 인스턴스와 연결한다.handle(OrderCreatedEvent event)
→ 주문이 생성되면, 이 핸들러가 호출되어 재고 감소 커맨드를 전송한다.handle(StockReducedEvent event)
→ 재고가 성공적으로 감소하면, 이 핸들러가 호출되어 주문 완료 커맨드를 전송한다.handle(StockSoldOutEvent event)
→ 재고가 부족하면, 이 핸들러가 호출되어 주문 취소 커맨드를 전송한다.handle(OrderCompletedEvent event)
와 handle(OrderCancelledEvent event)
→ 주문이 완료되거나 취소되면, 각각의 이벤트 핸들러가 호출되어 사가의 생명주기를 종료한다.@Getter
@AllArgsConstructor
public class ReduceStockCommand {
@TargetAggregateIdentifier
private Long orderId;
private List<OrderItem> items;
@Data
@AllArgsConstructor
public static class OrderItem {
private Long productId;
private Integer count;
}
}
ReduceStockCommand
는 Order-service에서 Orchestration Saga를 통해 ReduceStockCommand
를 보내게 된다.ReduceStockCommand
는 Stock-service에서 StockCommandHandler를 통해 관리가 되므로 Order-service, Stock-service 두개 서비스 모두에서 사용이 된다.@TargetAggregateIdentifier
→ 어떤 Aggregate 인스턴스가 이 커맨드를 처리해야 하는지를 Axon Framework에 알려주는 역할을 한다. 이를 통해, Axon Framework는 커맨드를 올바른 Aggregate 인스턴스로 라우팅할 수 있다.OrderAggregate(CreateOrderCommand command)
)를 통해 Aggregate 인스턴스가 생성된다.@Component
@AllArgsConstructor
public class StockCommandHandler {
@Autowired
private final EventGateway eventGateway;
private final ReduceStockHandlerUseCase reduceStockHandlerUseCase;
@CommandHandler
public void handle(ReduceStockCommand command) {
try {
boolean allSuccess = true;
for (ReduceStockCommand.OrderItem orderItem : command.getItems()) {
try {
// 재고 감소 로직
reduceStockHandlerUseCase.reduceStock(orderItem);
} catch (RuntimeException e) {
allSuccess = false;
log.info("재고 감소 실패: " + orderItem.getProductId());
// 다른 처리 로직 (예: 실패한 상품에 대한 정보 저장)을 여기에 추가할 수 있습니다.
break;
}
}
if (allSuccess) {
// 재고 감소에 성공하면 StockReducedEvent 발행
eventGateway.publish(new StockReducedEvent(command.getOrderId()));
} else {
// 재고 부족시 StockSoldOutEvent 발행
eventGateway.publish(new StockSoldOutEvent(command.getOrderId()));
}
} catch (RuntimeException e) {
log.error("Unexpected error", e);
}
}
}
commandGateway.send(new ReduceStockCommand(event.getOrderId(), items))
이 코드가 실행 됐을 때 StockCommandHandler(Stock-service)에서 handle(ReduceStockCommand command)
메소드가 실행이 된다.handle(ReduceStockCommand command)
→ 재고 감소에 대한 비즈니스 로직을 처리 한다.StockReducedEvent
발행StockSoldOutEvent
발행StockReducedEvent
발행되면 OrderManagementSaga에서 handle(StockReducedEvent event)
메소드가 실행된다.StockSoldOutEvent
발행되면 OrderManagementSaga에서 handle(StockSoldOutEvent event)
메소드가 실행된다.@Component
@AllArgsConstructor
public class OrderCommandHandler {
@Autowired
private EventGateway eventGateway;
private final RegisterOrderUseCase registerOrderUseCase;
private final CancelOrderUseCase cancelOrderUseCase;
@CommandHandler
public void handle(CompleteOrderCommand command) {
registerOrderUseCase.completeOrder(command.getOrderId());
eventGateway.publish(new OrderCompletedEvent(command.getOrderId()));
log.info("Order completed with ID: " + command.getOrderId());
}
@CommandHandler
public void handle(CancelOrderCommand command) {
cancelOrderUseCase.CancelOrder(command.getOrderId());
eventGateway.publish(new OrderCancelledEvent(command.getOrderId()));
log.info("Order cancel with ID: " + command.getOrderId());
}
}
StockReducedEvent
, StockSoldOutEvent
는 각각CompleteOrderCommand
, CancelOrderCommand
를 발행시킨다.handle(CompleteOrderCommand command)
→ 주문 완료에 대한 비즈니스 로직을 처리한다.OrderCompletedEvent
는 OrderManagementSaga
에서 EndSaga를 통해 Saga 인스턴스를 종료한다.handle(CancelOrderCommand command)
→ 주문 취소에 대한 비즈니스 로직을 처리한다.OrderCancelledEvent
는 OrderManagementSaga
에서 EndSaga를 통해 Saga 인스턴스를 종료한다.