juchanei

Event sourcing


https://microservices.io/patterns/data/event-sourcing.html을 번역한 글입니다.

Context

전형적인 서비스 커맨드는 데이터베이스를 업데이트 하는 동시에 메시지/이벤트를 발송할 필요가 있다. 예를 들면, saga에 참여하는 서비스는 데이터베이스 업데이트와 메시지/이벤트 발행을 원자적으로 수행해야 한다. 도메인 이벤트(domain event)를 발행하는 서비스 역시 aggregate을 업데이트하고 이벤트를 발행하는 동작을 원자적으로 수행해야 한다.

서비스 커맨드는 일반적으로 데이터베이스를 업데이트하는 동시에 메시지/이벤트를 발송한다. 그러나 메시지 브로커는 보통 전통적인 분산 트랜잭션(Two phase commit, 2PC)를 지원하지 않기 때문에, 데이터베이스와 메시지 브로커에 걸쳐 업데이트와 메시지/이벤트 발행을 원자적으로 수행할 수 없다. 만약 가능하다 하더라도 서비스가 데이터베이스와 메시지에 모두 결합되는 구조는 바람직 하지 않을 수 있다.

하지만 2PC를 사용하지 않으면, 트랜잭션 중에 메시지가 발송된 것을 확신할 수 없다. 트랜잭션이 커밋 되는 것을 보장할 수 없을 뿐만 아니라, 트랜잭션 커밋 후 메시지 발송 중에 크래시가 발생하지 않는다는 것 또한 보장할 수 없다.

더불어, 메시지는 반드시 서비스가 발송한 순서대로 메시지 브로커에 도착해야 한다. 예를 들면, 서비스에서 한 aggregate이 트랜잭션 T1, T2에 의해 업데이트 된다고 하자. 두 트랜잭션은 같은 서비스 인스턴스에서 수행될 수도 있고 다른 서비스 인스턴스에서 수행될 수도 있다. 각각 트랜잭션은 대응되는 이벤트 E1, E2가 있다. 이 때 만일 트랜잭션 T1 다음에 T2가 수행된다면, 이벤트 E2는 반드시 E1 다음에 발행되어야 한다.

Problem

어떻게 데이터베이스 업데이트와 메시지/이벤트 발행을 안정적/원자적으로 할 수 있을까?

Forces

  • 2PC는 사용하지 않는다.
  • 만약 데이터베이스 트랜잭션이 커밋됐다면, 메시지는 반드시 발송되어야 한다. 반대로 데이터베이스가 롤백되었다면, 메시지는 발송되지 않아야 한다.
  • 메시지는 반드시 서비스가 발송한 순서대로 메시지 브로커에 도착해야 한다. 같은 aggregate을 업데이트하는 다수에 서비스 인스턴스에 걸쳐서 이 순서는 반드시 지켜져야 한다.

Solution

이를 해결하기 위한 좋은 방안으로 이벤트 소싱이 있다. 이벤트 소싱은 Order, Customer와 같은 비즈니스 엔터티의 상태를 ‘연속된 상태 변화 이벤트’로 유지한다. 비즈니스 엔터티의 상태가 변경될 때 마다 이벤트 리스트에 새 이벤트가 추가된다. 이벤트를 한번에 저장하기 때문에 이 오퍼레이션은 기본적으로 원자적이다. 애플리케이션은 이 이벤트들을 다시 재생(replay)하여 엔터티를 재구성 한다.

애플리케이션은 이벤트를 이벤트 스토어(데이터베이스)에 저장한다. 이벤트 스토어는 이벤트 저장, 조회 뿐만 아니라 메시지 브로커 처럼 이벤트를 구독할 수 있도록 API를 제공한다. 따라서 한 서비스가 이벤트를 스토어에 저장하면 이를 구독하는 서비스들에게 이벤트가 전달 된다.

Customer와 같이 일부 엔터티는 매우 많은 이벤트를 가질 수 있다. 로딩을 최적화 하기 위해서 애플리케이션은 현재 엔터티 상태의 스냅샷을 주기적으로 저장한다. 애플리케이션은 가장 최근 스냅샷과 그 이후에 발생한 이벤트를 찾아서 현재 상태를 재구성한다. 결과적으로 적은 수의 이벤트만 재생하여 현재 상태를 얻을 수 있다.

Example

아래는 이벤트 소싱과 CQRS로 만든 Customers and Orders 예제 애플리케이션의 일부이다. 어플리캐이션은 Java로 작성되었으며 Spring Boot와 Eventuate를 사용한다.

아래 다이어그램은 Order를 어떻게 관리하는지 보여준다.

image

어플리케이션은 ORDERS 테이블에 Order의 현재 상태를 row로 저장하는 대신, 연속 된 Order 이벤트를 저장한다. CustomerServiceOrder 이벤트를 구독하고 자기 자신의 상태를 업데이트한다.

아래는 Order aggregate이다

public class Order extends ReflectiveMutableCommandProcessingAggregate<Order, OrderCommand> {

  private OrderState state;
  private String customerId;

  public OrderState getState() {
    return state;
  }

  public List<Event> process(CreateOrderCommand cmd) {
    return EventUtil.events(new OrderCreatedEvent(cmd.getCustomerId(), cmd.getOrderTotal()));
  }

  public List<Event> process(ApproveOrderCommand cmd) {
    return EventUtil.events(new OrderApprovedEvent(customerId));
  }

  public List<Event> process(RejectOrderCommand cmd) {
    return EventUtil.events(new OrderRejectedEvent(customerId));
  }

  public void apply(OrderCreatedEvent event) {
    this.state = OrderState.CREATED;
    this.customerId = event.getCustomerId();
  }

  public void apply(OrderApprovedEvent event) {
    this.state = OrderState.APPROVED;
  }

  public void apply(OrderRejectedEvent event) {
    this.state = OrderState.REJECTED;
  }

아래는 Order 이벤트를 구독하는 CustomerService의 이벤트 핸들러중 일부이다.

@EventSubscriber(id = "customerWorkflow")
public class CustomerWorkflow {

  @EventHandlerMethod
  public CompletableFuture<EntityWithIdAndVersion<Customer>> reserveCredit(
          EventHandlerContext<OrderCreatedEvent> ctx) {
    OrderCreatedEvent event = ctx.getEvent();
    Money orderTotal = event.getOrderTotal();
    String customerId = event.getCustomerId();
    String orderId = ctx.getEntityId();

    return ctx.update(Customer.class, customerId, new ReserveCreditCommand(orderTotal, orderId));
  }

}

핸들러는 고객의 신용 예약을 시도하여 OrderCreated 이벤트를 처리한다.

이 외에 이벤트 소싱을 어떻게 사용하는지 보여주는 몇가지 예제 애플리케이션이 있다.

Resulting context

이벤트 소싱의 장점

  • 이벤트 소싱은 EDA(Event-driven Architecture)를 구현하는데 있어 주요한 문제들을 해결하며, 상태 변화마다 안정적으로 이벤트를 발행하도록 한다.
  • 도메인 객체 대신 이벤트를 저장하기 때문에 객체-관계형 데이터베이스 간의 임피던스 미스매칭 문제를 피할 수 있다.
  • 이벤트 소싱은 비즈니스 엔터티가 변경될 때 마다 100% 신뢰할 수 있는 audit log를 제공한다.
  • 특정 시점의 상태를 결정하기 위한 위한 임시 쿼리 구현할 수 있다.
  • 이벤트 소싱 기반의 비즈니스 로직은 이벤트를 교환하는 느슨하게 결합의 엔터티로 구성된다. 따라서 모놀로딕 애플리케이션에서 마이크로서비스 아키텍처로 쉽게 마이그레이션 할 수 있다.

이벤트 소싱의 단점

  • 보통의 프로그래밍 스타일과 다르므로 러닝커브가 있다.
  • 스토어에 이벤트를 요청할 때 비즈니스 엔터티를 재구성하기 위한 반복적인 쿼리를 필요로 하기 때문에 어렵다. 이는 복잡하고 비효율적이다. 따라서, 어플리케이션은 반드시 CQRS(Command Query Responsibility Segregation)을 사용하여야 한다. 어플리케이션은 반드시 ‘최종적으로 일관된 데이터’를 다뤄야 함을 의미한다.
  • 이 패턴을 SagaDomain event 패턴을 위해 만들어 졌다.
  • 높은 확률로 이벤트 소싱과 CQRS는 함께 사용된다.
  • 이벤트 소싱은 Audit logging 패턴을 구현한다.

See also