상태 자체를 저장하지 말고, 상태가 만들어진 사건(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 업데이트
- 조회 모델은 약간 늦게 반영됨.
- 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
'역량 UP! > Architecture' 카테고리의 다른 글
| CQRS(=Command Query Responsibility Segregation)를 알아보자! (0) | 2025.11.14 |
|---|---|
| Backoff Retry와 DLQ(Dead Letter Queue) 차이? 사용법? (2) | 2025.11.11 |
| 동시성(Concurrency) 모델&스케줄링 모델(feat. ForkJoinPool) (0) | 2025.11.04 |
| Blocking&non-Blocking 그리고 Sync&Async 이해하기! (0) | 2025.11.03 |
| 대용량 데이터 처리 시 고려사항 (0) | 2025.10.26 |