경계를 나누는 핵심 질문은 “테이블이 몇 개인가”가 아니라 “어떤 상태를 누가 최종적으로 바꾸며, 다른 상태와 언제까지 함께 맞아야 하는가”입니다. 즉시 원자성이 필요한 규칙과 지연·보상으로 허용할 수 있는 규칙을 먼저 구분한 뒤 CQRS나 Event Sourcing을 선택해야 읽기 모델 분리가 불필요한 분산 복잡성을 만들지 않습니다.
테이블 집합과 Service 경계의 불일치
주문·결제·재고가 즉시 함께 확정돼야 하는지, 재고 예약→결제 승인→확정과 만료/보상을 허용할지 먼저 정합니다. 후자는 process 분리를 가능하게 할 수 있지만 중간 사용자 상태·늦은 승인·응답 유실·보상 실패를 새로 관리해야 합니다.
앞서 정한 즉시 확정 또는 중간 상태 허용 여부를 바탕으로, 공동 변경·transaction·데이터 owner·보안/규제·확장 패턴을 기준으로 경계를 정합니다. 초기에는 같은 process의 강한 module/port 경계로 결합을 드러낼 수 있습니다. 로그인 한 번에 연쇄 RPC 열 번이나 매번 공동 배포가 필요하면 별도 DB를 가졌어도 독립성이 낮습니다. 필요하면 다시 합치는 것도 합리적입니다.
쓰기 원본과 읽기 표현의 분리
각 owner가 자기 데이터의 권위 쓰기를 맡고 다른 service는 API/event를 사용합니다. 직접 상대 DB schema를 읽으면 그 내부 변경에 결합됩니다. CQRS는 command와 query의 책임·model 분리이며 별도 DB·process·event sourcing을 강제하지 않습니다. 같은 DB에서 command invariant와 조회 DTO를 나누는 작은 형태도 가능합니다.
| 경계 | 책임 |
|---|---|
| command model | 인가·불변식·transaction·멱등 |
| query model | 화면 filter·집계·정렬·공개 필드 |
| 별도 projection | 중복·순서·삭제·재구축·lag |
| source owner | 원본 사실·write 권위·복구 |
재고 확정 command가 성공했는데 조회 projection이 아직 이전 값이면, 빠르게 읽힌 화면을 확정 상태의 근거로 쓰지 않습니다. 응답에 command가 확정한 값과 version을 함께 주거나, 조회가 그 최소 version을 적용할 때까지 기다린 뒤 결과를 반환하는 read-your-writes 계약을 선택합니다. 사용자가 화면을 저장 실패로 오해해 같은 요청을 다시 보내도 command의 안정 key를 다시 사용해 중복을 막습니다.
그림을 글로 읽기
- Command · 업무 조건·인가
- 권위 상태 또는 원본 이벤트
- 조회 projection·적용 위치
- Query · 화면 결과·version
- Command · 업무 조건·인가 → 권위 상태 또는 원본 이벤트: 원자적 기록
- 권위 상태 또는 원본 이벤트 → 조회 projection·적용 위치: 명시적 반영 계약
- 조회 projection·적용 위치 → Query · 화면 결과·version: 지연을 가진 조회
Event Sourcing과 역사 사실 보존 요건
Event Sourcing에서는 상태를 덮어쓴 결과 대신 상태 변화 event를 원본으로 저장하고, event를 순서대로 적용해 현재 상태를 다시 만듭니다. 예를 들어 ‘10 입금’은 중복 적용하면 10이 두 번 더해질 수 있지만 ‘잔액을 100으로 설정’은 중복 적용해도 같은 값입니다.
두 event의 적용 순서도 결과를 바꿀 수 있으므로, 각 event의 순서·중복 처리 의미를 따로 정해야 합니다. 당시 가격을 event에 남기지 않은 채 replay에서 현재 가격 API를 호출하면 같은 주문의 과거 상태가 달라집니다.
따라서 당시 결정을 재현하는 데 필요한 사실과 policy/code version, 오래된 schema를 현재 형태로 읽는 schema upcaster를 함께 정합니다.
snapshot은 특정 event 위치까지 반영한 파생 상태입니다. snapshot version·포함한 마지막 event와 그 이후 replay를 연결하고 중복·누락·순서 오류를 검사합니다. 원본 event가 틀렸다고 자동 정답으로 복구되지는 않습니다. 정정 event를 추가할 수 있으나 그 의미와 이후 상태 영향도 검토합니다.
Replay와 외부 효과 실행의 분리
projection 계산과 외부 effect handler를 분리하고 replay 환경에서 실제 egress·자격을 차단합니다. 단순 replay flag만 믿지 않고 실행 경계를 보호합니다. 민감정보 삭제와 불변 로그 보관은 충돌할 수 있어 민감 필드 분리·암호화·보존·삭제 정책을 처음부터 정해야 합니다. 로그에 저장됐다고 법적·업무 삭제 요구가 사라지지 않습니다.
분리 설계와 실제 운영 비용 비교
단일 model 기준선과 조회 지연·command 복잡성·projection lag·재구축 시간·공동 변경·RPC 왕복·보상·장애 전파·팀 owner를 비교합니다. 오래된 event·snapshot 연결·삭제·중복·외부 자료 변경·부분 실패를 시험합니다. 이 노트는 구조 설계이며 실제 event store migration이나 replay를 실행한 결과는 아닙니다.
선택을 진단하려면 하나의 주문 변경을 기준으로 command 확정 시각, projection 반영 시각, 사용자 조회 시각을 기록해 보십시오. command가 성공했는데 query가 이전 version을 반환하면 이는 원본 실패가 아니라 read model lag이며, 최소 version 대기나 확정 응답 사용으로 해결할 문제입니다. 반대로 replay 중 외부 결제가 다시 호출되면 projection과 effect handler의 실행 경계가 분리되지 않은 결함입니다.