2P by GN⁺ | ★ favorite | 댓글 1개
  • 관계형 CRUD 모델은 저장 구조는 잘 보여주지만 비즈니스 과정을 덮어쓰기 쉬워, 시스템에서 실제로 무슨 일이 일어났는지 추적하기 어려움
  • Event Sourcing은 각 작업 뒤에 생긴 불변 이벤트를 Event Stream으로 남기고, 이후 의사결정에서 그 목록을 읽어 현재 상태를 판단함
  • 모델링은 이벤트를 먼저 찾고, 이어 사용자의 행동 의도인 명령(command) 과 비즈니스 규칙을 연결해 프로세스를 이해하는 흐름으로 진행됨
  • 기존 관계형 데이터에서 이벤트 후보를 찾을 때는 상태 컬럼, 날짜 컬럼, nullable 여부, 1:N 관계를 살피되, 상태 값만으로 완전한 이력을 복원할 수 있다고 보면 위험함
  • 최종 상태만 남은 데이터를 옮길 때는 과거 이벤트를 억지로 재구성하기보다 Order Imported 같은 명시적 import 이벤트로 시작하고, 안전한 환경에서 반복 검증하는 편이 현실적임

CRUD 데이터에서 이벤트 중심 모델로 보기

  • 관계형 데이터 모델은 어떤 데이터를 보관하는지는 보여주지만, 시스템 안에서 무슨 일이 일어났는지와 프로세스가 어떻게 상호작용하는지는 파악하기 어려움
  • 기존 CRUD 방식은 데이터를 덮어쓰면서 중요한 비즈니스 정보를 잃을 수 있음
  • Event Sourcing은 저장 크기보다 정보 품질을 우선하며, 각 작업 후 발생한 사실을 이벤트로 저장함

Event Sourcing의 기본 모델

  • 이벤트는 이미 일어난 일에 대한 사실이며, 한 번 저장되면 변경할 수 없는 불변 데이터임
  • Event Stream은 한 레코드에 대해 발생한 모든 일을 순서대로 담은 목록임
  • 과거 이벤트를 수정할 수는 없지만, 마지막에 새 이벤트를 추가해 이전 실수를 바로잡을 수 있음
  • 의사결정 시에는 이벤트 목록을 읽고 확인해 현재 상태와 다음 행동을 판단함

프로세스 모델링 순서

  • 모델링은 먼저 이벤트 발견에서 시작함
  • 이후 명령(command)을 찾아 어떤 행동을 실행하려는 의도인지 정의함
  • 마지막으로 비즈니스 규칙을 정리함
  • 이벤트는 기술 담당자와 비즈니스 담당자가 프로세스를 함께 이해하는 중심축이 됨
  • Alberto Brandolini의 EventStorming처럼 이벤트, 명령, 규칙을 함께 보며 프로세스를 파악할 수 있음

기존 관계형 데이터에서 이벤트 후보 찾기

  • 1. 상태 컬럼 살펴보기

    • status 컬럼의 값은 데이터 생애주기의 단계를 반영할 수 있음
    • 주문이 initiated, shipped, paid 같은 상태를 가진다면 각각 Order Initiated, Order Shipped, Order Paid 이벤트 후보가 될 수 있음
    • 다만 상태 값은 비즈니스 프로세스를 평평하게 만든 해석일 수 있으므로, 완전하다고 가정하면 안 됨
    • 이벤트 이름을 Order Created, Order Updated, Order Deleted처럼 CRUD 동작으로 붙이는 것은 피해야 함
    • State Obsession은 피해야 할 방식으로 제시됨
  • 2. 날짜 컬럼 확인하기

    • 날짜 컬럼은 프로세스 생애주기에서 중요한 발생 시점을 알려줄 수 있음
    • CreatedDateModifiedDate는 많은 정보를 주지 않지만, ShipmentDate, DeliveryDate, OrderPlacementDate는 더 나은 단서가 됨
    • 예시:
      • ShipmentDateOrder Shipped 이벤트 도입의 단서가 될 수 있음
      • OrderPlacementDateOrder Initiated보다 Order Placed가 더 나은 이름일 수 있음을 시사함
      • DeliveryDateOrder Delivered 이벤트가 필요할 수 있음을 보여줌
    • 이런 단서는 도메인 전문가와 확인해 실제 비즈니스 프로세스와 맞춰야 함
  • 3. 컬럼의 nullable 여부 분석하기

    • non-nullable 컬럼은 항상 제공되어야 하는 데이터임
    • nullable 컬럼은 나중에 다른 작업에서 제공되거나 선택 값일 수 있음
    • Ordering Process에서 필수 컬럼이라면 해당 데이터는 첫 Order Initiated 이벤트에도 포함되어야 함
    • 단일 이벤트 타입이 항상 스트림의 시작점은 아니며, 시작 이벤트는 여러 개일 수 있음
  • 4. 1:N 관계가 많은 테이블 찾기

    • 스트림 경계를 찾으려면 1:N 관계를 많이 가진 테이블부터 살펴볼 수 있음
    • 많은 “one” 관계를 가진 테이블은 스트림 타입 후보가 됨
    • 데이터가 서로 독립적으로 존재할 수 있는지도 논리적으로 판단해야 함
      • shipment는 order와 별도 프로세스일 수 있음
      • order line은 order 없이 존재하기 어려움
    • 경계를 논의하는 과정에서 더 많은 이벤트를 발견하고 프로세스 이해를 넓힐 수 있음

마이그레이션 시 거짓 이벤트를 만들지 않기

  • 관계형 데이터는 평평해진 최종 상태이므로, 그 상태만 보고 실제로 일어난 세부 과거 이벤트를 되맞추려 하면 실패하거나 부정확해질 수 있음
  • 작은 단위의 과거 이벤트를 억지로 만들기보다, 현재 상태 전체와 해석 코드를 담은 Order Imported 이벤트를 명시적으로 제공하는 편이 나음
  • import 이벤트는 데이터가 어떤 방식으로 들어왔는지 분명히 보여주며, 문제 해결과 진단에 중요할 수 있음

프로토타입으로 검증하기

  • 마이그레이션은 안전한 환경에서 프로토타입으로 시도하고, 모델이 실제로 어떻게 움직이는지 확인해야 함
  • 결과를 기대값과 비교하면서 반복적으로 수정해야 함
  • 서두르지 않고 기존 정보를 잃지 않으면서, 그 정보를 바탕으로 이후 모델을 개선하는 방식이 필요함
  • 관계형 데이터에서 문서 기반으로 옮기는 일반 전략은 General strategy for migrating relational data to document-based와도 연결됨

댓글과 토론

Hacker News 의견들
  • 2c: 앱의 다른 부분에서도 PostgreSQL이 필요하다면, 이벤트 데이터도 PostgreSQL + FOSS 리포팅 도구(Apache Superset, Metabase 등)에 저장하고 대략 2TB까지 버티는 게 좋음
    그 이후에는 2TB 전체를 온라인으로 유지해야 하는지, 아니면 일/시간 단위 요약만 있으면 되는지 판단하면 됨. 후자라면 PostgreSQL로 계속 가도 충분함[1]
    한 고객은 10TB+ 규모에 초당 1,500 이벤트, 레코드당 600바이트(인덱싱 전 하루 80GB)를 처리하고, 2일치 상세 데이터만 온라인에 두며 나머지는 요약하고 상세는 S3로 옮겨 Athena SQL로 계속 조회함[2]
    고객용 리포팅 포털까지 포함해 전체 비용이 2천 달러 미만이고, AWS RDS 다중 AZ 자동 장애 조치(db.m7g.2xlarge)에서 삽입과 리포팅 쿼리를 모두 처리하는데 부하는 2% 미만임. 비즈니스 팀이 직접 차트와 그래프를 만들기 때문에 엔지니어 1명이 월 5시간 미만만 유지보수에 씀
    독점 도구를 쓰면 차트 몇 개가 “기본 제공”되긴 하지만, pgsql을 쓰면 데이터가 한곳에 있고, 배울 시스템도 하나, 온라인 유지/복제/백업/복구할 시스템도 하나, 보안/확장할 시스템도 하나, 관리할 벤더도 하나이며, 이 시스템을 아는 엔지니어가 수백만 명임
    Preset이나 Metabase 같은 시스템에서는 차트 12개 만드는 데 한 시간이면 되고, 비기술 인력도 할 수 있음
    참고로 편향은 있지만, 20년 넘게 데이터베이스와 리포팅 시스템이 생겼다 사라지는 걸 봤고, 좋은 옛 PostgreSQL은 매년 더 좋아지고 있음
    https://instances.vantage.sh/aws/rds/db.m7g.2xlarge?region=u...
    [1] 정말 필요하면 추가 확장을 위한 PostgreSQL 호환 시스템도 있음. Aurora는 3~5배, TimescaleDB는 10배, CitusDB는 10배+ 정도 확장 가능함. 각각 약간 비표준이 되는 대가가 있으므로, 실제로 필요해지기 전에는 권하지 않음
    [2] 고객 리포팅 대시보드는 1초 미만 응답이 필요하고, 이는 PostgreSQL이 인덱싱된 요약 테이블을 조회해서 제공함. Athena는 병렬 스캔으로 1~2초 정도에 응답함

    • 이 방향이라면 “시간 여행”, “덮어쓴 상태 복구”, “과거 이벤트 재해석”이 필요할 때도 감사 로그만으로 충분한 경우가 있음
      저장 전 데이터의 스냅샷을 유지하고, 특정 이벤트 순서를 식별·수집하는 스크립트를 둔 뒤 사람이 검토해서 새 로직의 효과를 대량으로 소급 적용하면 됨
      https://django-simple-history.readthedocs.io/en/latest/ 같은 도구는 감사 테이블을 만드는 데 반쯤 신뢰할 만하고 단순한 해결책이고, 직접 데이터베이스 접근까지 감사해야 한다면 Postgres 트리거를 추가할 수도 있음
      이론적으로는 이벤트 소싱을 좋아하지만, 실제로는 새 CRUD 흐름을 추가하거나 초기~중기 스타트업이 예기치 못한 상황에서 자주 해야 하는 개입·핫픽스를 빠르고 안정적으로 배포하는 데 필요한 보일러플레이트가 너무 많음
      결제 처리 레일을 구현하는 식의 일이 아니라면 이벤트 소싱이 맞는 선택이 아닐 수 있음
      https://news.ycombinator.com/item?id=17817375 (2018)에도 이벤트 소싱의 단점에 대한 좋은 대화가 있음
    • 이 댓글이 글 자체보다 더 가치 있음
      PostgreSQL의 유일한 문제는 삽입 쪽에 흥미로운 확장성 문제가 있다는 점임. 보통 이벤트 소스와 DB 사이에 큐를 두는 것을 권장함
    • 대체로 {id:uuid,created_at:timestamptz,data:jsonb} 정의를 가진 테이블을 두는 식인지 궁금함
      특히 이벤트 구조가 다양하고 이벤트 정의가 변해가면 JSONB 인덱스 기능을 제대로 얻기가 어려움
      아마 이 문서를 더 익혀야 할 듯함: https://www.postgresql.org/docs/current/datatype-json.html#J...
    • 이런 시스템을 구성하려면 자세한 글을 어디서 찾을 수 있을지 궁금함
  • 예전에 팀에서 이벤트 소싱을 강하게 검토한 적이 있는데, 내게는 문제를 찾는 해법처럼 보였음
    우리에게도 동작할 수는 있었겠지만, 이점이 즉시 명확하지 않았고 새로운 방식을 도입하며 얻게 될 위험과 시행착오가 프로젝트나 회사에 최선으로 보이지 않아 결국 포기함
    학습 기회를 놓친 도구 같은 판단이었을 수도 있지만, 뒤에서 여우가 쫓아오는 상황도 아닌데 그 토끼굴에 들어가지 않은 건 후회하지 않음

    • 잘 동작하는 지루하고 전통적인 시스템은 할 일이 없고 이력서에 쓸 거리를 다듬는 비대한 엔지니어링 팀에게 위협이 됨
      이 해법이 푸는 “문제”가 바로 그거임
    • 예를 들어 시간형 데이터베이스는 금융 데이터에는 꽤 말이 됨
      하지만 대부분의 경우 일반 데이터베이스를 쓰고 과거 변경 이력을 보조 테이블에 저장하면 됨. 그러면 메인 데이터베이스는 일종의 구체화 뷰처럼 동작함
    • SQL에 저장하는 거의 모든 데이터는 사실 문서 데이터베이스에 더 잘 맞겠지만, 아무도 그쪽에 익숙하지 않아서 계속 그대로 가고 있음
      크게 불만은 없고, 잘못된 선택이었다고까지 생각하지도 않지만, 데이터 모델 변경을 처리하는 방식에서는 문제가 생김
      요즘 소프트웨어가 만들어지는 방식에 비해 대부분의 데이터 저장 방식이 따라오지 못한 것 같고, 이벤트와 큐 같은 것들은 필요한 기능을 기존 시스템 위에 얹어서 만든 결과임
      오늘날 많은 데이터 관계는 여러 서비스 사이, 즉 데이터베이스 밖에서 이뤄짐. 많은 조직의 현대 IT 환경이 그렇게 생겼기 때문임
      비즈니스 여러 팀을 지원하는 내부 마스터 데이터가 있고, 업무를 간소화하려고 300개 이상의 IT 시스템과 애플리케이션과 상호작용함
      마이크로서비스를 쓰면 비즈니스 로직과 데이터 모델을 깔끔하게 유지하기는 쉽지만, 그 대신 이벤트, 큐, 데이터 상태, 의존 저장소까지 관리해야 하며 지금은 그게 너무 복잡함
      SQL은 좋아하지만, 솔직히 요즘 우리가 만드는 시스템은 SQLite에 넣어도 거의 충분할 것 같음
  • 이런 논의에서 빠지는 부분은 이벤트 주도 아키텍처가 언제 적절한가임
    짧게 말하면 고객이 무언가를 했고 응답을 기대한다면 그건 이벤트 주도가 아니라 그냥 요청/응답임
    이벤트 주도는 대역 밖에서 무언가가 일어나는 경우임. 예를 들어 GitHub에 코드를 push해서 빌드가 트리거되는 상황임
    이 예에서 업데이트된 코드를 보려고 페이지를 새로고침하는 건 요청/응답이지만, 큐에 들어간 CI 빌드는 이벤트 주도임
    도움이 되길 바람

    • 그렇게 단순하지 않음. 요청-응답 여부는 이벤트 소싱이나 이벤트 주도 아키텍처를 고르는 기준이 아님
      이벤트 소싱이나 이벤트 주도에서도 요청-응답, 인라인, 블로킹, 순환 흐름을 만들 수 있음
      반대로 이벤트 소싱이나 이벤트 주도 없이도 워커, 큐, 액터, 멀티스레딩 같은 방식으로 비동기를 잘 만들 수 있음
  • 도메인 이벤트를 모델링하는 것은 도메인 전문가와 풀려는 문제를 설명하는 데 유용하고, 솔루션을 계획할 때 문서에 남겨두는 정도가 맞을 수 있음
    장기간 지속되는 상태 기계의 감사 추적을 제공하는 시스템을 실제로 구현하려면 Temporal.io나 durable functions 같은 것을 쓰는 편이 나을 가능성이 큼
    이런 도구는 내부 지속성에 이벤트 소싱을 사용하고, 기능을 오케스트레이션하는 코드(workflows)와 실제 세계와 상호작용하는 코드(activities)에 서로 다른 제약을 추가해 중복 제거와 멱등성을 생각하도록 강제하는 프로그래밍 모델을 제공함

    • Durable functions는 관측 가능성이 부족하다는 문제가 있음
      이 문제를 넘는 방법에 대한 제안을 듣고 싶음
  • 개념은 흥미로운데, 글이 작동 방식을 잘 설명하지 못함
    이벤트 스트림에서 현재 상태를 어떻게 효율적으로 재구성하는지, 이벤트 스트림을 데이터베이스에서 어떻게 모델링하는지가 궁금함

    • 저자의 발표가 여러 개 있음
      https://www.youtube.com/watch?v=gG6DGmYKk4I
      https://www.youtube.com/watch?v=jnDchr5eabI
      https://www.youtube.com/watch?v=ArcypYS5XBQ
      https://www.youtube.com/watch?v=uODSwR2CIV4
      GitHub에 샘플도 관리하고 있음
      https://github.com/oskardudycz/EventSourcing.NetCore
      https://github.com/oskardudycz/EventSourcing.NodeJS
      https://github.com/oskardudycz/EventSourcing.JVM
    • 방법은 두 가지임
      첫째, 이런 용도에 맞게 설계된 데이터베이스를 쓰는 것임. Google BigQuery, Amazon Redshift, ClickHouse 등이 있음
      모든 현재 데이터는 본질적으로 일종의 집계임. 다시 말해 이벤트 데이터베이스에 대한 group-by 쿼리와 같음
      이벤트가 있으면 집계 쿼리로 현재 상태나 과거 상태를 기술적으로 다시 만들 수 있으니 말이 됨
      둘째, 관계형 저장소의 이름을 바꿔 이벤트 시스템 옆에 있는 캐시 계층이라고 부르는 것임
      기능적으로는 같은 것이지만, 모든 것을 이벤트 주도로 만들어야 한다고 집착하는 사람들에게 경고등을 켜지 않음
      글에서 묘사한 아키텍처는 실제로 존재함. 다만 엄청나게 복잡해서, 이를 활용하는 서비스들은 보통 매우 표적화된 일을 함. Google Analytics, Datadog, Splunk 같은 것들을 떠올리면 됨
    • “현재 상태”가 하나만 있는 게 아님. 그런 사고방식은 모든 것을 하나의 DB에 중앙화하는 데서 나옴
      서로 다른 요구사항에 따라 서로 다른 시스템에 서로 다른 상태를 만들면 됨
      쇼핑 시스템을 만든다고 하면, 구매와 고객이 있을 때 한 서비스는 이벤트를 읽어 재무 목적의 관계형 테이블을 만들 수 있음
      다른 서비스는 이벤트를 읽어 고객 데이터의 키-값 저장소를 만들고, 세 번째 서비스는 상품 검색을 위한 OpenSearch 서비스를 구동할 수 있음
      이벤트 스트림은 목록임. Kafka처럼 목적에 맞는 것을 쓴다면 여러 목록, 즉 토픽과 파티션 등이 됨
    • 변화가 많고 경로 중간에서 무슨 일이 있었는지 볼 만큼 데이터가 흥미로운 특정 스트림에 쓰는 편이 더 말이 됨
      하지만 그것도 관계형 모델 안에서 해결할 수 있음
  • 이건 하향식 vs 상향식, 또는 맞춤형 vs 범용의 차이임
    하향식은 비즈니스 도메인에서 출발해, 사용 가능한 기술·도구·벤더 위에 구현을 매핑하는 것임
    상향식은 사용 가능한 기술·도구·벤더에서 출발해, 그것들을 조립해 작동하는 해결책을 만드는 방식임
    맞춤형에는 DDD, CQRS/ES, Sagas, TBUI(작업 기반/주도 UI), GraphQL, 대수적 자료형 등이 있음
    범용에는 RDBMS, CRUD, REST, ACID 트랜잭션, CDC, 범용 관리자 UI, 노코드/로우코드, 제한적/범용 타입 등이 있음

  • 그냥 좋은 옛날식 관계형 데이터를 계속 쓰겠음

    • 좋음, 안 될 때까지 그렇게 하면 됨. 나사에 망치를 쓰지 말라는 얘기임
  • 이벤트 기반 아키텍처에는 동의하지만, 이 글은 요점을 전달하는 데 애를 먹는 것 같음
    데이터 관계와 비즈니스 행위의 차이에 집중하겠음
    행위와 비즈니스 활동 관점으로 생각하기 시작하면 운영용 관계형 데이터 저장소에서 멀어지는 움직임이 훨씬 더 분명해짐

    • 추상적인 수준에서는 이벤트도 관계로 모델링할 수 있음
  • 이벤트 소싱에는 좋은 성질이 많아서 흥미롭긴 함
    하지만 여전히 관계가 필요하지 않나? 그렇다면 그 관계는 어떻게 구현하나?
    답이 “애플리케이션 계층 코드에 모두 암묵적으로 들어 있다”라면 받아들이기 어려움
    그래도 관계를 질의하거나, 관계 뷰를 최신으로 유지하거나, 그와 비슷한 무언가가 필요함
    관계가 지속성 모델의 핵심이 아니어도 괜찮지만, 데이터 계층 어딘가에는 구현되어야 하는데 여기서는 그 언급이 보이지 않음
    Firestore에도 같은 문제가 있음. 모두가 관계를 어떻게든 처리하지만, 결국 확장성 없는 스파게티 애플리케이션 코드가 됨

    • 이벤트 소싱 시스템에서는 이벤트 스트림을 읽기 모델로 투영하고, 읽기 모델은 관계형, 시계열 등 여러 개가 될 수 있음
      함수형 프로그래밍에 익숙하다면, 이벤트 스트림을 하나의 상태로 접는 fold 연산과 본질적으로 같음
      과거에 이벤트 소싱 시스템을 다뤄본 바로는, 명시적으로 저장된 이벤트 이력이 있다는 장점은 있지만 복잡성도 많이 늘어남
      읽기 모델을 실제로 어떻게 생성할지, 모델 버전을 어떻게 관리할지, 읽기 모델의 스냅샷을 둘지 같은 문제가 생김
      내 경험상 이 패턴이 적용된 대부분의 맥락에서는 추가 복잡성이 그만한 가치가 없었음
  • 필요한 것은 명령 큐임. 명령 이벤트는 도메인 이벤트가 아님