
이벤트 소싱: 상태 대신 '변화'를 저장하는 아키텍처
은행 계좌의 잔액이 왜 0원인지 궁금해본 적 있나요? 단순히 현재 상태(잔액)만 저장하는 대신, 입금, 출금 같은 모든 '사건(Event)'을 저장하여 시스템을 구축하는 이벤트 소싱 패턴. CQRS와 함께 사용되는 이유와 구현 시 마주하는 현실적인 문제들을 깊이 있게 다룹니다.

은행 계좌의 잔액이 왜 0원인지 궁금해본 적 있나요? 단순히 현재 상태(잔액)만 저장하는 대신, 입금, 출금 같은 모든 '사건(Event)'을 저장하여 시스템을 구축하는 이벤트 소싱 패턴. CQRS와 함께 사용되는 이유와 구현 시 마주하는 현실적인 문제들을 깊이 있게 다룹니다.
로버트 C. 마틴(Uncle Bob)이 제안한 클린 아키텍처의 핵심은 무엇일까요? 양파 껍질 같은 계층 구조와 의존성 규칙(Dependency Rule)을 통해 프레임워크와 UI로부터 독립적인 소프트웨어를 만드는 방법을 정리합니다.

왜 CPU는 빠른데 컴퓨터는 느릴까? 80년 전 고안된 폰 노이만 구조의 혁명적인 아이디어와, 그것이 남긴 치명적인 병목현상에 대해 정리했습니다.

ChatGPT는 질문에 답하지만, AI Agent는 스스로 계획하고 도구를 사용해 작업을 완료한다. 이 차이가 왜 중요한지 정리했다.

결제 API 연동이 끝이 아니었다. 중복 결제 방지, 환불 처리, 멱등성까지 고려하니 결제 시스템이 왜 어려운지 뼈저리게 느꼈다.

사용자가 고객센터에 전화해서 묻습니다. "내 통장에 왜 8만 원밖에 없어요?"
전통적인 CRUD 기반의 시스템 개발자라면 DB를 조회합니다.
SELECT balance FROM accounts WHERE user_id = 'A';
결과는 80000. 이게 전부입니다.
"고객님, DB에 8만 원이라고 적혀있어서 8만 원입니다."라고 답할 수밖에 없습니다. 왜 그렇게 됐는지는 데이터가 덮어씌워져서 날아갔기 때문입니다.
하지만 이벤트 소싱(Event Sourcing) 시스템이라면 다릅니다.
SELECT * FROM events WHERE user_id = 'A';
이벤트 소싱은 "현재 상태(State)는 도출된 값일 뿐, 진실은 이벤트(Event)들의 연속이다"라는 철학을 가집니다.
이벤트 소싱의 가장 큰 특징은 데이터를 수정하거나 삭제하지 않는다는 것입니다. 오직 추가(Append)만 합니다. 회계 장부(Ledger)를 생각하면 됩니다. 장부에 볼펜으로 한 번 적으면 지우개로 지울 수 없습니다. 실수를 했다면? 지우는 게 아니라 "정정 기입(Correction Entry)"이라는 새로운 줄을 추가해서 상쇄시켜야 합니다.
DB 관점에서 보면 UPDATE와 DELETE 문이 없습니다. 오직 INSERT만 존재합니다.
이것은 엄청난 이점을 가져다줍니다.
하지만 문제가 있습니다. 10년 쓴 통장의 잔액을 확인하려고 10년 치 입출금 내역을 처음부터 다 계산해야 할까요? 너무 느립니다. 그래서 중간중간 스냅샷(Snapshot)을 찍습니다. "2024년 1월 1일 기준 잔액: 500만 원"이라는 상태를 별도로 저장해 두는 것이죠. 그러면 조회할 때는 2024년 1월 1일 스냅샷을 불러오고, 그 이후에 발생한 소수의 이벤트만 합산하면 됩니다. 이렇게 하면 쓰기 성능(이벤트 적재)과 읽기 성능(조회)을 모두 잡을 수 있습니다.
이벤트 소싱을 하면 필연적으로 CQRS (Command Query Responsibility Segregation) 패턴을 쓰게 됩니다. 이벤트 저장소(Event Store)는 쓰기에는 최적화되어 있지만, 복잡한 조회("최근 1주일간 가입한 여성 회원 리스트")에는 쥐약입니다. 그래서 명령(Command/Write) 모델과 조회(Query/Read) 모델을 분리합니다.
MoneyDeposited 이벤트 저장.이 구조는 시스템의 확장성을 극대화합니다. 읽기 요청이 많으면 조회용 DB만 늘리면 되고, 쓰기가 많으면 이벤트 저장소만 확장하면 되니까요.
마이크로서비스 환경에서 이벤트 소싱이 빛을 발하는 곳은 사가 패턴(Saga Pattern)입니다. 여러 서비스에 걸친 트랜잭션을 처리할 때, 이벤트 소싱은 강력한 무기가 됩니다.
예를 들어 '주문' 트랜잭션이 [주문 서비스] -> [결제 서비스] -> [배송 서비스]를 거친다고 합시다. 중간에 결제가 실패하면? 이미 생성된 주문을 취소해야 합니다. 이때 이벤트 소싱을 쓰면 보상 트랜잭션(Compensating Transaction)을 구현하기 쉽습니다.
OrderCreated 발행OrderCreated 수신 -> 결제 시도 -> PaymentFailed 발행PaymentFailed 수신 -> OrderCancelled 발행 (보상)
이 모든 과정이 이벤트 로그에 남으므로, 어디서 무엇이 잘못되어 롤백되었는지 정확히 추적할 수 있습니다.실제로 가장 골치 아픈 문제는 이벤트 구조가 바뀔 때입니다.
처음에는 AddressChanged 이벤트에 city와 street만 있었는데, 나중에 zipcode가 필요해서 추가했다고 칩시다.
코드는 새로운 구조(zipcode 포함)를 기대하지만, DB에 저장된 3년 전 이벤트에는 zipcode가 없습니다.
일반 DB라면 마이그레이션 스크립트로 데이터를 다 고치겠지만, 이벤트 소싱은 불변이라 데이터를 고칠 수 없습니다.
결국 코드 레벨에서 Upcasting을 해야 합니다. 이벤트를 불러올 때, "어, 이거 버전 1 이벤트네?"라고 감지하면, 디폴트 값을 채워서 버전 2 객체로 변환해 주는 로직이 필요합니다. 시스템이 오래될수록 이런 변환 로직이 덕지덕지 붙어서 코드가 복잡해지는 것이 이벤트 소싱의 가장 큰 단점 중 하나입니다.
Q: 이벤트 소싱과 CDC(Change Data Capture)는 같은 건가요? A: 비슷해 보이지만 목적이 다릅니다. CDC는 "DB가 변경된 결과"를 캡처해서 다른 곳(Kafka 등)으로 보내는 기술입니다. 즉, State가 먼저고 Event가 나중입니다. 반면 이벤트 소싱은 Event가 먼저이고 State가 결과입니다. 이벤트 소싱이 비즈니스 의도(Intent)를 훨씬 잘 보존합니다.
Q: GDPR(개인정보 삭제 권리) 문제는 어떻게 해결하나요? A: 이벤트 소싱의 불변성 때문에 "삭제"가 어렵습니다. 이벤트를 지울 수 없으니까요. 가장 흔한 해결책은 Crypto-Shredding입니다. 이벤트에 개인정보를 평문으로 쓰지 않고 암호화해서 저장합니다. 그리고 암호화 키는 별도의 DB에 저장합니다. 사용자가 탈퇴해서 정보 삭제를 요청하면? 이벤트는 놔두고 암호화 키만 삭제해버립니다. 키가 없으면 이벤트 데이터를 아무도 읽을 수 없으므로, 사실상 삭제된 것과 같습니다.
Q: 이벤트 저장소로 Kafka를 써도 되나요? A: 가능은 하지만 추천하지 않습니다. Kafka는 Message Broker이지 장기 저장소(DB)가 아닙니다. 인덱싱 기능도 약하고, 특정 Aggregate의 이벤트만 조회하는 기능도 없습니다. EventStoreDB 같은 전용 솔루션이나, 차라리 PostgreSQL에 JSON으로 저장하는 것이 낫습니다.