이벤트 소싱(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) 데이터 구조가 단순합니다.

도메인 모델의 정규 데이터인 도메인 이벤트는 단순한 구조로 저장됩니다. 때문에 도메인 이벤트를 저장할 대상으로 관계형 데이터베이스, 문서 데이터베이스, 키-값 저장소, BLOB 저장소, 또는 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)라는 용어는 도메인 주도 설계에서 왔습니다. 이벤트 소싱을 사용한 시스템을 만들 때 도메인 주도 설계에서 많은 통찰력을 얻을 수 있습니다.

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

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

정리

글을 시작할 때 언급했 듯 이 글은 이벤트 소싱의 가장 기초적인 개념만을 다뤘습니다. 이벤트 소싱을 적절한 곳에 사용하고 활용을 극대화하려면 알아야할 것들이 많습니다. 앞으로 이벤트 소싱을 구현하고 마이크로서비스 아키텍처 등 분산 시스템에 적용하기 위해 필요한 여러가지 내용들을 다루겠습니다.

참고

Advertisements

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

  1. jino

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

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

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Google+ photo

Google+의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

%s에 연결하는 중