이벤트 소싱(Event Sourcing) 소개

이벤트 소싱(Event Sourcing)

이벤트 소싱은 도메인 모델에서 발생하는 모든 이벤트를 기록하는 데이터 저장 기법이다. 이벤트 소싱은 클라우드에서 구동되는 반응형 시스템에 적합하고 규모 확장이 쉽기 때문에 최근 더욱 주목받고 있다. 하지만 최종 상태만을 저장하는 기존 방식에 익숙한 프로그래머에게 이벤트 소싱은 낮선 기술이다. 이 글은 이벤트 소싱을 배울 때 가장 먼저 이해해야하는 기본적인 특성을 설명한다.

전통적 방식

관계형 데이터베이스를 사용하든 NoSQL 데이터베이스를 사용하든 시스템의 주요 데이터를 유지하기 위해 최종 상태를 저장하는 방식은 오랜 기간 주류로 자리잡아 왔다. 이 경우 일반적으로 시스템 변경 기록은 완벽한 일관성을 보장하지 않고 경우에 따라 일정 시간 유지된 후 영구삭제되기도 한다.

예를 들어 관계형 데이터베이스에 저장되는 주문 집합체(aggregate)를 가정하자. 주문 집합체는 주문 엔터티와 주문항목 등의 하위 엔터티를 포함한다. 각 엔터티는 외래 키(foreign key)로 연결된다. 주문항목 추가 명령이 주문 집합체에 전달되면 도메인 모델은 주문 집합체가 저장된 데이터베이스에 주문항목 엔티티를 추가하고, 주문항목 수량을 변경하는 명령이 전달되면 데이터베이스에서 기존 주문항목 엔터티의 상태를 갱신한다. 비슷하게 주문항목 삭제 명령에 대해선 데이터베이스에서 기존 주문항목 엔터티를 삭제한다.

주문을 만들고 수정하는 일련의 명령과 데이터베이스에 저장된 최종 실행결과는 다음과 같을 수 있다.

명령 목록

  1. 주문을 생성한다.
  2. 식별자가 9a37인 상품 3개를 주문에 추가한다.
  3. 식별자가 c52a인 상품 1개를 주문에 추가한다.
  4. 식별자가 c52a인 상품을 주문에서 삭제한다.
  5. 식별자가 a974인 상품 10개를 주문에 추가한다.
  6. 식별자가 9a37인 상품 수량을 5로 변경한다.

데이터베이스

주문 테이블
Id
7dd8
주문항목 테이블
Order_Id Item_Id Quantity
7dd8 9a37 5
7dd8 a974 10

주문이 만들어지고 수정되거나 삭제되면 관련된 시스템 구성요소가 후속 작업을 처리할 수 있도록 이벤트를 발행하거나 메시지를 보낼 수 있다. 주문항목이 추가되고 이벤트가 발행되는 절차는 다음과 유사할 것이다.

public class Order
{
    public ICollection OrderItems { get; }

    public void AddOrderItem(string itemId, int quantity)
    {
        OrderItems.Add(new OrderItem(itemId, quantity));
    }
}

public class ShopDomainController
{
    [HttpPost("api/orders/{orderId}/order_items")]
    public void PostOrderItem(
        string orderId,
        AddOrderCommand command)
    {
        Transaction transaction = db.BeginTransaction();

        Order order = db.FindOrder(orderId);
        order.AddOrderItem(command.ItemId, command.Quantity);

        transaction.Commit();

        eventPublisher.Publish(command.CreateEvent(orderId));
    }
}

코드에서 볼 수 있듯 이벤트 발행은 도메인 모델이 저장되는 트랜잭션 범위에 포함되지 않는다. 트랜잭션이 완료된 후 이벤트가 발행되기 전에 오류가 발생하면 변경된 도메인 모델은 저장되지만 관련 이벤트는 발행되지 않은 채 유실된다. 주문 도메인 모델을 참조하는 구성요소는 이벤트를 수신하지 못하기 때문에 후속 작업을 수행할 수 없으며 이벤트를 복원할 수도 없다.

도메인 이벤트 저장

이벤트 소싱은 도메인 모델에서 발생하는 모든 이벤트의 순차적 기록을 일급(first-class citizens) 데이터로 다루며 이러한 이벤트를 ‘도메인 이벤트’라 부른다. 이벤트 소싱에서 도메인 모델이나 엔터티의 상태는 초기 시점부터 현재까지 발생한 모든 도메인 이벤트의 결과물이다. 도메인 이벤트는 집합체에서 발생해 이벤트 저장소에 기록되며 집합체가 복원될 때 재생되어 집합체의 현재 상태를 구체화한다. 도메인 이벤트는 이미 과거에 일어난 사건이기 때문에 수정되거나 삭제되지 않는다.

이벤트 소싱을 사용해 주문 집합체를 만들면 작업은 앞서의 사례와는 다른 방식으로 진행된다. 주문 집합체를 위해 실행되는 데이터베이스 연산은 삽입(insert) 뿐이다. 주문항목의 수량이 변경된 것도, 주문항목이 삭제된 것도 모두 새로운 도메인 이벤트이기 때문에 갱신(update)이나 삭제(delete) 연산은 수행되지 않는다. 도메인 모델 데이터베이스에는 주문 집합체의 최종 상태가 아니라 그동안 발생한 도메인 이벤트 기록이 저장된다.

주문 집합체 도메인 이벤트 테이블
Order_Id Version Event_Type Event_Data
7dd8 1 OrderPlaced OrderId: 7dd8
7dd8 2 OrderItemAdded ItemId: 9a37, Quantity: 3
7dd8 3 OrderItemAdded ItemId: c52a, Quantity: 1
7dd8 4 OrderItemDeleted ItemId: c52a
7dd8 5 OrderItemAdded ItemId: a974, Quantity: 10
7dd8 6 OrderItemQuantityChanged ItemId: 9a37, Quantity: 5

이벤트 소싱에서 도메인 모델이 변경되었다는 것은 도메인 이벤트가 저장소에 기록되었음을 의미하기 때문에 이벤트는 안정적으로 관찰(observe)되거나 발행될 수 있다. 유실되는 도메인 이벤트는 존재하지 않는다.

이벤트 소싱이 적용된 주문 집합체와 컨트롤러가 주문항목을 추가하는 코드는 다음과 같은 모습이다.

public class Order : EventSourcedAggregate
{
    public ICollection OrderItems { get; }

    public void AddOrderItem(string itemId, int quantity)
    {
        RaiseEvent(new OrderItemAdded(itemId, quantity));
    }

    private void HandleEvent(OrderItemAdded domainEvent)
    {
        OrderItems.Add(new OrderItem(domainEvent.ItemId, domainEvent.Quantity));
    }
}

public class ShopDomainController
{
    [HttpPost("api/orders/{orderId}/order_items")]
    public void PostOrderItem(
        string orderId,
        AddOrderCommand command)
    {
        Order order = repository.Find(orderId);
        order.AddOrderItem(command.ItemId, command.Quantity);
        repository.Save(order);
    }
}

집합체의 공개(public) 메서드는 상태를 변경하지 않고 이벤트를 발생시킨다. 상태를 변경하는 책임은 이벤트 처리기(event-handler)에 있다. 이벤트 처리기는 명령이 실행될 때와 과거의 도메인 이벤트를 통해 집합체가 복원될 때 실행된다. 그리고 이벤트 소싱이 적용된 경우 컨트롤러(혹은 메시지 처리기)는 이벤트를 발행하는 별도의 코드를 가지지 않는다. 저장소에 집합체를 저장하면 도메인 이벤트가 발행된다.

이벤트 소싱의 장점

이벤트 소싱은 전통적 방식과 비교해 다음과 같은 장점을 가진다.

정규(Normalized) 데이터 구조가 단순하다.

도메인 모델의 정규 데이터인 도메인 이벤트는 단순한 구조로 저장된다. 때문에 도메인 이벤트를 저장할 대상으로 관계형 데이터베이스, 문서 데이터베이스, 키-값 저장소, 파일 시스템, 또는 Event Store와 같이 이벤트 소싱에 특화된 저장소 등 다양한 데이터베이스를 고려할 수 있고 분산 저장소에 적합해 규모 확장도 쉽다. 그리고 도메인 이벤트는 수정되거나 삭제되지 않으며 오직 추가만 되기 때문에 기존 데이터에 접근하기 위한 경쟁이 발생하지 않는다.

임피던스 불일치가 존재하지 않는다.

일반적으로 잘 정의된 도메인 모델을 메모리 상의 개체 그래프로 표현하는 것은 어렵지 않다. 하지만 전통적 방식에서 도메인 모델을 관계형 데이터베이스에 투영할 때에는 다양한 구조적 문제들이 발생하며 이것을 개체-관계형 임피던스 불일치(Object-Relational Impedance Mismatch)라고 한다. ORM 등의 도구를 사용해 반복작업을 줄일 수는 있지만 근본적인 해결책은 아니다. 반면 이벤트 소싱을 사용하면 도메인 모델은 직렬화된(serialized) 도메인 이벤트의 집합으로 저장되기 때문에 임피던스 불일치가 발생하지 않는다.

신뢰할 수 있는 시스템 기록을 확보할 수 있다.

도메인 모델의 모든 변경 내역은 도메인 이벤트로 기록되기 때문에 특정 시점까지의 이벤트를 재생하면 해당 시점의 도메인 모델이 복원된다. 시스템에 오류가 발생하면 프로그래머는 오류가 발생한 시점의 도메인 모델을 복원해 오류를 분석할 수 있다.

메시지 중심(Message-Driven) 아키텍처에 적합하다.

비동기 메시지 전달을 중심으로 한 설계는 반응형 시스템의 기반이다. 이미 설명된 것처럼 도메인 이벤트는 일급 데이터로 저장되기 때문에 이를 통해 ‘최소 일 회 배달(at-least-once delivery)’을 구현하기 용이하다. 도메인 모델에서 발생한 이벤트는 안정적으로 관찰될 수 있으며 반드시 발행됨을 보장할 수 있다. 또한 도메인 이벤트가 담긴 메시지는 높은 설명력을 가진다.

이벤트 소싱과 다른 기법의 조합

이벤트 소싱과 CQRS

이벤트 소싱은 가파른 학습곡선 등 단점도 가진다. 그 중 가장 치명적인 것은 이벤트 저장소가 비즈니스의 다양한 데이터 조회 요구를 수용하기 어렵다는 점이다. 이런 문제를 해결하기 위해 이벤트 소싱은 항상 CQRS(Command and Query Responsibility Segregation)와 함께 사용된다. 이벤트 소싱이 CQRS와 함께 사용되면 도메인 모델은 비즈니스 요구에 적합한 다양한 비정규(denormalized) 형상을 가질 수 있다.

이벤트 소싱과 도메인 주도 설계(Domain-Driven Design)

CQRS의 창시자인 Greg Young은 도메인 주도 설계의 고통을 해결하기 위해 CQRS를 사용했다고 말했다. 따라서 CQRS와 관련된 많은 요소들은 도메인 주도 설계에 기반한다. 이벤트 소싱 역시 그렇다. 이미 눈치챘겠지만 수차례 사용한 집합체(aggregate)라는 용어는 도메인 주도 설계에서 왔다. 이벤트 소싱을 사용한 시스템을 만들 때 도메인 주도 설계에서 많은 통찰력을 얻을 수 있다.

이벤트 소싱과 마이크로서비스 아키텍처(Microservices Architecture)

많은 프로그래머와 아키텍트들이 마이크로서비스 아키텍처에 메시지 중심의 비동기 작업 흐름을 도입해야 한다고 말하며 이미 관련 제품들이 출시되고 도입되고 있다. 그리고 어떤 사람들은 이런 성숙과정의 마지막에 이벤트 소싱을 배치하기도 한다. 이벤트 소싱이 반드시 마이크로서비스 아키텍처에서 사용되어야 하는 것은 아니지만 이벤트 소싱과 비동기 마이크로서비스 아키텍처는 서로 잘 들어맞아 시너지를 만든다.

참고

이벤트 소싱(Event Sourcing) 소개”에 대한 8개의 생각

  1. jino

    좋은 글 감사합니다.
    이벤트 소싱을 적용한 코드 에제에서 HandleEvent 메서드의 구현 부분에 혹시 오타가 없는건가요?

    응답
  2. 핑백: 이벤트 소싱 event-sourcing 패턴 정리 - Haruair

  3. 임정립

    이벤트 소싱 관련하여 컨설팅을 받으려고 합니다
    혹시 가능하신지 여쭙습니다

    응답
  4. 과객

    안녕하세요. 좋은 자료 감사합니다.
    위 전통적 방식 코드 예제를 아래와 같이 바꾸면(제가 C#을 몰라서 문법은 틀릴 수도 있습니다) DB 변경과 메시지 전달의 일관성을 보장할 수 있을 것 같은데, 학습 곡선이 가파른 이벤트 소싱 방식 대신 이 방식을 선택하는 것도 괜찮을까요(또는 이 방식은 어떤 단점이 있을까요)?

    public void PostOrderItem(
    string orderId,
    AddOrderCommand command)
    {
    boolean published = false;
    Transaction transaction = db.BeginTransaction();
    try
    {
    eventPublisher.Publish(command.CreateEvent(orderId));
    published = true;
    Order order = db.FindOrder(orderId);
    order.AddOrderItem(command.ItemId, command.Quantity);
    transaction.Commit();
    }
    catch (Exception e)
    {
    transaction.Rollback();
    if (published) eventPublisher.Publish(command.UndoCreateEvent(orderId));
    }
    }

    응답
    1. Gyuwon 글의 글쓴이

      관계형 데이터베이스 사용에 제약이 없고 트랜잭션이 성능에 문제를 주지 않으면 이벤트 발행을 위해 말씀하신 방법도 괜찮습니다. 이벤트 발행은 이벤트 소싱의 본질이 아니라 얻을 수 있는 효과 중 하나기 때문에 일관된 이벤트 발행이 필요한 상황이라면 꼭 이벤트 소싱을 선택할 필요는 없어요.

      응답
  5. 핑백: 수다콘 – 07 회 후기 – Place Of 42Seoul Story

댓글 남기기