프로그래밍/디자인패턴

이벤트 소싱 패턴(Event Sourcing Pattern) 완벽 정리

Jinwookoh 2025. 12. 7. 21:17

"통장에 잔액이 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)하는 것이 비즈니스의 핵심"**이라면, 구현이 힘들더라도 이벤트 소싱을 도입해야 합니다. 그래야 나중에 "돈이 왜 비지?"라는 사고가 터졌을 때 원인을 찾아낼 수 있습니다.