이벤트 소싱은 데이터를 덮어쓰는 것을 중단하라고 요구합니다. 전통적인 CRUD 애플리케이션에서는 사용자의 배송 주소를 업데이트하려면 해당 행을 찾아 값을 변경하고 이전 상태를 버려야 합니다. 이벤트 소싱은 다른 길을 택합니다. 각 변경 사항을 불변의 사실로 저장합니다. 예를 들어, 사용자가 계정을 생성했거나, 주소를 업데이트했거나, 이메일을 인증했다는 사실 말입니다. 시스템의 현재 상태는 직접 저장되지 않습니다. 이러한 이벤트들을 순서대로 재생(replay)하여 계산합니다.

이 패턴은 실제적인 문제들을 해결합니다. 감사 추적(Audit trails)은 공짜로 얻는 부산물이 됩니다. 과거 어느 시점의 주문 상태라도 재구성할 수 있습니다. 정확히 어떤 일이 일어났는지 재생함으로써 디버깅할 수 있습니다. 트레이드오프는 복잡성입니다. 이제 단순한 행 대신 사실의 스트림, 읽기 모델, 그리고 최종 일관성을 관리해야 합니다.

PostgreSQL을 이벤트 저장소로 사용할 수 있습니다. 대부분의 팀이 이미 이를 운영하고 있습니다. ACID 트랜잭션, 유연한 페이로드를 위한 JSONB, 그리고 검증된 백업 도구를 제공합니다. 첫날부터 Kafka, Cassandra 또는 특화된 이벤트 저장소 데이터베이스를 도입할 필요는 없습니다. 표준 Postgres 인스턴스는 인프라 규모를 확장하지 않고도 이벤트 소싱이 요구하는 트랜잭션 보장과 감사 추적을 제공합니다.

Postgres 이벤트 저장소의 형태

스키마는 당황스러울 정도로 단순할 수 있습니다. 최소한 이벤트를 추가(append)하기만 하고 기존 데이터를 직접 업데이트하지 않는 테이블이 필요합니다. 실용적인 설계는 다음과 같습니다:

  • id: 전역 순서를 나타내는 bigserial 또는 UUID.
  • stream_id: 단일 사용자나 주문에 대한 모든 변경 사항과 같이 관련된 이벤트를 그룹화합니다.
  • event_type: UserEmailChanged, PaymentReceived, InventoryAdjusted와 같은 일반 텍스트.
  • payload: 해당 발생 건에 대한 특정 데이터를 담는 JSONB.
  • occurred_at: 타임존 정밀도를 포함한 발생 시간.
  • version: 스트림당 버전, 낙관적 동시성 제어 적용.

애플리케이션 코드나 데이터베이스 제약 조건을 통해 불변성 규칙을 강제합니다. (stream_id, version)에 유니크 인덱스를 설정하면 두 명의 작성자가 동일한 시퀀스 번호를 추가하는 것을 방지할 수 있습니다. 명령이 들어오면 해당 스트림의 현재 버전을 읽고, 이를 증가시킨 뒤, 트랜잭션 내에서 새 이벤트를 삽입합니다. 만약 다른 프로세스가 먼저 처리했다면, 유니크 제약 조건 위반이 발생하며, 명령을 재시도하거나 거부할 수 있습니다.

구체적인 예를 들어보겠습니다. 재고 관리 시스템을 운영한다고 가정해 봅시다. quantity 컬럼이 있는 단일 inventory 행 대신, inventory_events 테이블에 이벤트를 추가합니다. ItemReceived는 10개를 추가합니다. ItemReserved는 2개를 차감합니다. ItemShipped는 3개를 차감합니다. SKU-42의 현재 재고를 알기 위해서는 관련 이벤트 페이로드를 모두 합산합니다. 3일 전의 재고를 알기 위해서는 해당 타임스탬프까지만 합산합니다. 지난 화요일에 배송 로직의 버그로 오류가 발생했다면, 수정된 코드로 이벤트를 재생하여 실제 상태를 파악할 수 있습니다. 단순한 UPDATE 문으로는 이를 수행할 수 없습니다.

문제를 방지하기 위한 원칙들

Postgres를 기반으로 구축한다고 해서 규율이 필요 없는 것은 아닙니다. 다음 원칙들은 이벤트 소싱 시스템에 직접적으로 적용됩니다.

단순함을 유지하세요. 복잡성은 신뢰성을 떨어뜨립니다. 하나의 작동하는 흐름을 배포하기 전에 범용 이벤트 프레임워크를 만들고 싶은 유혹을 뿌리치세요. 단일 테이블, 이벤트를 추가하는 레포지토리 함수, 그리고 읽기 모델을 구축하는 프로젝션 워커만으로도 충분히 가치를 증명할 수 있습니다. 구체적인 문제가 나타날 때만 도구를 추가하세요.

작게 시작하세요. 전체 모놀리스를 다시 작성하지 마세요. 감사 추적이 오버헤드를 상쇄할 만한 하나의 바운디드 컨텍스트(bounded context)를 선택하세요. 결제 원장, 워크플로우 엔진, 또는 재고 예약 시스템이 좋은 후보입니다. 해당 파이프라인 하나를 엔드 투 엔드로 구축하고 운영 환경에서 실행해 보세요. 그 후에 확장 여부를 결정하십시오.

성공의 기준을 먼저 정의하세요. 이벤트 소싱은 기본 아키텍처가 아니라 특정 요구 사항에 대한 해결책입니다. 요구 사항이 단순히 최신 상태를 추적하는 것이라면 CRUD가 더 빠르고 저렴합니다. 시계열 쿼리, 엄격한 감사 가능성, 또는 필요에 따라 읽기 모델을 재구축하는 기능이 필요하다면 이벤트 방식이 합리적입니다. 도입을 결정하기 전에 어떤 문제를 해결하려 하는지 명확히 하세요.

최적화하기 전에 측정하세요. 적절한 사양의 하드웨어에서 최신 PostgreSQL은 단순한 append-only 테이블로 초당 수천 개의 이벤트를 수용할 수 있습니다. 모니터링을 통해 더 간단한 해결책을 모두 시도했음이 증명되기 전까지는 이벤트 저장소를 샤딩하거나 복잡한 파티셔닝 체계를 도입하지 마세요. 쿼리하는 필드에 인덱스를 생성하세요. append-only 워크로드에 맞춰 autovacuum을 튜닝하세요. 그런 다음 다시 측정하세요.

모든 것을 테스트하십시오. 이벤트 핸들러를 유닛 테스트하십시오. append 경로를 통합 테스트하십시오. 가장 중요한 것은 실패 시나리오를 테스트하는 것입니다. 두 노드가 동시에 동일한 스트림에 append를 시도하면 어떻게 될까요? 프로젝션 워커가 배치 작업 도중 충돌하면 어떻게 될까요? 낙관적 동시성(optimistic concurrency)과 최소 한 번 전달(at-least-once delivery) 보장을 검증하는 테스트를 작성하십시오.

프로덕션 환경에서 모니터링하십시오. 이벤트 테이블은 계속 커질 것입니다. 업데이트가 행 수를 일정하게 유지하는 정규화된 스키마와 달리, 이벤트 소싱은 의도적으로 누적(additive)되는 방식입니다. 테이블 크기, 디스크 I/O, 그리고 쓰기 모델(write model)과 읽기 모델 프로젝션(read model projections) 사이의 지연(lag)을 추적하십시오. 사용자가 오래된 데이터(stale data)를 인지하기 전에 프로젝션 지연에 대한 알림을 설정하십시오.

수동 작업을 자동화하십시오. 수동 스키마 변경, 수동 프로젝션 재구축, 수동 이벤트 재생은 시한폭탄과 같습니다. 마이그레이션 전략을 스크립트로 만드십시오. 이벤트 스키마를 진화시킨다면, 자정의 인적 개입 없이도 오래된 이벤트를 새로운 로직으로 재생할 수 있도록 업캐스팅(upcasting) 또는 변환(transformation)을 자동화하십시오.

선택 사항을 문서화하십시오. 특정 스트림이 왜 존재하는지, 각 이벤트 유형이 무엇을 의미하는지, 그리고 팀이 이벤트 대신 CRUD를 선택해야 하는 시점은 언제인지 기록하십시오. 이벤트 소싱은 인지 부하(cognitive load)를 유발합니다. 잘 작성된 문서는 새로운 엔지니어가 잘못 추측하여 중요한 스트림에 잘못된 형식의 이벤트를 추가하는 실수를 방지합니다.

수개월을 낭비하게 만드는 함정들

이벤트 소싱은 다이어그램에서는 우아해 보이지만 프로덕션에서는 고통스러울 수 있습니다. 다음 함정들을 주의하십시오.

복잡성 과소평가. 상태를 재구축하기 위해 이벤트를 재생하는 것은 개념적으로 간단합니다. 하지만 멱등성(idempotency) 관리, 성능을 위한 스냅샷(snapshotting), 애그리거트(aggregate) 간의 보상 트랜잭션(compensating transactions) 관리는 그렇지 않습니다. 시스템을 작은 단위로 나누십시오. 한 번에 하나의 스트림씩 해결하십시오.

과잉 엔지니어링. 언젠가 이벤트 양이 많아질 것이라는 상상만으로 멀티 노드 Kafka 클러스터를 구축하지 마십시오. Postgres만으로도 놀라울 정도로 많은 것을 처리할 수 있습니다. 현재 설정 내에서 해결할 수 없는 측정 가능한 병목 현상이 발생했을 때만 새로운 인프라를 도입하십시오.

기술 부채 방치. 오래된 이벤트 스키마는 영원히 남습니다. OrderCreated 페이로드를 변경하더라도, 기존 형태의 역사적 이벤트가 천만 개나 남아 있을 수 있습니다. 이 부채를 추적하십시오. 하위 호환이 가능한 리더(backward-compatible readers)나 마이그레이션 스크립트를 계획하십시오. 레거시 이벤트의 부담이 새로운 기능 개발을 늦추게 두지 마십시오.

팀이 운영할 수 없는 도구 선택. 아무리 최고의 아키텍처라도 단 한 사람만 이해한다면 실패한 것입니다. 팀이 Postgres와 SQL에 익숙하다면 거기서부터 시작하십시오. 특화된 이벤트 스토어를 도입한다면, 새벽 2시에 그것을 디버깅할 수 있는 운영 전문성을 갖추었는지 확인하십시오.

실질적인 시작점

이 접근 방식이 문제 해결에 적합하다면, 5분기 동안의 재작업을 기다리지 마십시오. 이번 주부터 시작하십시오.

현재 시스템을 감사(audit)하십시오. 감사 추적(audit trail)이 실질적인 고통을 해결해 줄 수 있는 곳을 찾으십시오. 현재 단일 status 컬럼만 유지하고 있는 주문 상태 머신(state machine)일 수도 있고, 잔액 수정을 위해 수동 데이터베이스 패치가 필요한 금융 원장(ledger)일 수도 있습니다. 상태를 덮어쓰는 방식 때문에 문제가 되었던 지점 하나를 선택하십시오.

그런 다음 오늘 바로 실행할 수 있는 작은 개선 사항 하나를 선택하십시오. 이벤트 테이블 하나를 만드십시오. 스트림 하나를 모델링하십시오. 해당 이벤트로부터 읽기 모델을 구축하는 프로젝션 하나를 작성하십시오. 기능 플래그(feature flag) 뒤에 배포하고 실제 트래픽을 처리하는 모습을 지켜보십시오.

PostgreSQL을 이용한 이벤트 소싱은 마법이 아닙니다. 그것은 사물이 현재 어디에 있는지뿐만 아니라, 어떻게 그곳에 도달했는지까지 알아야 하는 팀을 위한 실용적인 도구입니다. 천천히 구축하고, 정직하게 측정하며, 실제 요구 사항이 아키텍처를 이끌도록 하십시오.

Source: https://dev.to/therizwansaleem/event-sourcing-with-postgresql-using-the-database-as-an-event-store-2kd4