역량 UP!/Architecture

이벤트 소싱(Event Sourcing)이란?

태하팍 2025. 11. 18. 17:54
반응형

상태 자체를 저장하지 말고, 상태가 만들어진 사건(Event)의 흐름을 저장하는 방식

DB 테이블에 현재값(Current State) 만 저장하는 게 아니라,
그 값이 만들어지기까지의 이벤트(명령의 결과)를 계속 쌓아두는 구조

이벤트1: UserCreated(id=1)
이벤트2: PointAdded(+100)
이벤트3: PointAdded(+50)
이벤트4: PointUsed(-30)
이벤트5: PointAdded(+20)

위의 이벤트들을 순서대로 재생(Replay)하면 현재상태(point=140)을 계산할 수 있음.

 Command  →  Event Store(이벤트 저장) → Read Model(Mongo/Elasticsearch) 업데이트
                                         ↑
                                   Replay 가능

  • Command = “포인트 적립하라”, “콘텐츠 구매해라”
  • Event = “100포인트 적립됨”, “콘텐츠 구매 성공”
  • Event Store = Kafka / Event Log / Append Only Storage
  • Read Model = 조회 최적화 DB (Mongo, ES, Redis 등)

상태를 이벤트 자체로 저장한다?

현재 상태를 저장하는 대신 상태가 만들어진 모든 이벤트를 기록한다는 의미

일반적인 CRUD 방식 : 마지막 상태만 남고, 중간에 무슨일이 있었는지 사라짐

order_status = "결제완료"
order_status = "제조중"
order_status = "배달중"
order_status = "완료"

이벤트 소싱 방식 : 모든 이벤트가 남아서 재생하면 현재 주문상태를 알수 있음
 - 로그분석 가능
 - 이슈 추적 가능

이벤트소싱 + CQRS 관계

CQRS = Command / Query Read Model 분리
Event Sourcing = Command 후 결과를 이벤트로 저장하고 Read Model을 갱신

CQRS는 구조적 패턴 / Event Sourcing은 상태 저장 방식 패턴

2025.11.14 - [역량 UP!/Architecture] - CQRS(=Command Query Responsibility Segregation)를 알아보자!

장점

  • 상태변화 히스토리 100% 추적가능
  • 장애복구상황에서 복구 쉬움(Replay)
  • 분산 환경에서 강함(=kafka같은 이벤트 스트림에도 자연스럽게 매핑 됨.)
    읽기 모델 최적화 기능(=Elasticsearch, Mongo, Redis 등을 용도에 따라 자유롭게 선택 가능)→ 읽기 부하가 폭증해도 Command(쓰기)에 영향 거의 없음.

단점

  • 시스템 설계 난이도 매우 높음
  • 이벤트 스키마 버전 관리 필요
    • 오래된 이벤트와 신규 이벤트가 호환돼야 함
  • 즉시 정합성이 아닌 Eventual Consistency
    • Write → Event → Consumer 처리 → Read Model 업데이트
      • 조회 모델은 약간 늦게 반영됨.

상태를 바로 저장이 아닌 이벤트들을 저장하는건 알겠어! 언제 쓰는거지??

 

이벤트소싱은 언제, 왜 쓰는가? 

정확한 변경 히스토리를 100% 보존해야 할 때 사용

Deposit +10,000  
Withdraw -3,000  
Deposit +5,000

데이터의 “원인과 과정”이 100% 남음
회계, 금융, 보험, 주문 시스템처럼 감사 추적(Audit) 중요 서비스에 필수

히스토리 테이블로도 되지 않나?

History는 상태만 저장 c “과정”이 없다

stock = 10 → 4 → 9 → 100 → 99

 

- 어떤 API가 호출되었는지 주문인지 반품인지 관리자가 직접 수정했는지 알 수 없음.
   하지만 이벤트소싱은? 비즈니스 행위의 의미가 살아있음.

OrderCreated(6)
OrderCanceled(5)
AdminAdjusted(+91)
CustomerReturned(-1)

 

- History는 정확한 재생불가(replay)
- 대규모 트래픽 시스템에서 확장성이 떨어짐
    히스토리는 RDB에 추가,  Insert 증가→Lock/IO 문제 발생
    Event Sourcing은  카프카 기반으로 수평확장 가능)

예제) 아래와 같은 상태의 변화를 이벤트 객체로 kafka에 넣는 흐름을 살펴봅시다.

행동 이벤트
계약 등록 ContractCreated
계약 금액 변경 ContractAmountChanged
계약 취소 ContractCancelled

1) command(쓰기) 호출들어오면
POST /contract/change-amount

{
  "contractId": 1001,
  "newAmount": 500000
}

2) command Handler
도메인 로직실행
RDB에 상태 저장( contracts table update)
logic 타고 상태가 변경했다는 사실을 이벤트로 만듬!
ContractAmountChangedEvent 객체 생성
Event 객체 생성

public class ContractAmountChangedEvent {
    public Long contractId;
    public Long beforeAmount;
    public Long newAmount;
    public LocalDateTime changedAt;
}

3) Event를 kafka에 publish(Producer)
producer.send(

    new ProducerRecord<>(
        "contract-events",
        contractId,
        eventJson  // String 또는 JSON
    )
);

4) kafka는 이벤트를 로그처럼 저장
offset 0 : ContractCreated(1001)
offset 1 : ContractAmountChanged(1001, 300000 → 500000)
offset 2 : ContractAmountChanged(1001, 500000 → 550000)
offset 3 : ContractCancelled(1001)

5) Consumer가 이벤트를  구독 후 읽어와서 Read Model에 상태를 저장
MongoDB, Elasticsearch, Redis같은 조회 전용 DB에 현재상태를 반영 합니다.
ex) MongoDB Upsert
public void onEvent(ContractAmountChangedEvent e) {
    mongo.update(
      {contractId: e.contractId},
      {$set: {amount: e.newAmount}}
    );
}

참고 : https://yoonseon.tistory.com/173

 

 

 

반응형