TypeScript를 사용한 CQRS 및 이벤트 소싱
CQRS (명령 쿼리 책임 분리) e 이벤트 소싱 이는 두 가지 아키텍처 패턴으로 결합되어 근본적으로 다른 방식을 제공합니다. 디자인 소프트웨어 시스템. 동일한 데이터 모델을 읽고 쓰는 대신 CQRS 쓰기 작업(명령)과 읽기 작업(쿼리)을 구분합니다. 이벤트 소싱, 차례로 전통적인 상태 지속성을 순서가 지정된 시퀀스로 대체합니다. 시스템에서 발생한 모든 것을 설명하는 불변 이벤트입니다.
이 기사에서는 구현을 통해 두 패턴을 모두 심층적으로 살펴보겠습니다. TypeScript의 콘크리트. 설명하기 위해 주문 관리 시스템을 예로 사용하겠습니다. 명령, 쿼리, 이벤트 저장소, 프로젝션 및 스냅샷.
무엇을 배울 것인가
- CQRS 패턴: 명령과 쿼리 분리
- TypeScript의 명령 버스 및 쿼리 버스
- 이벤트 소싱: 이벤트 기반 지속성
- 이벤트 저장소 구현
- 예측 및 읽기 모델
- 성능 최적화를 위한 스냅샷
- CQRS 및 이벤트 소싱을 사용해야 하는 경우(및 피해야 하는 경우)
CQRS: 명령과 쿼리의 분리
CQRS의 원리는 간단합니다. 데이터를 쓰는 데 사용되는 모델 읽는 데 사용한 것과 동일할 필요는 없습니다.. 전통건축에서는 단일 모델(및 종종 단일 테이블)이 쓰기 및 쓰기 작업을 모두 처리합니다. 읽는 것. CQRS는 각각 최적화된 두 가지 모델을 도입합니다. 자신의 목적.
명령
Un 명령 시스템의 상태를 변경하려는 의도를 나타냅니다. 작업을 수행하는 데 필요한 모든 데이터를 포함하는 불변 개체입니다. 명령은 데이터를 반환하지 않습니다. 유일한 효과는 상태를 변경하는 것입니다.
// cqrs/command.ts
export interface Command {
readonly type: string;
}
export interface CommandHandler<T extends Command> {
execute(command: T): Promise<void>;
}
// commands/create-order.command.ts
export class CreateOrderCommand implements Command {
readonly type = 'CreateOrder';
constructor(
public readonly orderId: string,
public readonly customerId: string,
public readonly items: {
productId: string;
productName: string;
quantity: number;
unitPrice: number;
}[]
) {}
}
// commands/handlers/create-order.handler.ts
export class CreateOrderHandler
implements CommandHandler<CreateOrderCommand> {
constructor(private readonly eventStore: EventStore) {}
async execute(command: CreateOrderCommand): Promise<void> {
// Verifica che l'ordine non esista gia
const existing = await this.eventStore.getEvents(command.orderId);
if (existing.length > 0) {
throw new Error('Ordine già esistente');
}
// Crea l'aggregate e genera gli eventi
const order = Order.create(
command.orderId,
command.customerId,
command.items
);
// Salva gli eventi generati
const events = order.pullUncommittedEvents();
await this.eventStore.appendEvents(command.orderId, events, 0);
}
}
쿼리
에이 쿼리 데이터 요청을 나타냅니다. 상태는 변경되지 않습니다. 시스템을 실행하고 특정 사용 사례에 최적화된 결과를 반환합니다. 모델 (읽기 모델)은 정확하게 비정규화, 사전 컴파일 및 구조화될 수 있습니다. UI에서 필요에 따라.
// cqrs/query.ts
export interface Query {
readonly type: string;
}
export interface QueryHandler<TQuery extends Query, TResult> {
execute(query: TQuery): Promise<TResult>;
}
// queries/get-order-summary.query.ts
export class GetOrderSummaryQuery implements Query {
readonly type = 'GetOrderSummary';
constructor(public readonly orderId: string) {}
}
export interface OrderSummaryReadModel {
orderId: string;
customerName: string;
status: string;
itemCount: number;
totalAmount: number;
createdAt: string;
lastUpdatedAt: string;
}
// queries/handlers/get-order-summary.handler.ts
export class GetOrderSummaryHandler
implements QueryHandler<GetOrderSummaryQuery, OrderSummaryReadModel> {
constructor(private readonly readDb: ReadDatabase) {}
async execute(
query: GetOrderSummaryQuery
): Promise<OrderSummaryReadModel> {
const summary = await this.readDb.findOne<OrderSummaryReadModel>(
'order_summaries',
{ orderId: query.orderId }
);
if (!summary) {
throw new Error('Ordine non trovato');
}
return summary;
}
}
명령 버스 및 쿼리 버스
Il 버스 이는 명령/쿼리를 해당 처리기에 연결하는 메커니즘입니다. 이는 명령을 보내는 사람과 이를 관리하는 사람을 분리하는 중앙 집중식 파견자 역할을 합니다.
// cqrs/command-bus.ts
export class CommandBus {
private handlers = new Map<string, CommandHandler<any>>();
register<T extends Command>(
commandType: string,
handler: CommandHandler<T>
): void {
if (this.handlers.has(commandType)) {
throw new Error(`Handler già registrato per: ${commandType}`);
}
this.handlers.set(commandType, handler);
}
async dispatch(command: Command): Promise<void> {
const handler = this.handlers.get(command.type);
if (!handler) {
throw new Error(`Nessun handler per il comando: ${command.type}`);
}
await handler.execute(command);
}
}
// cqrs/query-bus.ts
export class QueryBus {
private handlers = new Map<string, QueryHandler<any, any>>();
register<TQuery extends Query, TResult>(
queryType: string,
handler: QueryHandler<TQuery, TResult>
): void {
this.handlers.set(queryType, handler);
}
async dispatch<TResult>(query: Query): Promise<TResult> {
const handler = this.handlers.get(query.type);
if (!handler) {
throw new Error(`Nessun handler per la query: ${query.type}`);
}
return handler.execute(query);
}
}
이벤트 소싱: 이벤트의 순서로서의 상태
이벤트 소싱은 지속성에 대한 전통적인 사고 방식을 근본적으로 바꿉니다. 대신에 저장해 현재 상태 엔터티의 각 항목을 저장합니다. 이벤트 그 상태로 이어진 것입니다. 현재 상태는 모든 것을 재생하여 얻습니다. 사건을 시간순으로.
이벤트 소싱의 장점
| 이점 | 설명 |
|---|---|
| 완전한 감사 추적 | 모든 변경 사항이 기록됩니다. 언제든지 상태를 재구성할 수 있습니다. |
| 고급 디버깅 | 이벤트를 재현함으로써 우리가 어떻게 현재 상태에 이르렀는지 정확히 이해할 수 있습니다. |
| 다중 투영 | 동일한 이벤트에서 다양한 요구에 맞게 다양한 읽기 모델을 구축할 수 있습니다. |
| 시간적 진화 | 역사적 사건을 재처리하여 새로운 예측을 소급하여 생성할 수 있습니다. |
| 데이터 손실 없음 | 이벤트는 변경할 수 없으며 추가만 가능합니다. 아무것도 삭제되지 않습니다 |
이벤트 소싱을 위한 도메인 이벤트
// events/base-event.ts
export interface DomainEvent {
readonly eventType: string;
readonly aggregateId: string;
readonly occurredOn: Date;
readonly version: number;
readonly payload: Record<string, unknown>;
}
// events/order-events.ts
export class OrderCreatedEvent implements DomainEvent {
readonly eventType = 'OrderCreated';
readonly occurredOn = new Date();
constructor(
public readonly aggregateId: string,
public readonly version: number,
public readonly payload: {
customerId: string;
items: {
productId: string;
productName: string;
quantity: number;
unitPrice: number;
}[];
}
) {}
}
export class OrderConfirmedEvent implements DomainEvent {
readonly eventType = 'OrderConfirmed';
readonly occurredOn = new Date();
constructor(
public readonly aggregateId: string,
public readonly version: number,
public readonly payload: {
confirmedAt: string;
totalAmount: number;
}
) {}
}
export class OrderItemAddedEvent implements DomainEvent {
readonly eventType = 'OrderItemAdded';
readonly occurredOn = new Date();
constructor(
public readonly aggregateId: string,
public readonly version: number,
public readonly payload: {
productId: string;
productName: string;
quantity: number;
unitPrice: number;
}
) {}
}
export class OrderCancelledEvent implements DomainEvent {
readonly eventType = 'OrderCancelled';
readonly occurredOn = new Date();
constructor(
public readonly aggregateId: string,
public readonly version: number,
public readonly payload: {
reason: string;
cancelledAt: string;
}
) {}
}
이벤트 소싱으로 집계
이벤트 소스 시스템에서 집계는 상태를 직접 유지하는 것이 아니라 상태를 유지합니다. 이벤트를 적용하여 재구성합니다. 모든 비즈니스 운영은 새로운 이벤트를 생성합니다. 이는 누적되어 이벤트 저장소에 유지됩니다.
// aggregates/order.aggregate.ts
export class Order {
private _id: string = '';
private _customerId: string = '';
private _status: OrderStatus = OrderStatus.CREATED;
private _items: OrderItem[] = [];
private _version: number = 0;
private _uncommittedEvents: DomainEvent[] = [];
// Factory method: crea un nuovo ordine
static create(
orderId: string,
customerId: string,
items: { productId: string; productName: string;
quantity: number; unitPrice: number }[]
): Order {
const order = new Order();
order.apply(new OrderCreatedEvent(orderId, 1, {
customerId,
items,
}));
return order;
}
// Ricostruzione da eventi storici
static fromHistory(events: DomainEvent[]): Order {
const order = new Order();
for (const event of events) {
order.applyFromHistory(event);
}
return order;
}
confirm(): void {
if (this._status !== OrderStatus.CREATED) {
throw new Error('Solo ordini in stato CREATED possono essere confermati');
}
if (this._items.length === 0) {
throw new Error('Impossibile confermare un ordine vuoto');
}
const total = this._items.reduce(
(sum, item) => sum + item.quantity * item.unitPrice, 0
);
this.apply(new OrderConfirmedEvent(this._id, this._version + 1, {
confirmedAt: new Date().toISOString(),
totalAmount: total,
}));
}
cancel(reason: string): void {
if (this._status === OrderStatus.SHIPPED) {
throw new Error('Impossibile cancellare un ordine già spedito');
}
this.apply(new OrderCancelledEvent(this._id, this._version + 1, {
reason,
cancelledAt: new Date().toISOString(),
}));
}
// Applica un nuovo evento (genera uncommitted event)
private apply(event: DomainEvent): void {
this.mutate(event);
this._uncommittedEvents.push(event);
}
// Applica un evento storico (senza generare uncommitted)
private applyFromHistory(event: DomainEvent): void {
this.mutate(event);
}
// Modifica lo stato interno in base all'evento
private mutate(event: DomainEvent): void {
switch (event.eventType) {
case 'OrderCreated':
this._id = event.aggregateId;
this._customerId = (event.payload as any).customerId;
this._items = (event.payload as any).items.map((i: any) => ({
productId: i.productId,
productName: i.productName,
quantity: i.quantity,
unitPrice: i.unitPrice,
}));
this._status = OrderStatus.CREATED;
break;
case 'OrderConfirmed':
this._status = OrderStatus.CONFIRMED;
break;
case 'OrderItemAdded':
this._items.push(event.payload as any);
break;
case 'OrderCancelled':
this._status = OrderStatus.CANCELLED;
break;
}
this._version = event.version;
}
pullUncommittedEvents(): DomainEvent[] {
const events = [...this._uncommittedEvents];
this._uncommittedEvents = [];
return events;
}
get id(): string { return this._id; }
get version(): number { return this._version; }
get status(): OrderStatus { return this._status; }
}
이벤트 매장
L'이벤트 매장 이벤트 데이터베이스입니다. 각 이벤트 스트림은 집계에. 두 가지 기본 작업이 있습니다. 새 이벤트를 걸고 읽는 것입니다. 스트림의 이벤트. 이벤트 스토어는 주문을 보장하고 낙관적인 지원을 지원합니다. 버전 관리를 통한 동시성.
// infrastructure/event-store.ts
export interface EventStore {
appendEvents(
streamId: string,
events: DomainEvent[],
expectedVersion: number
): Promise<void>;
getEvents(streamId: string): Promise<DomainEvent[]>;
getEventsFromVersion(
streamId: string,
fromVersion: number
): Promise<DomainEvent[]>;
getAllEvents(): Promise<DomainEvent[]>;
}
export class InMemoryEventStore implements EventStore {
private streams = new Map<string, DomainEvent[]>();
private allEvents: DomainEvent[] = [];
private subscribers: ((event: DomainEvent) => void)[] = [];
async appendEvents(
streamId: string,
events: DomainEvent[],
expectedVersion: number
): Promise<void> {
const currentEvents = this.streams.get(streamId) || [];
const currentVersion = currentEvents.length > 0
? currentEvents[currentEvents.length - 1].version
: 0;
// Optimistic concurrency check
if (currentVersion !== expectedVersion) {
throw new ConcurrencyError(
`Conflitto di concorrenza: versione attesa ${expectedVersion}, ` +
`versione corrente ${currentVersion}`
);
}
const updatedStream = [...currentEvents, ...events];
this.streams.set(streamId, updatedStream);
this.allEvents.push(...events);
// Notifica i subscriber (per le proiezioni)
for (const event of events) {
this.subscribers.forEach(sub => sub(event));
}
}
async getEvents(streamId: string): Promise<DomainEvent[]> {
return this.streams.get(streamId) || [];
}
async getEventsFromVersion(
streamId: string,
fromVersion: number
): Promise<DomainEvent[]> {
const events = this.streams.get(streamId) || [];
return events.filter(e => e.version > fromVersion);
}
async getAllEvents(): Promise<DomainEvent[]> {
return [...this.allEvents];
}
subscribe(handler: (event: DomainEvent) => void): void {
this.subscribers.push(handler);
}
}
예측 및 읽기 모델
Le 투영 그것은 사건을 다음으로 변환하는 메커니즘이다. 모델 읽기 독서에 최적화되어 있습니다. 각 프로젝션은 세부 사항에 귀를 기울입니다. 이벤트 유형을 지정하고 비정규화된 읽기 모델을 업데이트합니다. 이 모델은 생각됩니다 가능한 한 효율적으로 쿼리에 응답합니다.
// projections/order-summary.projection.ts
export interface OrderSummaryReadModel {
orderId: string;
customerId: string;
status: string;
itemCount: number;
totalAmount: number;
createdAt: string;
updatedAt: string;
}
export class OrderSummaryProjection {
private summaries = new Map<string, OrderSummaryReadModel>();
constructor(eventStore: EventStore) {
// Sottoscrizione agli eventi rilevanti
eventStore.subscribe((event) => this.handleEvent(event));
}
private handleEvent(event: DomainEvent): void {
switch (event.eventType) {
case 'OrderCreated':
this.onOrderCreated(event);
break;
case 'OrderConfirmed':
this.onOrderConfirmed(event);
break;
case 'OrderItemAdded':
this.onOrderItemAdded(event);
break;
case 'OrderCancelled':
this.onOrderCancelled(event);
break;
}
}
private onOrderCreated(event: DomainEvent): void {
const payload = event.payload as any;
const items = payload.items || [];
const totalAmount = items.reduce(
(sum: number, i: any) => sum + i.quantity * i.unitPrice, 0
);
this.summaries.set(event.aggregateId, {
orderId: event.aggregateId,
customerId: payload.customerId,
status: 'created',
itemCount: items.length,
totalAmount,
createdAt: event.occurredOn.toISOString(),
updatedAt: event.occurredOn.toISOString(),
});
}
private onOrderConfirmed(event: DomainEvent): void {
const summary = this.summaries.get(event.aggregateId);
if (summary) {
summary.status = 'confirmed';
summary.totalAmount = (event.payload as any).totalAmount;
summary.updatedAt = event.occurredOn.toISOString();
}
}
private onOrderItemAdded(event: DomainEvent): void {
const summary = this.summaries.get(event.aggregateId);
if (summary) {
const payload = event.payload as any;
summary.itemCount += 1;
summary.totalAmount += payload.quantity * payload.unitPrice;
summary.updatedAt = event.occurredOn.toISOString();
}
}
private onOrderCancelled(event: DomainEvent): void {
const summary = this.summaries.get(event.aggregateId);
if (summary) {
summary.status = 'cancelled';
summary.updatedAt = event.occurredOn.toISOString();
}
}
// Query methods
getById(orderId: string): OrderSummaryReadModel | undefined {
return this.summaries.get(orderId);
}
getByCustomer(customerId: string): OrderSummaryReadModel[] {
return Array.from(this.summaries.values())
.filter(s => s.customerId === customerId);
}
getByStatus(status: string): OrderSummaryReadModel[] {
return Array.from(this.summaries.values())
.filter(s => s.status === status);
}
}
스냅샷: 성능 최적화
시간이 지남에 따라 집계에 대한 이벤트 수가 크게 늘어날 수 있습니다. 수천 건의 이벤트를 통해 상태를 재건하는 데 비용이 많이 듭니다. 그만큼 스냅 사진 그들은 집계 상태의 스냅샷을 주기적으로 저장하여 이 문제를 해결합니다. 재구성은 가장 최근 스냅샷부터 시작되며 후속 이벤트만 적용됩니다.
// infrastructure/snapshot-store.ts
export interface Snapshot {
aggregateId: string;
version: number;
state: Record<string, unknown>;
createdAt: Date;
}
export interface SnapshotStore {
save(snapshot: Snapshot): Promise<void>;
getLatest(aggregateId: string): Promise<Snapshot | null>;
}
export class InMemorySnapshotStore implements SnapshotStore {
private snapshots = new Map<string, Snapshot>();
async save(snapshot: Snapshot): Promise<void> {
this.snapshots.set(snapshot.aggregateId, snapshot);
}
async getLatest(aggregateId: string): Promise<Snapshot | null> {
return this.snapshots.get(aggregateId) || null;
}
}
// repository/order.repository.ts
export class EventSourcedOrderRepository {
private static readonly SNAPSHOT_INTERVAL = 50;
constructor(
private readonly eventStore: EventStore,
private readonly snapshotStore: SnapshotStore
) {}
async getById(orderId: string): Promise<Order> {
// 1. Cerca lo snapshot più recente
const snapshot = await this.snapshotStore.getLatest(orderId);
let events: DomainEvent[];
let order: Order;
if (snapshot) {
// 2a. Ricostruisci dallo snapshot + eventi successivi
order = Order.fromSnapshot(snapshot.state);
events = await this.eventStore.getEventsFromVersion(
orderId,
snapshot.version
);
} else {
// 2b. Ricostruisci da tutti gli eventi
events = await this.eventStore.getEvents(orderId);
order = Order.fromHistory(events);
return order;
}
// 3. Applica gli eventi mancanti
for (const event of events) {
order.applyFromHistory(event);
}
return order;
}
async save(order: Order): Promise<void> {
const uncommitted = order.pullUncommittedEvents();
const expectedVersion = order.version - uncommitted.length;
await this.eventStore.appendEvents(
order.id,
uncommitted,
expectedVersion
);
// Crea snapshot se necessario
if (order.version % EventSourcedOrderRepository.SNAPSHOT_INTERVAL === 0) {
await this.snapshotStore.save({
aggregateId: order.id,
version: order.version,
state: order.toSnapshot(),
createdAt: new Date(),
});
}
}
}
전체 흐름: 명령부터 쿼리까지
CQRS + 이벤트 소싱 시스템의 전체 작업 흐름을 살펴보겠습니다. 업데이트된 읽기 모델에 대한 쿼리가 완료될 때까지 명령을 수신합니다.
// app/order-service.ts
export class OrderService {
constructor(
private readonly commandBus: CommandBus,
private readonly queryBus: QueryBus
) {}
// Lato scrittura: invia un comando
async createOrder(
customerId: string,
items: { productId: string; productName: string;
quantity: number; unitPrice: number }[]
): Promise<string> {
const orderId = crypto.randomUUID();
await this.commandBus.dispatch(
new CreateOrderCommand(orderId, customerId, items)
);
return orderId;
}
async confirmOrder(orderId: string): Promise<void> {
await this.commandBus.dispatch(
new ConfirmOrderCommand(orderId)
);
}
// Lato lettura: esegui una query
async getOrderSummary(orderId: string): Promise<OrderSummaryReadModel> {
return this.queryBus.dispatch<OrderSummaryReadModel>(
new GetOrderSummaryQuery(orderId)
);
}
async getCustomerOrders(
customerId: string
): Promise<OrderSummaryReadModel[]> {
return this.queryBus.dispatch<OrderSummaryReadModel[]>(
new GetCustomerOrdersQuery(customerId)
);
}
}
// Configurazione iniziale
function bootstrap(): OrderService {
// Event Store
const eventStore = new InMemoryEventStore();
const snapshotStore = new InMemorySnapshotStore();
const orderRepo = new EventSourcedOrderRepository(eventStore, snapshotStore);
// Proiezioni
const orderSummaryProjection = new OrderSummaryProjection(eventStore);
const readDb = new InMemoryReadDatabase(orderSummaryProjection);
// Command Bus
const commandBus = new CommandBus();
commandBus.register('CreateOrder', new CreateOrderHandler(eventStore));
commandBus.register('ConfirmOrder', new ConfirmOrderHandler(orderRepo));
// Query Bus
const queryBus = new QueryBus();
queryBus.register('GetOrderSummary', new GetOrderSummaryHandler(readDb));
queryBus.register('GetCustomerOrders', new GetCustomerOrdersHandler(readDb));
return new OrderService(commandBus, queryBus);
}
이벤트 소싱이 없는 CQRS
그 점을 강조하는 것이 중요하다 CQRS와 이벤트 소싱은 독립적인 패턴입니다.. 이벤트 소싱 없이 CQRS를 채택하여 기존 데이터베이스를 유지 관리할 수 있습니다. 동기식 또는 비동기식 프로젝션을 통해 별도의 읽기 모델을 작성하고 생성합니다.
| 접근하다 | 복잡성 | 장점 | 제한 사항 |
|---|---|---|---|
| 단순 CQRS | 평균 | 최적화된 읽기 모델, 명확한 분리 | 기본 감사 추적 없음, 일관성 가능 |
| CQRS + 이벤트 소싱 | 높은 | 감사 추적, 상태 재구성, 다중 예측 | 관리 복잡성, 학습 곡선 |
| 전통 건축 | 낮은 | 간단하고 친숙하며 성숙한 도구 | 동일한 모델에서 읽고 쓰기 때문에 유연성이 떨어집니다. |
CQRS 및 이벤트 소싱을 사용하는 경우
이상적인 시나리오
- 감사 추적 요구 사항: 규제 부문(금융, 의료, 규정 준수)
- 비대칭 읽기 및 쓰기: 글보다 읽기가 더 많음(또는 그 반대)
- 다중 투영: 매우 다양한 형식으로 읽어야 하는 데이터
- 협업 시스템: 여러 사용자가 동시에 동일한 데이터를 편집합니다.
- 이벤트 중심 아키텍처: 이벤트를 통해 소통하는 시스템과의 통합
피해야 할 때
- 간단한 CRUD 애플리케이션: 추가된 복잡성은 이점을 정당화하지 않습니다.
- 경험이 없는 소규모 팀: 학습 곡선이 중요합니다
- 강력한 일관성 요구 사항: CQRS는 읽기와 쓰기 간의 일관성을 도입합니다.
- 프로토타입 및 MVP: 확장성보다 초기 개발 속도가 더 중요
과제 및 고려 사항
| 도전 | 설명 | 완화 전략 |
|---|---|---|
| 가능한 일관성 | 읽기 모델은 마지막 쓰기를 즉시 반영하지 않을 수 있습니다. | 낙관적 UI, 폴링, 실시간 알림 |
| 이벤트의 진화 | 사건의 구조는 시간에 따라 변한다 | 이벤트 버전 관리, 업캐스팅, 스키마 레지스트리 |
| 예측 재구성 | 수백만 개의 이벤트를 재처리하는 데 시간이 걸릴 수 있음 | 스냅샷, 병렬 처리, 증분 예측 |
| 운영 복잡성 | 모니터링하고 유지 관리할 구성 요소가 더 많습니다. | 관측 가능성, 구조화된 로깅, 상태 확인 |
결론
CQRS 및 이벤트 소싱은 특정 규모 문제를 해결하는 강력한 도구입니다. 감사와 유연성. CQRS는 읽기 및 쓰기 패턴을 분리하여 다음을 수행할 수 있습니다. 각각을 독립적으로 최적화합니다. 이벤트 소싱은 지속성을 불변의 사실 기록으로 시간 이동 디버깅과 같은 가능성을 열어줍니다. 소급 예측 및 기본 감사 추적.
다른 고급 패턴과 마찬가지로 상황에 대한 신중한 평가가 필요합니다. 나는 아니다 모든 아키텍처 문제에 대한 솔루션이지만 올바른 시나리오(복잡한 시스템, 이벤트 중심 도메인, 규정 준수 요구 사항 - 아키텍처가 제공하는 이점 전통적인 것은 일치할 수 없습니다. 핵심은 간단한 형식의 CQRS로 시작하는 것입니다. 부가가치가 명확하고 측정 가능한 경우에만 이벤트 소싱을 도입하세요.







