"통장에 잔액이 100만 원 있습니다. 그런데 이 돈이 월급이 들어와서 생긴 건지, 적금을 깨서 생긴 건지, 누가 보낸 건지 알 수 있나요?"
전통적인 방식은 UPDATE 쿼리로 잔액을 바꿔버리기 때문에 **과거(Context)**가 사라집니다. 반면, 이벤트 소싱은 데이터를 절대 지우거나 수정하지 않고(Append Only), 모든 변경 사항을 '이벤트'로 기록하여 완벽한 감사(Audit) 로그를 남깁니다.
1. 방식 1: 상태 저장 방식 (State-Oriented / CRUD) - "전통적인 덮어쓰기"
우리가 흔히 사용하는 RDBMS 방식입니다. 현재 시점의 최신 데이터만 테이블에 유지합니다.
특징
- UPDATE 위주: 데이터가 변경되면 기존 값을 덮어씁니다.
- 스냅샷: DB에 있는 데이터는 항상 "지금 이 순간"의 모습(Snapshot)입니다.
코드 예시 (JPA)
Java
@Service
@Transactional
public class AccountService {
public void deposit(Long id, int amount) {
// 1. 현재 상태 조회
Account account = accountRepository.findById(id).orElseThrow();
// 2. 상태 변경 (기존 값 + 입금액)
account.setBalance(account.getBalance() + amount);
// 3. 덮어쓰기 (UPDATE account SET balance = ... WHERE id = ...)
// 과거 잔액이 얼마였는지는 영원히 사라짐
}
}
장단점
- 장점:
- 구현 용이: 직관적이고 데이터를 읽을 때 별도 계산이 필요 없습니다.
- 빠른 조회: 그냥 SELECT 하면 현재 값이 바로 나옵니다.
- 단점:
- 이력 소실: "어제 3시의 잔액 상태"를 조회할 수 없습니다. (별도의 History 테이블을 또 만들어야 함)
- 원인 파악 불가: 잔액이 왜 마이너스가 됐는지 버그를 추적하기 어렵습니다.
2. 방식 2: 이벤트 소싱 방식 (Event Sourcing) - "모든 역사의 기록"
데이터베이스에 현재 상태를 저장하지 않습니다. 대신 DepositEvent(100), WithdrawEvent(50) 같은 사건(Event)을 순서대로 저장합니다.
특징
- Append Only: UPDATE나 DELETE가 없습니다. 오직 INSERT만 존재합니다.
- 재생(Replay): 현재 잔액을 알고 싶으면, 태초부터 지금까지 발생한 모든 이벤트를 순서대로 실행(Play)해봅니다.
코드 예시
Java
// 1. 이벤트 객체
class DepositEvent { public int amount; }
class WithdrawEvent { public int amount; }
// 2. 이벤트 저장소 (Event Store)
public void saveEvent(Long aggregateId, Object event) {
eventStore.append(aggregateId, event); // DB에 이벤트 로그만 쌓음
}
// 3. 현재 상태 복원 (Replay)
public Account getAccount(Long id) {
Account account = new Account(); // 빈 껍데기(0원)
List<Object> events = eventStore.loadEvents(id); // 모든 내역 로딩
for (Object event : events) {
// 과거의 일을 순서대로 다시 적용
if (event instanceof DepositEvent) account.balance += ((DepositEvent) event).amount;
else if (event instanceof WithdrawEvent) account.balance -= ((WithdrawEvent) event).amount;
}
return account; // 계산된 현재 상태 반환
}
성능 최적화 (Snapshot)
"이벤트가 100만 개면 매번 계산하다가 날 새지 않나요?"
그래서 일정 주기(예: 이벤트 100개마다)로 중간 계산 결과인 **스냅샷(Snapshot)**을 저장해두고, 그 이후의 이벤트만 로딩해서 계산합니다.
장단점
- 장점:
- 타임머신: "2023년 1월 1일 시점의 상태"로 데이터를 완벽하게 되돌리거나 조회할 수 있습니다.
- 완벽한 감사 로그: 누가 언제 무엇을 했는지 100% 추적 가능합니다. (금융권 필수)
- 이벤트 기반 아키텍처: 저장된 이벤트를 Kafka로 발행하면 다른 마이크로서비스들이 쉽게 구독할 수 있습니다.
- 단점:
- 조회 성능: 단순 조회(SELECT)가 매우 어렵고 느립니다. (그래서 CQRS 패턴과 짝꿍으로 쓰여야만 합니다.)
- 난이도 최상: 스키마가 변경되거나 이벤트 버전이 바뀌면 마이그레이션(Upcasting)이 매우 복잡합니다.
3. 실무 비교: 언제 무엇을 쓰는가?
| 구분 | CRUD (State-Stored) | Event Sourcing (Event-Stored) |
| 저장 대상 | 현재 값 (Current State) | 변경 내역 (Series of Events) |
| 수정/삭제 | 자유로움 (UPDATE/DELETE) | 불가능 (보상 이벤트로 처리) |
| 과거 조회 | 불가능 (별도 로깅 필요) | 가능 (Replay) |
| 복잡도 | 낮음 (일반적) | 매우 높음 (CQRS, Snapshot 필수) |
| 추천 상황 | 일반적인 웹 게시판, 회원 정보, 단순 조회성 데이터 | 은행 계좌, 회계 장부, 법적 감사, 버전 관리 시스템(Git) |
결론
"데이터의 현재 값만 중요하고 과거는 몰라도 된다"면 무조건 CRUD 방식을 쓰세요. 이벤트 소싱은 오버 엔지니어링이 될 확률이 99%입니다.
하지만 **"돈과 관련된 시스템"**이거나 **"데이터가 어떻게 변해왔는지 분석(Audit)하는 것이 비즈니스의 핵심"**이라면, 구현이 힘들더라도 이벤트 소싱을 도입해야 합니다. 그래야 나중에 "돈이 왜 비지?"라는 사고가 터졌을 때 원인을 찾아낼 수 있습니다.
'프로그래밍 > 디자인패턴' 카테고리의 다른 글
| 피처 토글 패턴(Feature Toggle Pattern) 완벽 정리 (0) | 2025.12.07 |
|---|---|
| 메시징 패턴(Messaging Pattern) 완벽 정리 (0) | 2025.12.07 |
| 멀티테넌시 패턴(Multi-tenancy Pattern) 완벽 정리 (0) | 2025.12.07 |
| API 버저닝 패턴(API Versioning Pattern) 완벽 정리 (0) | 2025.12.07 |
| 인증 상태 관리 패턴: 세션(Session) vs 토큰(JWT) 완벽 비교 (0) | 2025.12.07 |