이벤트 소싱을 적용하기 전에 반드시 고민해봤어야 할 것들

2026.01.06

📚 Overview

우리 서비스의 주문 도메인에는 조금 특이한 특징이 하나 있다.

일반적인 상품 주문과 달리 SNS 마케팅 주문은 하나의 주문이 여러 개의 실행 단위로 쪼개진다. 예를 들어 “좋아요” 주문이 들어오면 시스템 내부에서는 다음과 같은 일들이 순차적으로 발생한다.

  • 주문이 생성된다
  • 결제 및 포인트 차감이 이루어진다
  • 주문 상태가 “진행중”으로 변경된다
  • 좋아요 작업이 여러 번에 나뉘어 실행된다
  • 각 실행 결과가 개별 로그로 기록된다
  • 모든 작업이 끝나면 주문 상태가 “완료”로 변경된다

이 과정에서 우리는 주문 하나를 처리하기 위해 수백, 많게는 수천 건의 실행 로그를 쌓고 있었다.

또한, 이 로그들은 단순한 디버깅 용도가 아니었다. 운영에서는 이 로그를 기준으로 주문 진행 상황을 판단했고 CS 대응에서도 “어디까지 실행되었는지” 를 설명하는 근거로 사용되고 있었다.

이 지점에서 이런 생각이 들기 시작했다.

이미 우리는 “무엇이 언제 일어났는지”를 이벤트처럼 기록하고 있지 않은가?

주문 상태 하나로는 설명되지 않는 흐름을 결국 수많은 히스토리 로그로 보완하고 있었고, 이 로그들이 사실상 주문의 실제 진행 상황을 가장 정확하게 말해주고 있었다.

그래서 우리는 주문 도메인을 “현재 상태”가 아니라 “발생한 일들의 흐름”으로 표현하는 것이 더 자연스럽지 않을까? 라는 고민을 하게 되었다. 이 고민의 끝에서 등장한 선택지가 바로 이벤트 소싱(Event Sourcing) 이었다.

이 글은 비교적 복잡한 도메인에서 왜 이벤트 소싱을 고려하게 되었는지, 그리고 실제로 적용해본 뒤 무엇을 얻고 무엇을 내려놓게 되었는지를 정리한 기록이다.



1. 이벤트 소싱을 처음 접했을 때, 가장 매력적으로 느껴졌던 점


이벤트 소싱이 매력적으로 느껴졌던 이유는 새로운 아키텍처 패턴이어서가 아니었다. 이미 우리 시스템은 상태보다 “무슨 일이 일어났는지” 로 더 많은 질문에 답하고 있었기 때문이다.

  • 이 주문은 지금 어디까지 진행되었는가?
  • 마지막으로 성공한 작업은 무엇인가?
  • 실패가 발생했다면 어느 지점에서 멈췄는가?

이 질문들에 대해 우리는 주문 테이블의 상태 값이 아니라 수많은 실행 로그를 기준으로 판단하고 있었다. 이벤트 소싱은 이런 상황을 전혀 다른 방식으로 해석하게 만들었다.

이벤트 소싱에서는 현재 상태가 진실의 원천이 아니다. “주문이 완료되었다” 라는 상태는 결과일 뿐이고, 그 상태에 도달하기까지 발생한 사건들이 진짜 기록이 된다.

  • 주문이 생성되었다
  • 포인트가 차감되었다
  • 주문 상태가 변경되었다
  • 좋아요 작업이 실행되었다

이 각각을 하나의 이벤트로 다루고 현재 상태는 이 이벤트들의 누적 결과로 계산한다는 개념은 이미 우리가 운영 과정에서 하고 있던 일과 크게 다르지 않아 보였다.

오히려 이벤트 소싱은 그동안 흩어져 있던 로그와 히스토리를 시스템의 중심 개념으로 끌어올리는 구조처럼 느껴졌다.

로그가 보조 수단이 아니라 진실의 원천이 되고 상태는 결과로서 파생되는 구조. 이 점이 이벤트 소싱을 기술적으로 멋있어 보이기보다 도메인 흐름에 잘 맞는 선택지처럼 보이게 만들었다.



2. 이벤트 소싱 아키텍처를 시범적으로 도입해보며 느낀 점


이벤트 소싱을 검토하면서 가장 먼저 정리하려 했던 건 “이걸 어디까지 이해하고 도입할 것인가” 였다. 이벤트 소싱은 단순히 이벤트를 저장하는 방식이 아니라 시스템의 상태를 바라보는 관점을 바꾸는 아키텍처에 가깝다.

기본적인 구조는 비교적 명확했다.

  • 이벤트 소싱에서는 시스템의 현재 상태를 직접 저장하지 않는다.
  • Aggregate가 내린 판단의 결과를 이벤트로 만들어 Event Store에 순서대로 기록한다.
  • 현재 상태는 이 이벤트들을 재생해 계산한다.
  • 이벤트 수가 많아질 경우를 대비해, 중간 결과를 Snapshot으로 저장해 재생 비용을 줄인다.

이 구조에서 중요한 전제는 하나였다.

현재 상태가 진실의 원천이 아니다. 진실은 항상 이벤트의 흐름에 있다.

이 말이 의미하는 바는 생각보다 컸다. 기존 구조에서는 상태를 먼저 정하고 이 상태를 어떻게 변경할지를 고민했다면 이벤트 소싱에서는 그 반대였다.

  • 어떤 일이 발생했는가
  • 그 일이 어떤 순서로 일어났는가
  • 그 결과로 지금의 상태가 만들어졌는가

상태가 더 이상 기준이 아니라 이벤트들의 누적 결과가 기준이 된다. 이 관점이 가장 크게 와닿았던 이유는 도메인을 설명하는 방식 자체가 달라졌기 때문이다.

“주문이 완료되었다.” 라는 짧은 문장 대신 “주문이 생성되었고, 결제가 되었고, 작업이 여러 번 실행된 끝에 완료되었다.” 라는 흐름으로 도메인을 설명해야만 한다. 그래서 이벤트 소싱을 단순한 저장 전략이 아니라, 도메인을 표현하는 방법으로 이해하게 되었다.

시범적으로 이벤트 소싱 구조를 도입해보며 우리는 이 아키텍처가 요구하는 전제들을 하나씩 체감하기 시작했다.

  • 이벤트는 한 번 기록되면 수정할 수 없고, 항상 누적된다.
  • 명령을 처리하는 모델과 조회를 담당하는 모델이 구조적으로 분리된다.
  • 상태 조회를 위해 이벤트 스트림을 기반으로 Projection*을 유지해야 한다.

💡 Projection이란?

기존 CRUD 방식에서는 테이블 하나 조회하면 상태가 바로 나왔지만 이벤트 소싱에서는 다르다. Event Store에는 이벤트들만 있기에 “현재 상태”라는 개념의 테이블이 없다.

상태는 이벤트를 모두 재생해야만 계산 가능한데 이걸 매번 하면 너무 느리고 비효율적이다. 이를 위해 이벤트 스트림을 구독해서 조회에 최적화된 형태의 데이터를 따로 만들어두는 것을 Projection이라 한다.

이 시점부터 이벤트 소싱은 “적용할지 말지” 의 문제가 아니라, “이 전제를 감당할 수 있는 도메인인가”를 묻는 문제로 바뀌기 시작했다.



3. 이벤트 소싱은 장점만큼의 비용을 요구했다


이벤트 소싱을 시범적으로 적용해보면서 가장 먼저 느낀 건 “이 구조는 단순히 저장 방식을 바꾸는 문제가 아니다” 라는 점이었다.

이벤트 소싱은 설계 단계에서부터 여러 전제를 자연스럽게 요구한다.

먼저, 이벤트는 한 번 기록되면 수정할 수 없다. 잘못된 이벤트를 지우거나 고치는 대신, 항상 새로운 이벤트로 보정해야 한다. 이건 데이터 관점에서는 굉장히 정직하지만 설계 단계에서 훨씬 신중한 판단을 요구한다.

또 하나는 이벤트 스키마의 무게였다. 이벤트는 단순한 로그가 아니라 도메인에서 실제로 일어난 일을 설명하는 공식 기록이 된다. 한 번 정의한 이벤트 스키마는 Projection이나 다른 소비자들이 그대로 의존하게 되고 변경이 필요해질수록 그 영향 범위도 함께 커진다.

읽기 모델을 따로 관리해야 한다는 점도 생각보다 부담이었다. 이벤트를 저장하는 것만으로는 바로 화면에 보여줄 상태를 만들 수 없기 때문에 이벤트를 소비해 상태를 만들어주는 Projection을 별도로 설계하고 유지해야 했다.

결국 이런 질문들이 계속 따라붙었다.

  • 이 도메인은 이벤트 불변성을 끝까지 감당할 수 있는가?
  • 이벤트 스키마 변경 비용을 받아들일 수 있는가?
  • 읽기 모델과 쓰기 모델을 분리해서 관리할 만큼의 복잡도가 있는가?

이 질문들에 선뜻 “그렇다” 고 답하기 어려운 순간들이 늘어나면서 이벤트 소싱은 점점 “모든 도메인에 적용할 기본 구조” 가 아니라 “조건이 맞는 경우에만 선택해야 할 아키텍처” 로 보이기 시작했다.

구조 자체는 분명 매력적이었지만, 그 매력을 유지하기 위해 감당해야 할 비용도 같이 바라봐야 한다는 걸 이 시점에서 체감하게 되었다.



4. 모든 도메인에 같은 구조를 적용하지 않기로 했다


모든 도메인에 이벤트 소싱을 도입했다가 걷어낸 경험 이후, 우리 팀은 아키텍처를 바라보는 기준을 조금 다르게 세우게 되었다. “이 구조가 좋은가?” 가 아니라 “이 도메인에 이 구조가 필요한가?” 를 먼저 묻기로 한 것이다.

현재 우리 팀은 MSA 환경에서 여러 도메인을 운영하고 있다. 그 과정에서 모든 도메인에 이벤트 소싱을 동일하게 적용하지 않고 도메인의 성격에 따라 서로 다른 구조를 선택하고 있다.

도메인 히스토리가 핵심 가치인 영역에서는 이벤트 소싱을 유지하고, 현재 상태만 명확하면 충분한 영역에서는 일반적인 CQRS 패턴만 적용해 모델을 단순화했다.

예를 들어, 이벤트의 순서와 누적 히스토리가 비즈니스 의미를 갖는 도메인에서는 이벤트 소싱이 분명한 장점을 제공한다. 반대로, 상태 전이가 단순하고 과거 변경 이력이 큰 의미를 갖지 않는 경우에는 이벤트 소싱이 오히려 복잡도를 키우는 선택이 될 수 있었다.

이렇게 도메인별로 구조를 달리 가져가면서 느낀 점은 분명했다. 아키텍처는 일관성보다 적합성이 더 중요하다는 사실이다.

모든 도메인을 같은 방식으로 맞추는 것이 아니라 각 도메인이 해결해야 하는 문제의 성격에 맞춰 이벤트 소싱을 선택하거나, 혹은 선택하지 않는 것.

이 경험 이후로 우리 팀은 아키텍처를 “도입하는 것” 보다 언제 쓰고, 언제 쓰지 않을지를 결정하는 과정을 더 중요하게 다루고 있다.



🤔 Understanding

이벤트 소싱을 도입했다가 다시 일부 도메인에서 걷어내는 과정은 단순히 아키텍처 하나를 선택했다가 실패한 경험은 아니었다.

오히려, “어떤 구조가 더 좋아 보이는가” 보다 “이 도메인이 정말 필요로 하는 게 무엇인가” 를 더 진지하게 고민하게 만든 계기였다.

이벤트 소싱은 분명 강력한 도구다. 히스토리가 곧 도메인의 가치가 되는 영역에서는 다른 구조로는 대체하기 어려운 장점을 가진다. 하지만 그만큼 많은 전제와 책임을 함께 요구하는 구조이기도 했다.

이 경험 이후로 우리 팀은 아키텍처를 하나의 정답으로 두지 않게 되었다. MSA 환경에서 각 도메인의 성격에 맞춰 이벤트 소싱을 선택하기도 하고, 단순한 CQRS로 충분하다고 판단하기도 한다.

지금 돌이켜보면, 가장 큰 수확은 “이벤트 소싱을 썼다”는 사실이 아니라 직접 써보고, 유지해보고, 다시 되돌릴 수 있었던 경험 자체였던 것 같다.

아키텍처는 도입하는 것보다 언제 쓰고, 언제 쓰지 않을지를 결정하는 일이 더 어렵다. 이번 경험은 그 판단을 조금 더 단단하게 만들어주었다.