기존의 소프트웨어 도메인 모델링은 엔티티 간의 관계를 그래프 형태로 표현하는 구조 중심적 접근 방식을 따릅니다. 이는 시스템의 구조를 파악하는 데는 유용하지만, ‘커피 한 잔을 주문하는 과정’과 같은 비즈니스 워크플로우나 행동을 명시적으로 모델링하는 데는 한계가 있습니다. 이러한 워크플로우는 웹 컨트롤러, 백그라운드 작업 등 다양한 실행 컨텍스트에 암묵적으로 결합되어 이해하고 관리하기 어렵습니다.
이벤트 소싱은 이러한 한계를 극복하기 위해 시간의 흐름과 행동에 초점을 맞춥니다. 시스템의 상태 변경 의도인 커맨드(Command)가 유효성 검사를 통과하면, 시스템의 변경된 사실을 나타내는 이벤트(Event)로 기록됩니다. 현재 시스템의 상태는 과거에 발생한 모든 이벤트를 순차적으로 적용하여 재구성됩니다. 이는 은행 계좌의 잔액이 모든 입출금 내역(이벤트)을 합산하여 결정되는 방식과 유사합니다. Ruby에서는 Struct를 이벤트 타입으로, 이벤트 인스턴스 배열을 히스토리로 사용하여 reduce 메서드로 현재 상태를 재구성하는 방식으로 구현할 수 있습니다.
이벤트 소싱은 상태 업데이트에 최적화되어 있지만, 복잡한 쿼리나 읽기 작업에는 비효율적입니다. 이 문제를 해결하기 위해 CQRS(Command Query Responsibility Segregation) 패턴이 도입됩니다. 이는 시스템을 쓰기(Command) 모델과 읽기(Query) 모델로 분리하는 아키텍처 패턴입니다. 쓰기 모델은 이벤트 소싱 기반으로 상태를 업데이트하고, 읽기 모델은 UI나 클라이언트에 최적화된 검색 인덱스, 캐시, 구체화된 뷰(Materialized View) 등으로 구성됩니다. 이벤트가 발생하면 읽기 모델은 해당 이벤트를 수신하여 필요한 뷰를 업데이트하며, 읽기 모델의 뷰는 언제든 이벤트 히스토리로부터 재구축될 수 있어 유연성을 제공합니다. 프로젝션(Projection)은 이러한 읽기 모델의 뷰를 생성하는 코드로, 특정 이벤트에 반응하여 데이터를 업데이트하고 저장하는 역할을 수행합니다.
워크플로우 모델링과 동시성 처리는 액터(Actor) 모델을 통해 구현됩니다. 동일한 스트림(예: 주문 ID)에 속하는 커맨드와 이벤트는 순차적으로 처리됨이 보장되지만, 서로 다른 스트림 간에는 동시성이 허용됩니다. 예를 들어, 주문 결제와 상품 준비 과정이 동시에 진행될 수 있습니다. 리액션 핸들러(Reaction Handler)는 이벤트 발생 후 부수 효과(Side Effect)를 처리하거나 다른 액터에게 커맨드를 전송하여 워크플로우를 연결하고 동시성을 관리합니다. 이는 외부 시스템과의 연동(예: 결제 API 호출)이나 워크플로우의 다음 단계를 트리거하는 데 사용됩니다. 모든 커맨드와 이벤트는 시스템 내에서 인과 관계(Causation)와 상관 관계(Correlation)를 추적하여 복잡한 동시성 워크플로우의 감사 추적 및 디버깅을 용이하게 합니다.
나아가, 시스템은 특정 조건이 충족될 때 자동으로 다음 커맨드를 실행하여 수동 작업을 자동화할 수 있습니다. 이는 프로젝션이 특정 상태 변화를 감지하여 리액션 핸들러를 통해 커맨드를 스케줄링하는 방식으로 구현됩니다. 실시간 UI는 Server-Sent Events(SSE) 연결을 통해 백엔드에서 발생한 이벤트에 반응하여 서버 렌더링된 HTML 조각을 브라우저로 전송하고, 브라우저가 이를 DOM에 병합하여 페이지 새로고침 없이 동적인 사용자 경험을 제공합니다.