이벤트 소싱의 핵심 개념
이벤트 소싱은 데이터를 ‘스냅샷’으로 저장하는 대신, ‘사실’의 시퀀스로 저장합니다. 여기서 ‘사실’은 애플리케이션 내에서 발생하는 비즈니스 이벤트(Event)를 의미합니다. 이벤트는 불변하며(Immutable), 이벤트 스토어(Event Store)라는 추가 전용(Append-only) 데이터베이스에 저장됩니다. 이벤트 스토어에 한 번 저장된 이벤트는 수정하거나 삭제할 수 없습니다.
이벤트와 스트림
-
이벤트(Event): 시스템 내에서 발생한 특정 사실을 나타내며, 고유 식별자, 데이터(payload), 메타데이터(예: 클라이언트 ID, 작업자 ID, 발생 시간)를 포함합니다.
-
스트림(Stream): 특정 비즈니스 엔티티(예: 장바구니, 배송)에 대한 논리적인 이벤트 시퀀스입니다. 하나의 이벤트는 최소한 하나의 전역 스트림(Global Stream)에 속하며, 필요에 따라 여러 스트림에 연결될 수 있습니다. 예를 들어, 장바구니 이벤트는 특정 장바구니 스트림과 특정 날짜의 주문 스트림에 동시에 연결될 수 있습니다.
상태 재구성 및 활용
이벤트 스토어에 저장된 이벤트 스트림을 읽고 순차적으로 적용(Left Fold)함으로써, 특정 시점의 엔티티 상태를 정확하게 재구성할 수 있습니다. 이는 비즈니스 로직 테스트, 과거 데이터 분석, 감사 추적 등에 매우 유용합니다. 예를 들어, ‘어떤 상품이 장바구니에서 가장 많이 버려졌는가?’와 같은 비즈니스 질문에 정확하게 답할 수 있습니다.
Rails Event Store
Rails Event Store는 Ruby on Rails 개발자를 위한 이벤트 소싱 라이브러리로, 기존의 PostgreSQL, MySQL, SQLite와 같은 관계형 데이터베이스 위에서 작동합니다. 이는 새로운 데이터베이스를 학습할 필요 없이 이벤트 소싱을 도입할 수 있게 합니다. Rails Event Store는 이벤트 및 스트림 정보를 저장하는 두 개의 테이블과 함께 마이그레이션, 인덱스, 이벤트 브라우저 등의 기능을 제공합니다.
실제 사용 사례: 창고 관리 시스템
창고 배송(Shipment) 관리 시스템에서, 배송 상태가 ‘포장됨’으로 변경될 때 기존 스냅샷 모델은 ‘포장’ 비용을 한 번만 청구합니다. 하지만 배송이 포장되었다가 운송 문제로 인해 다시 ‘보류’ 상태로 되돌아간 후 재포장되는 경우, 스냅샷 모델은 단 한 번의 포장 비용만 청구하게 되어 실제 작업 횟수를 반영하지 못합니다. 이벤트 소싱을 사용하면 ‘포장됨’, ‘포장 해제됨’, ‘재포장됨’과 같은 모든 이벤트가 기록되므로, 실제 발생한 포장 횟수에 따라 정확하게 비용을 청구할 수 있습니다.
이벤트 소싱에 대한 오해 해소
-
새로운 데이터베이스 학습 필요: Rails Event Store처럼 기존 RDB 위에서 작동하는 라이브러리가 존재합니다.
-
모든 것이 비동기여야 함: 동기식으로도 충분히 구현 가능하며, 점진적으로 비동기 방식을 도입할 수 있습니다.
-
느리다: 바운디드 컨텍스트(Bounded Context)를 잘 정의하고 엔티티의 라이프사이클이 적절하다면 성능 문제는 발생하지 않습니다.
-
결과적 일관성(Eventual Consistency) 필수: 동기식 구독을 통해 즉각적인 읽기 모델 업데이트가 가능하므로, 시작 단계에서는 필수가 아닙니다.
-
시스템 재시작 시 모든 상태 재구성: 읽기 모델(Read Model)을 Active Record 모델에 저장하여 영속화할 수 있습니다.
-
리팩토링이 어렵다: 기존 CRUD 방식과는 다르지만, 충분히 가능한 기술과 방법론이 존재합니다.
-
고확장성 시스템 전용: 비즈니스 의사결정의 ‘이유’를 파악하는 데 중요하므로, 모든 규모의 시스템에 유용합니다.
-
DDD(Domain-Driven Design)에만 국한: DDD 원칙은 Rails 프로젝트에도 적용될 수 있으며, 이벤트 소싱과 잘 통합됩니다.