NestJS CQRS

명령은 Aggregate를 바꾸고, 조회는 Projection을 읽는다

CQRS의 핵심은 handler 파일을 늘리는 것이 아니라 변경 의도, 도메인 이벤트, 읽기 모델 갱신을 서로 다른 책임 경계로 두는 것이다. 쓰기는 즉시 일관성을 지키고, 조회는 허용된 지연 안에서 따라온다.

명령에서 조회 모델까지의 책임 지도

command -> aggregate -> event -> projection -> query

빨간 경로는 상태 변경, 초록 경로는 조회 모델 갱신, 파란 경로는 읽기 요청이다. 가운데 비동기 경계가 consistency lag를 만드는 지점이다.

CQRS 명령과 조회 모델 분리 흐름 Command가 CommandBus와 CommandHandler를 지나 Aggregate를 변경하고 Domain Event를 만든 뒤, Projection이 Read Model을 갱신하며 QueryHandler는 Read Model만 조회한다. Write side 변경 규칙과 트랜잭션 경계 Read side 조회 최적화와 projection 경계 async boundary Command 변경 의도 Handler 권한, 유효성 Aggregate 불변식 적용 Domain Event outbox / stream Projection event handler Read Model denormalized QueryBus 읽기 요청 Query read only consistency lag command 실패는 write side에서 멈춘다 projection 실패는 read lag로 격리한다
command CommandBus -> CommandHandler

사용자의 변경 의도를 검증하고 aggregate 호출 전 권한과 입력을 확정한다.

aggregate Aggregate는 불변식을 적용한다

쓰기 모델의 트랜잭션 안에서 상태 변경과 domain event 생성을 끝낸다.

event Domain Event가 비동기 경계를 지난다

outbox나 event stream이 projection 재처리와 장애 분리의 기준점이 된다.

projection Projection이 read model을 갱신한다

조회 최적화 구조로 데이터를 펴되 지연 시간과 재생 가능성을 기록한다.

query QueryHandler는 read model만 읽는다

조회 요청은 aggregate를 우회하고 허용된 consistency lag 안에서 응답한다.

쓰기 모델과 읽기 모델의 경계

responsibility split
write model Command는 결과 화면을 만들지 않는다

상태 변경, aggregate 불변식, event 발행을 한 트랜잭션 경계로 묶는다.

read model Query는 쓰기 저장소를 추론하지 않는다

화면에 필요한 형태로 projection된 모델을 읽고 부작용 없이 끝낸다.

침범 신호
CommandHandler가 목록 DTO를 조립하거나 QueryHandler가 aggregate method를 호출한다.
수정 방향
event 계약을 기준으로 projection을 만들고, query 응답은 read model schema에 맞춘다.

Consistency lag를 관측하는 방식

eventual consistency
event committed projection pending query visible
지연은 버그가 아니라 계약이다

read model이 늦게 보일 수 있음을 SLA로 정하고, eventId, aggregateId, projectedAt을 로그에 남긴다.

재처리 가능해야 장애가 분리된다

projection handler 실패는 command 재실행이 아니라 event replay와 dead-letter 처리로 복구한다.

장애를 어느 경계에서 분리할지 결정한다

same symptom, different boundary
command reject

권한, 입력, aggregate 불변식 실패는 event를 만들지 않고 즉시 실패 응답으로 끝낸다.

event publish gap

DB 변경과 event 발행 사이의 공백은 outbox 저장 여부와 재발행 로그로 확인한다.

projection lag

쓰기 성공 뒤 조회가 늦으면 event handler 지연, 큐 적체, read DB 반영 시간을 분리한다.

stale query

오래된 read model을 반환할 때는 commandId가 아니라 projectedAt과 model version으로 추적한다.

검증 증거

CQRS는 흐름을 추적할 수 있을 때 의미가 있다.

  • CommandHandler 테스트가 aggregate 규칙과 event 생성을 함께 검증하는가?
  • Projection은 같은 event를 재생해도 같은 read model을 만드는가?
  • QueryHandler 응답에 stale 허용 시간과 model version을 설명할 수 있는가?