AI-generated

Event Sourcing Pattern

이벤트 소싱 패턴은 객체의 현재 상태만 저장하는 대신, 객체에 수행된 전체 행위 시퀀스를 추가 전용 저장소에 기록하는 접근 방식이다. 복잡한 시스템에서 성능, 확장성, 감사 가능성을 향상시킬 수 있다.

Event Sourcing Pattern

원문: Event Sourcing Pattern - Azure Architecture Center

1. Highlights / Summary

이벤트 소싱(Event Sourcing) 패턴은 객체의 현재 상태만 관계형 데이터베이스에 저장하는 대신, 객체에 수행된 전체 행위(action) 시퀀스를 추가 전용 저장소(append-only store)에 기록하는 접근 방식이다. 이 저장소가 시스템의 기록 원본(system of record)으로 작동하며, 도메인 객체를 구체화(materialize)하는 데 사용된다. 복잡한 시스템에서 성능, 확장성, 감사 가능성(auditability)을 향상시킬 수 있다.

문서는 이벤트 소싱이 아키텍처 전체에 스며드는(permeate) 복잡한 패턴이며, 한번 도입하면 모든 미래 설계 결정이 이벤트 소싱 시스템이라는 사실에 제약된다고 강하게 경고한다. 이벤트 소싱 시스템으로/으로부터 마이그레이션하는 비용이 높으며, 성능과 확장성이 최우선 요구사항인 시스템에 가장 적합하다. 대부분의 시스템에서는 이벤트 소싱이 추가하는 복잡성이 정당화되지 않는다.

이벤트 소싱은 CQRS(Command Query Responsibility Segregation) 패턴과 결합하여 사용되는 것이 일반적이며, 이벤트 저장소가 쓰기 모델의 영구적 정보 원천이 되고, 구체화된 뷰(materialized view)가 읽기 모델을 구성한다.


2. Detailed Summary

2.1 Context and Problem (맥락과 문제)

대부분의 애플리케이션은 데이터의 최신 상태를 관계형 데이터베이스에 저장하고, 필요에 따라 삽입하거나 업데이트하는 전통적인 CRUD 모델을 사용한다. 이 접근 방식은 대부분의 시나리오에서 간단하고 빠르지만, 고부하(high-load) 시스템에서 다음과 같은 도전 과제가 있다:

  • 성능(Performance): 시스템이 확장될수록 리소스 경합(contention)과 잠금(locking) 문제로 성능이 저하된다.
  • 확장성(Scalability): CRUD 시스템은 동기적이며 데이터 작업이 업데이트에서 차단된다. 부하 시 병목(bottleneck)과 높은 지연 시간(latency)을 유발한다.
  • 감사 가능성(Auditability): CRUD 시스템은 최신 상태만 저장한다. 별도의 감사 메커니즘이 없으면 이력이 유실된다.

2.2 Solution (해결책)

이벤트 소싱은 데이터에 대한 작업을 이벤트(event) 시퀀스로 처리하는 접근 방식을 정의한다. 각 이벤트는 추가 전용 저장소에 기록된다. 애플리케이션 코드는 객체에 수행된 행위를 명령적으로(imperatively) 기술하는 이벤트를 발생시킨다. 예: AddedItemToOrder, OrderCanceled.

이벤트는 이벤트 저장소(event store)에 영속화되며, 이것이 데이터의 현재 상태에 대한 권위 있는 데이터 원천(authoritative data source)으로 작동한다. 추가 이벤트 핸들러(event handler)가 관심 있는 이벤트를 수신하여 적절한 조치를 취한다.

이벤트를 읽고 재생(replay)하는 것은 상대적으로 비용이 크므로, 애플리케이션은 일반적으로 구체화된 뷰(materialized view) — 쿼리에 최적화된 이벤트 저장소의 읽기 전용 프로젝션(projection)을 구현한다.

이벤트 소싱 패턴 개요

2.3 Workflow (워크플로우)

1. 프레젠테이션 레이어 → 읽기 전용 저장소에서 데이터 읽기 → UI 표시
2. 프레젠테이션 레이어 → 커맨드 핸들러에 액션 요청 (카트 생성, 항목 추가 등)
3. 커맨드 핸들러 → 이벤트 저장소에서 엔티티의 과거 이벤트 조회
→ 이벤트를 재생하여 현재 상태 구체화
4. 비즈니스 로직 실행 → 이벤트 발생 → 큐/토픽에 전달
5. 이벤트 핸들러가 이벤트를 수신하여:
- 이벤트 저장소에 기록
- 쿼리 최적화된 읽기 전용 저장소 업데이트
- 외부 시스템과 통합

2.4 Pattern Advantages (패턴의 이점)

  • 불변성과 추가 전용 작업(Immutability & Append-only): 이벤트는 불변이며 추가 전용으로 저장된다. 트랜잭션 처리 중 경합이 없어 성능과 확장성이 크게 향상된다.

  • 단순한 이벤트 객체: 이벤트는 발생한 행위와 관련 데이터를 기술하는 단순 객체다. 데이터 저장소를 직접 업데이트하지 않아 구현과 관리가 단순해진다.

  • 도메인 전문가에게 의미 있는 이벤트: 이벤트는 도메인 전문가(domain expert)에게 의미가 있지만, 객체-관계 임피던스 불일치(object-relational impedance mismatch)로 인해 복잡한 데이터베이스 테이블은 이해하기 어렵다.

  • 동시 업데이트 충돌 방지: 데이터 저장소의 객체를 직접 업데이트할 필요가 없으므로 동시 갱신 충돌을 방지한다. 단, 도메인 모델은 불일치 상태를 유발할 수 있는 요청으로부터 자신을 보호하도록 설계되어야 한다.

  • 감사 추적(Audit Trail): 이벤트의 추가 전용 저장은 감사 추적을 제공한다. 언제든지 이벤트를 재생하여 현재 상태를 구체화된 뷰나 프로젝션으로 재생성할 수 있다. 보상 이벤트(compensating event)를 사용한 변경 취소 이력도 제공한다.

  • 이벤트 생산자와 소비자의 디커플링(Decoupling): 커맨드 핸들러가 이벤트를 발생시키고 태스크가 응답한다. 태스크는 이벤트 유형과 데이터를 알지만, 이벤트를 트리거한 작업은 모른다. 여러 태스크가 각 이벤트를 처리할 수 있어 다른 서비스 및 시스템과의 통합이 용이하다.

2.5 Issues and Considerations (문제와 고려사항)

  • 최종 일관성(Eventual Consistency): 구체화된 뷰를 생성하거나 이벤트를 재생하여 프로젝션을 생성할 때 시스템은 최종적으로만 일관적(eventually consistent)이다. 요청 처리 결과로 이벤트를 추가하는 시점, 이벤트가 발행되는 시점, 소비자가 처리하는 시점 사이에 지연이 있다.

  • 이벤트 버전 관리(Versioning Events): 이벤트 저장소는 영구적 정보 원천이므로 이벤트 데이터는 절대 업데이트되어서는 안 된다. 엔티티를 업데이트하거나 변경을 취소하는 유일한 방법은 보상 이벤트(compensating event)를 추가하는 것이다. 이벤트 스키마가 변경되어야 할 때의 전략:

    • 모든 버전의 이벤트를 지원하는 핸들러 구현
    • 특정 이벤트 버전을 처리하는 핸들러 구현
    • 과거 이벤트를 새 스키마로 업데이트 (불변성 위반)
  • 이벤트 순서(Event Ordering): 멀티스레드 애플리케이션과 다중 인스턴스에서 이벤트 순서의 일관성이 중요하다. 타임스탬프를 추가하거나 증분 식별자(incremental identifier)로 주석을 달아 문제를 방지한다.

  • 이벤트 쿼리(Querying Events): 이벤트에서 정보를 얻기 위한 표준 접근법이나 SQL 쿼리 같은 메커니즘이 없다. 이벤트 식별자를 기준으로 이벤트 스트림만 추출 가능하다. 엔티티의 현재 상태는 해당 엔티티의 모든 이벤트를 재생해야만 결정된다.

  • 상태 재생성 비용(Cost of Recreating State): 이벤트 스트림이 길어지면 관리와 업데이트에 영향을 미친다. 지정된 간격으로 스냅샷(snapshot)을 생성하여 성능을 개선한다. 스냅샷에서 현재 상태를 얻고 그 이후의 이벤트만 재생한다.

  • 충돌(Conflicts): 이벤트 소싱이 충돌 가능성을 최소화하지만, 최종 일관성과 트랜잭션 부재로 인한 불일치를 처리할 수 있어야 한다. 예: 주문이 진행 중일 때 재고 감소 이벤트가 도착하는 경우.

  • 멱등성 필요(Need for Idempotency): 이벤트 발행이 최소 한 번(at least once)일 수 있으므로, 이벤트 소비자는 멱등(idempotent)해야 한다. 동일 이벤트가 두 번 이상 처리되더라도 업데이트를 재적용하지 않아야 한다.

  • 순환 논리(Circular Logic): 하나의 이벤트 처리가 새로운 이벤트를 생성하는 시나리오에서 무한 루프가 발생할 수 있으므로 주의해야 한다.

2.6 When to Use This Pattern (사용 시점)

적합한 경우:

  • 데이터에 의도(intent), 목적(purpose), 이유(reason)를 포착하고자 할 때. 예: 고객 엔티티 변경을 Moved home, Closed account, Deceased 같은 특정 이벤트로 캡처
  • 데이터에 대한 충돌 업데이트를 최소화하거나 완전히 방지해야 할 때
  • 시스템 상태 복원, 변경 롤백, 이력과 감사 로그를 유지해야 할 때
  • 이벤트 사용이 애플리케이션 운영의 자연스러운 특성일 때
  • 데이터 입력/업데이트 프로세스와 이를 적용하는 태스크를 디커플링해야 할 때
  • 구체화된 모델과 엔티티 데이터의 형식 변경에 유연성이 필요할 때
  • CQRS와 함께 사용하고 최종 일관성이 허용될 때

부적합한 경우:

  • 초대규모(hyper-scale) 성능이 불필요한 애플리케이션
  • 작거나 단순한 도메인, 비즈니스 로직이 적은 시스템
  • 데이터 뷰의 일관성과 실시간 업데이트가 필요한 시스템
  • 기본 데이터에 대한 충돌 업데이트 빈도가 낮은 시스템 (주로 데이터를 추가하기만 하는 시스템)

2.7 Example - 컨퍼런스 좌석 예약 시스템

컨퍼런스 관리 시스템에서 완료된 예약 수를 추적하여 좌석 가용 여부를 확인하는 예시:

전통적 CRUD 방식: 예약 정보를 보관하는 DB에 총 예약 수를 별도 엔티티로 저장. 예약/취소 시 숫자를 증감. 이론적으로 단순하지만, 예약 마감 직전 대량의 동시 예약 시 확장성 문제 발생.

이벤트 소싱 방식: 예약과 취소를 이벤트 저장소에 이벤트로 저장. 이벤트를 재생하여 가용 좌석 수를 계산. 이벤트의 불변성 덕분에 더 확장 가능하며, 읽기와 추가만 필요.

컨퍼런스 좌석 예약 이벤트 소싱 예시

좌석 예약 시퀀스:

  1. UI가 두 참석자를 위한 좌석 예약 커맨드를 발행
  2. 커맨드 핸들러가 예약/취소 이벤트를 쿼리하여 SeatAvailability 엔티티 구성
  3. 스냅샷과 메모리 캐시를 활용한 최적화 고려
  4. 커맨드 핸들러가 도메인 모델의 메서드를 호출하여 예약 수행
  5. SeatAvailability 엔티티가 예약된 좌석 수를 포함한 이벤트를 발생
  6. 새 이벤트가 이벤트 저장소의 이벤트 목록에 추가

2.8 Azure Well-Architected Framework

Pillar이벤트 소싱 패턴의 지원
Reliability복잡한 비즈니스 프로세스의 변경 이력을 캡처하여 상태 저장소 복구 시 상태 재구성을 지원
Performance EfficiencyCQRS, 적절한 도메인 설계, 전략적 스냅샷과 결합하여 원자적 추가 전용 작업과 읽기/쓰기 DB 잠금 회피로 성능 향상

3. Conclusion and Personal View

  1. 이벤트 소싱은 “현재 상태” 대신 “발생한 사건의 전체 이력”을 저장하는 근본적으로 다른 데이터 관리 패러다임이다. 이 결정은 시스템 전체 아키텍처에 스며들기 때문에 신중하게 도입해야 한다.

  2. 문서가 강조하는 가장 중요한 경고: “대부분의 시스템에서 이벤트 소싱의 복잡성은 정당화되지 않는다.” 성능과 확장성이 최우선이 아니라면 전통적 CRUD가 더 적합하다.

  3. 이벤트 소싱의 핵심 가치는 감사 추적(audit trail)과 시간 여행(time travel) 능력이다. 규제가 엄격한 금융, 의료 도메인에서 모든 변경의 이유와 이력을 보존할 수 있는 이점은 압도적이다.

  4. 이벤트 버전 관리(event versioning)는 실무에서 가장 어려운 도전 과제 중 하나다. 이벤트는 불변이므로 스키마 변경 시 하위 호환성을 유지하는 전략이 필수적이며, 이는 지속적으로 유지보수 비용을 발생시킨다.

  5. 스냅샷(snapshot) 전략은 이벤트 스트림 길이에 따른 성능 저하를 완화하는 핵심 기법이다. 스냅샷 주기와 방식을 올바르게 설계하지 않으면 재생 비용이 급증한다.

  6. 멱등성(idempotency) 요구는 이벤트 소싱 시스템의 필수 요소다. “최소 한 번” 전달 보장에서 중복 이벤트 처리를 안전하게 처리해야 하며, 이는 모든 이벤트 소비자에게 적용되어야 한다.

  7. CQRS와의 결합은 자연스럽고 거의 표준적인 조합이다. 이벤트 저장소를 쓰기 모델로, 구체화된 뷰를 읽기 모델로 분리하면 각각을 독립적으로 최적화할 수 있다.

  8. 순환 논리(circular logic) 경고는 실무에서 자주 발생하는 함정이다. 이벤트 핸들러 체인에서 무한 루프를 방지하는 가드(guard) 메커니즘이 반드시 필요하다.

  9. 보상 이벤트(compensating event) 패턴은 이벤트 소싱에서 “취소”를 구현하는 유일한 방법이다. 이는 도메인 모델 설계에서 모든 작업의 역작업(inverse operation)을 정의해야 함을 의미한다.

  10. 컨퍼런스 좌석 예약 예시는 CRUD의 동시성 문제(잠금 경합)를 이벤트 소싱이 어떻게 우아하게 해결하는지 잘 보여준다. 추가 전용 작업은 본질적으로 잠금이 불필요하다.


  • CQRS Pattern - 읽기/쓰기 책임 분리
  • Materialized View Pattern - 효율적 쿼리를 위한 사전 계산된 뷰
  • Compensating Transaction Pattern - 이전 작업을 취소하는 보상 트랜잭션
  • Circuit Breaker Pattern - 원격 서비스 장애 처리

관련 글