이벤트 소싱 (Event Sourcing) 상태 재생 가능성 보장
핵심 인사이트 (3줄 요약)
- 본질: 이벤트 소싱(Event Sourcing)은 데이터의 최종 상태(Current State)를 데이터베이스에 덮어쓰는(Update/Delete) 대신, 상태를 변경시키는 모든 비즈니스 이벤트(Event)들을 발생 순서대로 덧붙여(Append-only) 저장하는 아키텍처 패턴이다.
- 가치: 과거의 모든 변경 이력이 불변(Immutable) 상태로 보존되므로 100% 완벽한 감사 추적(Audit Trail)이 가능하며, 이벤트를 처음부터 다시 재생(Replay)함으로써 언제든 특정 시점의 상태를 완벽하게 복원할 수 있는 상태 재생 가능성(State Reproducibility)을 보장한다.
- 융합: 이벤트 소싱은 필연적으로 읽기 속도가 느려지므로, 이를 해결하기 위해 상태를 비동기로 투영(Projection)하는 CQRS (명령 조회 책임 분리) 패턴과 반드시 결합하여 사용되며, MSA 환경에서 서비스 간 데이터 동기화의 확실한 원천(Source of Truth) 역할을 한다.
Ⅰ. 개요 및 필요성 (Context & Necessity)
-
개념: 전통적인 CRUD (Create, Read, Update, Delete) 모델에서는 데이터베이스의 레코드가 덮어씌워지므로 "현재 상태"만 남고 과거는 사라진다. 반면, 이벤트 소싱은 "잔고가 10,000원이다"라고 저장하는 대신, "15,000원 입금됨", "5,000원 출금됨" 이라는 이벤트 로그 자체를 저장한다. 현재 상태는 이 로그들을 순차적으로 계산(Fold/Reduce)하여 도출해낸다.
-
필요성: 은행 계좌나 쇼핑몰 장바구니처럼 "왜 이 상태가 되었는가?"에 대한 이력이 비즈니스적으로 매우 중요한 도메인에서는 단순 CRUD가 치명적인 한계를 갖는다. 별도의 이력 테이블(History Table)을 유지하려 해도 본진 테이블과 이력 테이블 간의 트랜잭션 불일치가 발생할 위험이 크다. 모든 변경을 '이벤트'라는 단일 진실의 원천(Source of Truth)으로 통합하면, 동시성 문제와 데이터 유실을 원천적으로 차단할 수 있다.
-
💡 비유: 체스 게임을 할 때 '현재 체스판에 놓인 말들의 위치'만 사진으로 찍어두는 것이 CRUD 방식이라면, '백이 폰을 E4로 옮겼다', '흑이 나이트를 C6로 옮겼다'는 '기보(棋譜)'를 순서대로 모두 적어두는 것이 이벤트 소싱이다. 기보만 있으면 언제든 빈 체스판에서부터 지금의 상태를 똑같이 재현할 수 있다.
-
등장 배경 및 발전 과정:
- 회계 장부 모델 (Ledger): 이벤트 소싱의 철학은 수백 년 전부터 쓰인 복식부기 회계 장부와 동일하다. 회계 장부는 한 번 적힌 기록을 지우개로 지우지 않고(No Delete), 취소할 때는 차감 내역을 새로 적어 넣는다(Append-only).
- 분산 시스템과 MSA의 등장: 마이크로서비스 간에 데이터를 주고받을 때 상태값만 주고받으면 동기화 문제가 생긴다. "어떤 사건이 발생했는지(Domain Event)"를 전달하는 것이 훨씬 결합도를 낮춘다는 사실이 증명되면서 이벤트 주도 아키텍처(EDA)가 부상했다.
- Event Store의 발전: Kafka, EventStoreDB 등 Append-only에 최적화된 고성능 스토리지 기술이 성숙해지며 이벤트 소싱을 엔터프라이즈 레벨에서 구현할 수 있는 토대가 마련되었다.
-
📢 섹션 요약 비유: 일기장에 "오늘은 슬프다"라고만 지우고 덧쓰는 것이 아니라, "아침에 비가 옴", "우산을 잃어버림", "넘어짐"이라는 사건들을 시간순으로 적어두면, 나중에 왜 슬퍼졌는지 완벽하게 추적하고 과거를 돌아볼 수 있는 것과 같습니다.
Ⅱ. 아키텍처 및 핵심 원리 (Deep Dive)
구성 요소
| 요소명 | 역할 | 내부 동작 | 관련 기술 | 비유 |
|---|---|---|---|---|
| 이벤트 스토어 (Event Store) | 모든 도메인 이벤트를 시간순으로 저장하는 Append-only DB | 삽입(Insert)과 조회(Read)만 지원, 수정/삭제 불가 | Kafka, EventStoreDB, DynamoDB | 지워지지 않는 마법의 일기장 |
| 도메인 이벤트 (Domain Event) | 과거에 발생한 사실을 표현하는 불변 객체 | 과거형 명명 규칙 사용 (예: OrderCreated) | JSON, Protobuf 포맷 | 사진으로 찍힌 과거의 순간 |
| 커맨드 (Command) | 상태를 변경하려는 의도나 요청 | 유효성 검사 후 통과 시 이벤트로 변환됨 | API 요청 객체 (예: CreateOrder) | "이렇게 해줘!"라는 명령 |
| 스냅샷 (Snapshot) | 긴 이벤트 로그를 전부 계산하는 비용을 줄이기 위한 중간 저장 상태 | 주기적으로 특정 시점의 상태를 캐싱 | Redis, RDBMS | 비디오 중간중간 찍어둔 썸네일 |
이벤트 소싱의 동작 흐름 (상태 재생)
전통적인 DB 업데이트 방식과 이벤트 소싱 방식의 차이를 비교하면, 이벤트 소싱이 어떻게 상태를 '재생'하는지 명확해진다.
┌───────────────────────────────────────────────────────────────┐
│ 전통적 CRUD 방식 vs 이벤트 소싱 (Event Sourcing) 방식 │
├───────────────────────────────────────────────────────────────┤
│ │
│ [전통적 CRUD: 은행 계좌] │
│ 1. 계좌 생성: Account Table [ Balance: 0 ] │
│ 2. 1만원 입금: Account Table [ Balance: 10,000 ] (덮어쓰기) │
│ 3. 3천원 출금: Account Table [ Balance: 7,000 ] (덮어쓰기) │
│ ▶ 결과: 현재 7,000원인 건 알지만, 과거 이력은 소실됨! │
│ │
│ [이벤트 소싱 (Event Sourcing)] │
│ 이벤트 스토어 (Append-Only Log) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Seq | Aggregate ID | Event Type | Payload │ │
│ ├──────────────────────────────────────────────────┤ │
│ │ 1 | ACC_123 | AccountCreated | Bal: 0 │ │
│ │ 2 | ACC_123 | MoneyDeposited | Amt: 10000│ │
│ │ 3 | ACC_123 | MoneyWithdrawn | Amt: 3000 │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ [상태 재생 (Replay / Fold / Reduce)] │
│ 상태 = 0 (초기) + 10,000 (입금) - 3,000 (출금) = 7,000 │
│ ▶ 결과: 현재 7,000원 도출. 원한다면 Seq 2번 시점(10,000원) │
│ 으로 완벽한 시간 여행(Time Travel) 가능! │
└───────────────────────────────────────────────────────────────┘
[다이어그램 해설] 전통적 CRUD는 UPDATE 연산을 수행하여 과거 데이터를 파괴한다. 반면 이벤트 소싱은 UPDATE 연산이 아예 존재하지 않는다. 오직 INSERT(Append)만 있다. 계좌(Aggregate ID: ACC_123)에 대한 조회가 들어오면, 시스템은 이벤트 스토어에서 해당 ID의 모든 이벤트를 가져와 빈 객체에 순차적으로 적용(Replay)하여 최종 잔고 7,000원을 도출해낸다. 이 과정은 함수형 프로그래밍의 Reduce나 Fold 연산과 정확히 일치한다. 만약 특정 시점의 데이터가 필요하다면 그 시점까지만 이벤트를 재생하면 되므로, 시점 복구(Time-machine) 기능이 무료로 얻어진다.
스냅샷 (Snapshot) 최적화 메커니즘
이벤트가 1만 개 쌓인 계좌를 조회할 때마다 1만 번의 연산을 다시 하는 것은 성능(레이턴시) 상 불가능하다. 이를 극복하기 위해 스냅샷 기법을 도입한다.
┌───────────────────────────────────────────────────────────────┐
│ 스냅샷(Snapshot)을 통한 조회 성능 최적화 │
├───────────────────────────────────────────────────────────────┤
│ │
│ Events: E1 ─▶ E2 ─▶ ... ─▶ E100 ─▶ E101 ─▶ E102 │
│ │ │
│ Snapshot: ▼ │
│ [Snapshot at Seq 100: Balance=50,000] │
│ │
│ [최적화된 Replay 과정] │
│ 1. 가장 최근의 스냅샷을 불러옴 (Balance = 50,000) │
│ 2. 스냅샷 이후의 이벤트(E101, E102)만 불러와서 적용 │
│ 3. 50,000 + E101(입금) + E102(출금) = 최종 상태 즉시 도출! │
└───────────────────────────────────────────────────────────────┘
- 📢 섹션 요약 비유: 게임을 할 때 매번 처음부터 10시간 치 플레이를 다시 하는 게 아니라, 중간중간 '세이브(스냅샷)'를 해두고 게임을 켤 때 가장 최근 세이브 파일부터 이어서 플레이하는 것과 같습니다.
Ⅲ. 실무 적용 및 기술사적 판단
실무 시나리오
-
시나리오 — 레거시 버그에 의한 데이터 오염 복구: 한 달 전 배포한 비즈니스 로직에 버그가 있어, 특정 조건의 고객들 포인트가 10%씩 적게 적립된 것을 뒤늦게 발견한 상황. CRUD DB라면 이미 잘못 적립된 채 덮어씌워진 포인트를 역추적할 길이 없어 수작업 복구가 거의 불가능하다.
- 판단: 이벤트 소싱 아키텍처라면 진실의 원천인 이벤트 자체(예:
OrderCompleted)는 잘못된 것이 아니라 순수하게 보존되어 있다. 단지 이벤트를 해석해 포인트를 누적하는 '프로젝션 로직'에 버그가 있었을 뿐이다. - 해결책: 버그가 수정된 새로운 로직(버전)으로 배포한 뒤, 이벤트 스토어의 모든 이벤트를 처음부터 다시 재생(Replay)시켜 새로운 읽기 모델(Read Model) DB를 처음부터 싹 다시 빌드한다. 이를 통해 오염된 과거 데이터를 100% 완벽하고 안전하게 소급 수정(Retroactive Fix)할 수 있다.
- 판단: 이벤트 소싱 아키텍처라면 진실의 원천인 이벤트 자체(예:
-
시나리오 — 이벤트 스키마(Schema) 버전 변경: 초창기
UserCreated이벤트에는 이름과 이메일만 있었으나, 1년 뒤 정책이 바뀌어 '전화번호'가 필수 이벤트 필드로 추가되었다. 구버전 이벤트와 신버전 이벤트가 섞여 있어 역직렬화(Deserialization) 및 재생 시 에러가 발생하는 상황.- 판단: 이벤트는 불변이므로 과거 이벤트를 수정해서는 안 된다 (수정하면 이벤트 소싱의 철학이 깨진다).
- 해결책: 이벤트 버전 관리(Upcasting) 전략을 도입한다. 과거 버전(v1) 이벤트를 메모리로 읽어 들일 때, 애플리케이션 단의
Upcaster라는 어댑터를 거쳐 임의의 기본값(전화번호=미상)을 채워 넣은 v2 객체로 변환하여 로직에 전달하도록 구현해야 한다.
도입 체크리스트
- 비즈니스적: 해당 도메인이 결제, 장바구니, 창고 재고 이동처럼 **과거의 이력 자체가 비즈니스의 핵심 자산(Audit Trail)**인가? (단순한 사용자 프로필 변경 같은 도메인에 이벤트 소싱을 적용하는 것은 오버엔지니어링이다).
- 아키텍처적: 이벤트 소싱은 쓰기(Append)와 조회(Read)의 모델이 완전히 분리되므로, 읽기 전용 DB에 이벤트를 투영(Projection)하기 위한 CQRS 패턴이 반드시 함께 구축되어 있는가?
안티패턴
-
모든 도메인에 이벤트 소싱 남용: 시스템의 모든 마이크로서비스에 이벤트 소싱을 적용하는 안티패턴. 이벤트 소싱은 학습 곡선이 매우 높고 스키마 변경, 최종 일관성 등의 복잡성을 수반하므로 핵심 하위 도메인(Core Subdomain)에만 국한해서 제한적으로 사용해야 한다.
-
📢 섹션 요약 비유: 작은 구멍가게 영수증 관리에 대기업 회계 장부 시스템(이벤트 소싱)을 억지로 도입하면, 물건 하나 팔 때마다 서류 작업이 너무 많아져서 정작 장사를 못 하게 되는 것과 같습니다. 적재적소에만 써야 합니다.
Ⅳ. 기대효과 및 결론
정량/정성 기대효과
| 구분 | 전통적 CRUD | 이벤트 소싱 (Event Sourcing) | 개선 효과 |
|---|---|---|---|
| 정량 | UPDATE 처리 시 DB 레코드 락(Lock) 발생 | 락 없는 Append-only 동작 | 쓰기 처리량(Write Throughput) 수십 배 향상 |
| 정량 | 버그 발생 시 데이터 복구 스크립트 작성에 수 일 | 이벤트 재생(Replay) 버튼 클릭 | 데이터 정합성 복구 시간 수 초~수 분 내 완수 |
| 정성 | 상태가 왜 이렇게 변했는지 추론 불가능 | 완벽한 불변 감사 로그 확보 | 규제 컴플라이언스(금융, 의료) 요구사항 완벽 충족 |
이벤트 소싱은 데이터베이스에 대한 철학을 근본적으로 바꾼다. "데이터를 덮어쓰지 마라. 잃어버리는 정보가 더 많다." 기술사는 시스템을 설계할 때 모든 데이터가 가치를 가지는 도메인을 식별하고, 이곳에 이벤트 소싱을 적용하여 동시성 병목을 해소하며, 동시에 CQRS를 결합해 읽기 성능까지 보장하는 성숙한 아키텍처 설계를 제시해야 한다.
📌 관련 개념 맵 (Knowledge Graph)
| 개념 명칭 | 관계 및 시너지 설명 |
|---|---|
| CQRS (명령 조회 책임 분리) | 이벤트 소싱은 현재 상태 조회가 매우 비효율적이므로, 조회 전용 DB를 분리하여 동기화하는 CQRS가 짝꿍처럼 필수적으로 결합된다. |
| 도메인 주도 설계 (DDD) | 애그리게이트(Aggregate) 상태 변경 시 발생하는 도메인 이벤트(Domain Event) 개념이 이벤트 소싱의 저장 단위가 된다. |
| 사가 패턴 (Saga Pattern) | MSA의 분산 트랜잭션 관리 패턴으로, 각 서비스가 발생시키는 이벤트 로그(Event Store)를 기반으로 실패 시 보상 트랜잭션을 트리거한다. |
| 이벤트 주도 아키텍처 (EDA) | 이벤트 소싱의 결과를 큐(Kafka)를 통해 타 서비스에 퍼블리싱함으로써 비동기 결합도 해소 아키텍처를 완성한다. |
| 최종 일관성 (Eventual Consistency) | 이벤트를 저장한 후 읽기 DB(Read Model)에 반영되기까지 미세한 시간 차이가 발생하므로, 시스템은 항상 최종 일관성을 감수해야 한다. |
👶 어린이를 위한 3줄 비유 설명
- 그림을 그릴 때 도화지에 연필로 그리고 계속 지우개로 지워서 다시 그리면 (CRUD), 나중에 처음에 뭘 그렸는지 전혀 알 수가 없어요.
- 하지만 투명한 비닐 필름을 겹겹이 올려놓고 한 장 한 장마다 그리는 과정을 따로따로 그려두면 (이벤트 소싱), 언제든지 필름을 빼보면서 옛날 모습을 볼 수 있답니다.
- 이렇게 변경되는 과정 하나하나를 지우지 않고 차곡차곡 쌓아서 저장하는 방법을 '이벤트 소싱'이라고 해요. 과거로 시간 여행을 갈 수 있는 마법의 데이터 저장법이랍니다!