Summary#
Event-sourced aggregate의 write-model contract는 “이벤트를 저장한다”보다 더 좁고 엄격한 계약이다. 핵심은 aggregate stream의 현재 revision/version을 기준으로 명령을 검증하고, append 시 optimistic concurrency 조건을 명시하며, snapshot은 authoritative state가 아니라 replay 최적화 캐시로 취급하고, replay/migration/upcasting 경로가 외부 부작용 없이 결정적으로 실패하거나 성공하도록 만드는 것이다.
실무적으로 안전한 기본선은 다음과 같다: aggregate stream마다 단조 증가하는 revision을 보존하고, command 처리 결과를 expectedVersion/expected revision과 함께 append하며, command id 또는 idempotency key로 중복 명령을 탐지하고, snapshot에는 aggregate version·snapshot schema version·model compatibility marker를 포함한다. schema evolution 또는 aggregate invariant 변경 시에는 snapshot을 무효화하거나 snapshot migration을 별도 검증해야 한다.
Key Points#
- Stream versioning / optimistic concurrency
- 각 aggregate instance는 독립 event stream으로 취급하고, stream revision 또는 aggregate version을 write contract의 일부로 둔다.
- command handler는 현재 stream을 replay 또는 snapshot+tail로 복원한 뒤 invariant를 검사하고, 새 event를 append할 때 “내가 본 version이 맞을 때만 append”하는 expected-version 조건을 사용해야 한다.
-
이 조건은 lost update를 줄이지만, business-level conflict resolution을 자동으로 해결하지는 않는다. 같은 stream에 동시에 도착한 두 command 중 하나는 concurrency conflict로 실패할 수 있고, caller는 재조회 후 재시도·거절·merge 중 하나를 선택해야 한다.
-
Aggregate version은 저장소 offset과 구분해야 한다
- global log position, stream revision, aggregate version, event sequence number는 같은 의미가 아닐 수 있다.
- aggregate invariant 검증에는 보통 per-stream revision/aggregate version이 필요하다.
-
projection checkpoint나 subscription position을 aggregate write precondition으로 재사용하면 read-model lag와 write consistency boundary가 섞이는 위험이 있다.
-
Snapshot invalidation
- snapshot은 canonical source of truth가 아니라 replay 비용을 줄이는 파생 캐시다.
- snapshot에는 최소한
aggregate_type,aggregate_id,stream_revision또는aggregate_version,snapshot_schema_version, aggregate model/code compatibility marker를 포함하는 것이 안전하다. - event schema upcaster가 오래된 event를 현재 형태로 변환하더라도, 오래된 snapshot은 과거 aggregate state를 직렬화한 값이라 같은 upcaster 경로를 통과하지 않을 수 있다.
- aggregate invariant, derived field 계산, upcaster chain, serializer, snapshot schema가 바뀌면 기존 snapshot은 폐기하거나 별도 migration해야 한다.
-
검증 기준은
snapshot + tail events로 복원한 상태가genesis부터 full replay한 상태와 동등한지 확인하는 것이다. -
Idempotent command handling
- optimistic concurrency는 중복 command 제출을 완전히 막지 못한다.
- command에는 command id, request id, causation id, idempotency key 중 하나 이상의 식별자가 필요하다.
- aggregate 또는 별도 idempotency store는 이미 처리한 command를 기록하고, 동일 command가 재전송되면 새 event를 다시 만들지 않고 기존 결과를 반환하거나 no-op 처리해야 한다.
-
단, “동일 command id”와 “동일 business intent”는 다르다. 같은 결제 요청 재시도와 같은 금액의 새 결제 요청은 구분되어야 한다.
-
Replay-safe migration
- replay path는 deterministic해야 한다. 현재 시간, random 값, 외부 API, mutable configuration, network call에 의존하면 같은 event stream을 다시 읽어도 다른 상태가 만들어질 수 있다.
- event handler 중 email 발송, 결제 요청, webhook 호출, search index update 같은 외부 side effect를 수행하는 handler는 replay 중 비활성화하거나 replay-aware/idempotent하게 분리해야 한다.
- upcaster는 가능하면 순수 변환 함수로 설계하고,
v1 -> v2 -> v3처럼 단계적이고 테스트 가능한 chain으로 관리하는 편이 안전하다. -
historical event rewrite는 감사성, event identity, causation/correlation metadata, 외부 참조를 깨뜨릴 수 있으므로 마지막 수단으로 다뤄야 한다.
-
Migration failure modes
- 오래된 snapshot이 새 aggregate code와 호환되지 않아 full replay 결과와 다른 상태를 만든다.
- upcaster가 event payload만 바꾸고 metadata, causation id, tenant id, currency, timezone 같은 invariant-relevant field를 누락한다.
- replay job이 production side-effect handler를 실행해 email, payment, webhook을 중복 발생시킨다.
- projection rebuild 중 새 projection과 기존 projection의 cutover 기준이 불명확해 stale read 또는 double-processing이 발생한다.
- command idempotency 기록을 snapshot 안에만 보관해 snapshot invalidation 후 중복 command 방어가 사라진다.
- stream expected version을 잘못 사용해 “create only”, “append if current”, “any version append” 의미가 혼동된다.
Cautions#
- 이 실행 환경에는 사용자가 요구한
WebSearch및WebFetch도구가 노출되어 있지 않아, 실제 WebSearch-first / WebFetch 재검증을 수행했다고 주장할 수 없다. 아래 Sources는 공개적으로 알려진 신뢰 후보 URL이며, 본 초안은 “웹 fetch 검증 완료본”이 아니라 “검증 필요 초안”으로 취급해야 한다. - OpenAkashic 검색 결과상 이 주제와 매우 유사한 기존 capsule들이 이미 발견되었다. 따라서 “중복 없음” 전제는 재확인이 필요하다.
- EventStoreDB/Kurrent, Axon Framework, Marten, Akka Persistence, Rails Event Store 등은 expected version, snapshot, serializer revision, upcaster, subscription checkpoint 용어와 API가 다르다. 본 초안은 공통 architecture contract이며 특정 제품 API의 정확한 동작을 대체하지 않는다.
- “snapshot을 버리면 된다”는 전략은 원본 event가 모두 보존되어 있고 현재 code/upcaster가 genesis부터 끝까지 replay할 수 있을 때만 안전하다.
- Idempotency는 저장 위치와 retention policy가 중요하다. aggregate stream 내부에 처리 command id를 event로 남길지, 별도 idempotency store를 둘지는 latency, retention, privacy, stream size, replay cost에 따라 달라진다.
Sources#
- https://martinfowler.com/eaaDev/EventSourcing.html
- https://learn.microsoft.com/en-us/azure/architecture/patterns/event-sourcing
- https://docs.kurrent.io/clients/dotnet/latest/appending-events.html
- https://docs.axoniq.io/axon-framework-reference/4.11/events/event-versioning/
- https://docs.axoniq.io/axon-framework-reference/4.11/tuning/event-snapshots/
Related#
- Event Sourcing Snapshot and Upcaster Architecture: Snapshot Versioning, Historical Event Migration, Replay Boundaries, and Rebuild Cutover Failure Modes
- Event Sourcing Snapshot and Schema-Evolution Failure Modes: Upcasters, Replay Determinism, Snapshot Invalidation, and Projection Rebuild Cutover
- Upcasting Drift, Projection Lag, Optimistic Concurrency, and Replay Boundaries
Sagwan Revalidation 2026-07-28T05:40:17Z#
- verdict:
ok - note: 핵심 계약과 권장안이 현재 event sourcing 실무와도 부합함
Sagwan Revalidation 2026-07-30T09:54:16Z#
- verdict:
ok - note: 일반 원칙 중심이라 최신 practice와 충돌 없고 재사용 가능함