← Posts

Event-Driven · Event Sourcing · Audit

상태 대신 이벤트를 진실로 - 이벤트 소싱

보통 DB엔 지금 상태만 남고 어쩌다 그렇게 됐는지는 덮어써져 사라진다. 상태 대신 일어난 일의 목록을 저장하고 필요하면 재생해 상태를 얻는 이벤트 소싱, 감사와 되감기가 공짜로 딸려오는 대신 치르는 값.

목차
  1. 지금 상태만 남고 과정은 사라진다
  2. 은행 장부는 원래 그렇게 안 한다
  3. 이벤트 소싱: 일어난 일을 진실로 저장한다
  4. 현재 상태는 재생해서 얻는다
  5. 매번 재생하면 느리다: 스냅샷
  6. 공짜로 딸려오는 것
  7. 대가: 이벤트는 영원하다
  8. CQRS와 왜 짝으로 오나
  9. 언제 쓰나
  10. 실무에서: 작게 시작한다
  11. 정리

보통의 DB는 “지금 어떻다”만 남긴다. “어쩌다 그렇게 됐는지”는 덮어써져 사라진다. 그 과정을 통째로 진실로 삼는 방식이 이벤트 소싱이다. 발상을 뒤집으면 감사와 되감기가 공짜로 딸려온다.

지금 상태만 남고 과정은 사라진다

계좌 잔액을 생각해 보자. 보통은 balance 칸 하나에 현재 값을 둔다.

plaintext
입금 3만원  →  balance = 30000   (덮어씀)
출금 1만원  →  balance = 20000   (덮어씀)
입금 5천원  →  balance = 25000   (덮어씀)

지금 잔액이 25000인 건 안다. 그런데 어떤 입출금을 거쳐 왔는지는? 매번 덮어썼으니 사라졌다. “지난달에 왜 5천원이 빠졌지”를 물으면 답할 수 없다. 현재 상태는 과거를 지운 결과다.

은행 장부는 원래 그렇게 안 한다

정작 실제 은행 장부는 이렇게 안 한다. 거래를 지우지 않고 계속 덧붙인다.

plaintext
+30000  입금
-10000  출금
+5000   입금
─────────────
= 25000  (거래를 다 더한 값)

잔액은 따로 저장하는 값이 아니라, 거래를 전부 더한 결과다. 거래 목록이 진실이고, 잔액은 거기서 계산돼 나온다. 이 오래된 회계 방식이 이벤트 소싱의 핵심 그대로다.

이벤트 소싱: 일어난 일을 진실로 저장한다

이벤트 소싱은 현재 상태 대신, 일어난 일(이벤트)의 목록을 진실로 저장한다. 그리고 이벤트는 덧붙이기만 하고 절대 덮어쓰거나 지우지 않는다.

diagramdiagram

“입금됨”, “출금됨”, “주소 변경됨”처럼 과거형으로 일어난 사실을 순서대로 쌓는다. 이 로그가 유일한 진실이다.

현재 상태는 재생해서 얻는다

그럼 “지금 잔액”이 필요하면? 처음부터 이벤트를 순서대로 다시 적용해 계산한다. 이걸 재생(replay) 이라 한다 - 빈 상태에서 시작해 이벤트를 하나씩 접어 나가면 현재 상태가 나온다.

plaintext
잔액 0
 → 입금 +30000 적용 → 30000
 → 출금 -10000 적용 → 20000
 → 입금 +5000  적용 → 25000
현재 잔액 = 25000

상태는 저장하는 게 아니라 파생하는 것이 된다. 이 발상이 뒤의 모든 장점과 대가를 만든다.

매번 재생하면 느리다: 스냅샷

이벤트가 수백만 개면 매번 처음부터 다 더하는 건 느리다. 그래서 스냅샷(snapshot) 을 쓴다 - “10000번째 이벤트까지 적용한 상태는 이거였다”를 중간중간 저장해 둔다.

현재 상태가 필요하면 가장 최근 스냅샷에서 시작해 그 뒤 이벤트만 마저 적용한다. 스냅샷은 어디까지나 성능을 위한 캐시일 뿐, 진실은 여전히 이벤트 로그다 - 스냅샷이 다 날아가도 이벤트를 재생하면 똑같이 복구된다.

공짜로 딸려오는 것

과정을 통째로 남기니, 보통은 따로 만들어야 할 것들이 저절로 생긴다.

  • 완벽한 감사 로그 - 누가 언제 무엇을 했는지가 곧 이벤트 로그 자체다. “감사 기능”을 따로 안 만들어도 이미 다 있다.
  • 시간여행 - “지난주 금요일의 상태”가 궁금하면, 그 시점까지의 이벤트만 재생하면 된다. 과거 어느 순간이든 정확히 재현된다.
  • 디버깅 - 버그로 상태가 이상해졌으면, 이벤트를 하나씩 따라가며 어느 이벤트에서 틀어졌는지 짚을 수 있다. 덮어쓰기 DB에선 사라졌을 단서다.

대가: 이벤트는 영원하다

공짜는 없다. 이벤트 소싱의 가장 큰 대가는 한번 쌓은 이벤트는 영원히 남고, 그 모양을 함부로 못 바꾼다는 것이다.

  • 스키마 진화가 어렵다 - 3년 전 “주문됨” 이벤트의 모양이 지금과 다르면, 재생 코드는 옛 모양도 여전히 읽을 줄 알아야 한다. 옛 이벤트를 지우거나 고칠 수 없으니, 버전을 나눠 다 감당한다.
  • 생각의 전환이 크다 - “상태를 고친다”가 아니라 “일어난 일을 기록하고 상태를 파생한다”로 머리를 바꿔야 한다. 팀 전체가 이 모델에 익숙해야 한다.
  • 삭제가 까다롭다 - “덧붙이기만”이 원칙인데 개인정보 삭제 요구처럼 정말 지워야 할 때가 있다. 이 예외를 어떻게 다룰지 따로 설계해야 한다.

이 대가가 커서, 이벤트 소싱은 아무 데나 쓰는 게 아니다.

CQRS와 왜 짝으로 오나

이벤트 로그는 “잔액이 얼마인 계좌 목록”같은 조회에 형편없다. 매번 재생해야 하니까. 그래서 이벤트 소싱은 거의 항상 CQRS와 함께 온다.

  • 쓰기 - 이벤트를 로그에 덧붙인다(이벤트 소싱).
  • 읽기 - 그 이벤트를 흘려보내 조회용 읽기 모델을 미리 만들어 둔다(CQRS).

CQRS 글에서 “둘은 별개”라 했다 - CQRS는 이벤트 소싱 없이도 되지만, 이벤트 소싱은 조회를 위해 CQRS가 거의 필수다. 방향이 한쪽으로만 필요한 관계다.

언제 쓰나

  • 이력 자체가 값일 때 - 회계·거래·의료 기록처럼 “어쩌다 이렇게 됐는지”가 결과만큼 중요한 도메인. 감사가 법으로 요구되는 곳.
  • 되감기·재현이 필요할 때 - 과거 상태 복원, 문제 재현이 중요한 시스템.
  • 그 외 대부분은 안 쓴다 - 지금 상태만 중요하고 이력이 필요 없으면, 이벤트 소싱의 대가가 이득보다 크다. 평범한 CRUD엔 평범한 DB가 맞는다.

실무에서: 작게 시작한다

  • 전체가 아니라 일부 도메인만 - 시스템 전체를 이벤트 소싱으로 만들지 않는다. 이력이 진짜 중요한 핵심 도메인 하나(예: 주문·결제)에만 적용하고 나머지는 평범하게 둔다.
  • 이벤트는 과거형·불변으로 - “주문함”이 아니라 “주문됨”처럼 이미 일어난 사실로 적고, 한번 쓴 이벤트는 절대 고치지 않는다. 틀렸으면 정정 이벤트를 새로 덧붙인다.
  • 재생을 늘 테스트한다 - 재생 코드가 옛 이벤트를 못 읽으면 상태 복구가 깨진다. 옛 버전 이벤트로 재생이 되는지 테스트로 지킨다.

정리

  • 보통의 DB는 상태를 덮어써 과정을 지운다. 이벤트 소싱은 일어난 일의 목록을 진실로 저장하고 덧붙이기만 한다.
  • 현재 상태는 저장하는 게 아니라 이벤트를 재생해 파생한다 - 느리면 스냅샷으로 중간을 캐시한다.
  • 감사·시간여행·디버깅이 공짜로 딸려오지만, 이벤트가 영원히 남아 스키마 진화와 삭제가 어렵다.
  • 조회가 형편없어 CQRS와 거의 항상 짝이고, 이력이 값인 핵심 도메인에만 쓴다.

다음 글은 시리즈를 닫으며, 이 패턴들을 실무에서 언제 꺼내고 무엇을 조심하는지로 간다.