Axon 프레임 워크 가이드

1. 개요

이 기사에서는 Axon 을 살펴 보고 CQRS (Command Query Responsibility Segregation) 및 이벤트 소싱 을 염두에두고 애플리케이션을 구현하는 데 어떻게 도움이되는지 살펴 봅니다 .

이 가이드에서는 Axon Framework와 Axon Server가 모두 활용됩니다. 전자는 구현을 포함하고 후자는 전용 이벤트 저장소 및 메시지 라우팅 솔루션이 될 것입니다.

우리가 구축 할 샘플 애플리케이션은 주문 도메인 에 중점을 둡니다 . 이를 위해 Axon이 제공하는 CQRS 및 Event Sourcing 빌딩 블록을 활용할 것 입니다.

많은 공유 개념이 DDD에서 바로 나온다는 점에 유의 하세요.이 문서의 범위를 벗어납니다.

2. Maven 종속성

Axon / Spring Boot 애플리케이션을 만들 것입니다. 따라서 최신 axon-spring-boot-starter 종속성을 pom.xml 에 추가하고 테스트를 위한 axon-test 종속성을 추가해야합니다.

 org.axonframework axon-spring-boot-starter 4.1.2   org.axonframework axon-test 4.1.2 test 

3. Axon 서버

Axon Server를 이벤트 저장소이자 전용 명령, 이벤트 및 쿼리 라우팅 솔루션으로 사용할 것입니다.

이벤트 스토어로서 이벤트를 저장할 때 필요한 이상적인 특성을 제공합니다. 이 기사는 이것이 바람직한 이유를 설명합니다.

메시지 라우팅 솔루션으로서 RabbitMQ 또는 Kafka 토픽과 같은 항목을 구성하는 데 초점을 맞추지 않고 여러 인스턴스를 함께 연결하여 메시지를 공유하고 발송하는 옵션을 제공합니다.

Axon Server는 여기에서 다운로드 할 수 있습니다. 간단한 JAR 파일이므로 다음 작업으로 충분합니다.

java -jar axonserver.jar

그러면 localhost : 8024를 통해 액세스 할 수있는 단일 Axon Server 인스턴스가 시작됩니다 . 엔드 포인트는 연결된 애플리케이션 및 이들이 처리 할 수있는 메시지에 대한 개요와 Axon Server에 포함 된 이벤트 저장소에 대한 쿼리 메커니즘을 제공합니다.

axon-spring-boot-starter 종속성 과 함께 Axon Server의 기본 구성은 주문 서비스가 자동으로 연결되도록합니다.

4. 주문 서비스 API – 명령

CQRS를 염두에두고 주문 서비스를 설정할 것입니다. 따라서 우리는 응용 프로그램을 통해 흐르는 메시지를 강조 할 것입니다.

먼저 의도의 표현을 의미하는 명령을 정의합니다. 주문 서비스는 다음 세 가지 유형의 작업을 처리 할 수 ​​있습니다.

  1. 새 주문하기
  2. 주문 확인
  3. 주문 배송

당연히 도메인에서 처리 할 수있는 세 가지 명령 메시지 ( PlaceOrderCommand , ConfirmOrderCommandShipOrderCommand)가 있습니다 .

public class PlaceOrderCommand { @TargetAggregateIdentifier private final String orderId; private final String product; // constructor, getters, equals/hashCode and toString } public class ConfirmOrderCommand { @TargetAggregateIdentifier private final String orderId; // constructor, getters, equals/hashCode and toString } public class ShipOrderCommand { @TargetAggregateIdentifier private final String orderId; // constructor, getters, equals/hashCode and toString }

TargetAggregateIdentifier의 주석은 주석 필드는 명령이 대상이되어야하는 주어진 집합의 ID입니다 축삭을 알려줍니다. 이 기사의 뒷부분에서 집계에 대해 간략하게 설명하겠습니다.

또한 명령의 필드를 최종 항목으로 표시했습니다. 이는 모든 메시지 구현이 변경 불가능한 모범 사례 이므로 의도적 입니다.

5. 주문 서비스 API – 이벤트

집계는 주문을 접수, 확인 또는 배송 할 수 있는지 여부를 결정하는 역할을하므로 명령을 처리합니다 .

이벤트를 게시하여 나머지 응용 프로그램에 대한 결정을 알립니다. OrderPlacedEvent, OrderConfirmedEventOrderShippedEvent 의 세 가지 유형의 이벤트가 있습니다 .

public class OrderPlacedEvent { private final String orderId; private final String product; // default constructor, getters, equals/hashCode and toString } public class OrderConfirmedEvent { private final String orderId; // default constructor, getters, equals/hashCode and toString } public class OrderShippedEvent { private final String orderId; // default constructor, getters, equals/hashCode and toString }

6. 명령 모델 – 오더 집계

이제 명령 및 이벤트와 관련하여 핵심 API를 모델링 했으므로 명령 모델 생성을 시작할 수 있습니다.

도메인이 주문 처리에 초점을 맞추기 때문에 명령 모델의 중심으로 OrderAggregate생성 할 것 입니다.

6.1. 집계 클래스

따라서 기본 집계 클래스를 생성 해 보겠습니다.

@Aggregate public class OrderAggregate { @AggregateIdentifier private String orderId; private boolean orderConfirmed; @CommandHandler public OrderAggregate(PlaceOrderCommand command) { AggregateLifecycle.apply(new OrderPlacedEvent(command.getOrderId(), command.getProduct())); } @EventSourcingHandler public void on(OrderPlacedEvent event) { this.orderId = event.getOrderId(); orderConfirmed = false; } protected OrderAggregate() { } }

집계 주석은 집계로이 클래스를 표시 축삭 봄 특정 주석이다. 이 OrderAggregate에 대해 필요한 CQRS 및 이벤트 소싱 특정 빌딩 블록을 인스턴스화해야 함을 프레임 워크에 알립니다 .

집계는 특정 집계 인스턴스를 대상으로하는 명령을 처리하므로 AggregateIdentifier 주석으로 식별자를 지정해야합니다 .

우리의 집계는 OrderAggregate '명령 처리 생성자' 에서 PlaceOrderCommand 를 처리 할 때 수명주기를 시작합니다 . 주어진 함수가 명령을 처리 할 수 ​​있음을 프레임 워크에 알리기 위해 CommandHandler 주석을 추가합니다 .

PlaceOrderCommand를 처리 할 때 OrderPlacedEvent 를 게시하여 주문이 접수되었음을 나머지 애플리케이션에 알립니다. 집계 내에서 이벤트를 게시하려면 AggregateLifecycle # apply (Object…)를 사용 합니다.

이 시점부터 실제로 이벤트 스트림에서 집계 인스턴스를 다시 만드는 원동력으로 이벤트 소싱을 통합하기 시작할 수 있습니다.

We start this off with the ‘aggregate creation event', the OrderPlacedEvent, which is handled in an EventSourcingHandler annotated function to set the orderId and orderConfirmed state of the Order aggregate.

Also note that to be able to source an aggregate based on its events, Axon requires a default constructor.

6.2. Aggregate Command Handlers

Now that we have our basic aggregate, we can start implementing the remaining command handlers:

@CommandHandler public void handle(ConfirmOrderCommand command) { apply(new OrderConfirmedEvent(orderId)); } @CommandHandler public void handle(ShipOrderCommand command) { if (!orderConfirmed) { throw new UnconfirmedOrderException(); } apply(new OrderShippedEvent(orderId)); } @EventSourcingHandler public void on(OrderConfirmedEvent event) { orderConfirmed = true; }

The signature of our command and event sourcing handlers simply states handle({the-command}) and on({the-event}) to maintain a concise format.

Additionally, we've defined that an Order can only be shipped if it's been confirmed. Thus, we'll throw an UnconfirmedOrderException if this is not the case.

This exemplifies the need for the OrderConfirmedEvent sourcing handler to update the orderConfirmed state to true for the Order aggregate.

7. Testing the Command Model

First, we need to set up our test by creating a FixtureConfiguration for the OrderAggregate:

private FixtureConfiguration fixture; @Before public void setUp() { fixture = new AggregateTestFixture(OrderAggregate.class); }

The first test case should cover the simplest situation. When the aggregate handles the PlaceOrderCommand, it should produce an OrderPlacedEvent:

String orderId = UUID.randomUUID().toString(); String product = "Deluxe Chair"; fixture.givenNoPriorActivity() .when(new PlaceOrderCommand(orderId, product)) .expectEvents(new OrderPlacedEvent(orderId, product));

Next, we can test the decision-making logic of only being able to ship an Order if it's been confirmed. Due to this, we have two scenarios — one where we expect an exception, and one where we expect an OrderShippedEvent.

Let's take a look at the first scenario, where we expect an exception:

String orderId = UUID.randomUUID().toString(); String product = "Deluxe Chair"; fixture.given(new OrderPlacedEvent(orderId, product)) .when(new ShipOrderCommand(orderId)) .expectException(IllegalStateException.class); 

And now the second scenario, where we expect an OrderShippedEvent:

String orderId = UUID.randomUUID().toString(); String product = "Deluxe Chair"; fixture.given(new OrderPlacedEvent(orderId, product), new OrderConfirmedEvent(orderId)) .when(new ShipOrderCommand(orderId)) .expectEvents(new OrderShippedEvent(orderId));

8. The Query Model – Event Handlers

So far, we've established our core API with the commands and events, and we have the Command model of our CQRS Order service, the Order aggregate, in place.

Next, we can start thinking of one of the Query Models our application should service.

One of these models is the OrderedProducts:

public class OrderedProduct { private final String orderId; private final String product; private OrderStatus orderStatus; public OrderedProduct(String orderId, String product) { this.orderId = orderId; this.product = product; orderStatus = OrderStatus.PLACED; } public void setOrderConfirmed() { this.orderStatus = OrderStatus.CONFIRMED; } public void setOrderShipped() { this.orderStatus = OrderStatus.SHIPPED; } // getters, equals/hashCode and toString functions } public enum OrderStatus { PLACED, CONFIRMED, SHIPPED }

We'll update this model based on the events propagating through our system. A Spring Service bean to update our model will do the trick:

@Service public class OrderedProductsEventHandler { private final Map orderedProducts = new HashMap(); @EventHandler public void on(OrderPlacedEvent event) { String orderId = event.getOrderId(); orderedProducts.put(orderId, new OrderedProduct(orderId, event.getProduct())); } // Event Handlers for OrderConfirmedEvent and OrderShippedEvent... }

As we've used the axon-spring-boot-starter dependency to initiate our Axon application, the framework will automatically scan all the beans for existing message-handling functions.

As the OrderedProductsEventHandler has EventHandler annotated functions to store an OrderedProduct and update it, this bean will be registered by the framework as a class that should receive events without requiring any configuration on our part.

9. The Query Model – Query Handlers

Next, to query this model, for example, to retrieve all the ordered products, we should first introduce a Query message to our core API:

public class FindAllOrderedProductsQuery { }

Second, we'll have to update the OrderedProductsEventHandler to be able to handle the FindAllOrderedProductsQuery:

@QueryHandler public List handle(FindAllOrderedProductsQuery query) { return new ArrayList(orderedProducts.values()); }

The QueryHandler annotated function will handle the FindAllOrderedProductsQuery and is set to return a List regardless, similarly to any ‘find all' query.

10. Putting Everything Together

We've fleshed out our core API with commands, events, and queries, and set up our Command and Query model by having an OrderAggregate and OrderedProducts model.

Next is to tie up the loose ends of our infrastructure. As we're using the axon-spring-boot-starter, this sets a lot of the required configuration automatically.

First, as we want to leverage Event Sourcing for our Aggregate, we'll need an EventStore. Axon Server which we have started up in step three will fill this hole.

Secondly, we need a mechanism to store our OrderedProduct query model. For this example, we can add h2 as an in-memory database and spring-boot-starter-data-jpa for ease of use:

 org.springframework.boot spring-boot-starter-data-jpa com.h2database h2 runtime 

10.1. Setting up a REST Endpoint

Next, we need to be able to access our application, for which we'll be leveraging a REST endpoint by adding the spring-boot-starter-web dependency:

 org.springframework.boot spring-boot-starter-web 

From our REST endpoint, we can start dispatching commands and queries:

@RestController public class OrderRestEndpoint { private final CommandGateway commandGateway; private final QueryGateway queryGateway; // Autowiring constructor and POST/GET endpoints }

The CommandGateway is used as the mechanism to send our command messages, and the QueryGateway, in turn, to send query messages. The gateways provide a simpler, more straightforward API, compared to the CommandBus and QueryBus that they connect with.

From here on, our OrderRestEndpoint should have a POST endpoint to place, confirm, and ship an order:

@PostMapping("/ship-order") public void shipOrder() { String orderId = UUID.randomUUID().toString(); commandGateway.send(new PlaceOrderCommand(orderId, "Deluxe Chair")); commandGateway.send(new ConfirmOrderCommand(orderId)); commandGateway.send(new ShipOrderCommand(orderId)); }

This rounds up the Command side of our CQRS application.

Now, all that's left is a GET endpoint to query all the OrderedProducts:

@GetMapping("/all-orders") public List findAllOrderedProducts() { return queryGateway.query(new FindAllOrderedProductsQuery(), ResponseTypes.multipleInstancesOf(OrderedProduct.class)).join(); }

In the GET endpoint, we leverage the QueryGateway to dispatch a point-to-point query. In doing so, we create a default FindAllOrderedProductsQuery, but we also need to specify the expected return type.

As we expect multiple OrderedProduct instances to be returned, we leverage the static ResponseTypes#multipleInstancesOf(Class) function. With this, we have provided a basic entrance into the Query side of our Order service.

We completed the setup, so now we can send some commands and queries through our REST Controller once we've started up the OrderApplication.

POST-ing to endpoint /ship-order will instantiate an OrderAggregate that'll publish events, which, in turn, will save/update our OrderedProducts. GET-ing from the /all-orders endpoint will publish a query message that'll be handled by the OrderedProductsEventHandler, which will return all the existing OrderedProducts.

11. Conclusion

In this article, we introduced the Axon Framework as a powerful base for building an application leveraging the benefits of CQRS and Event Sourcing.

We implemented a simple Order service using the framework to show how such an application should be structured in practice.

Lastly, Axon Server posed as our Event Store and the message routing mechanism.

The implementation of all these examples and code snippets can be found over on GitHub.

For any additional questions you may have, also check out the Axon Framework User Group.