Nest JS와 함께 CQRS를 알아봅니다.
원문입니다 : https://medium.com/swlh/cqrs-explained-with-nest-js-1bcf83c5c839
반응형 프로그래밍은 모바일, 웹 또는 백엔드 애플리케이션 개발 여부에 관계없이 점점 더 인기를 얻고 있습니다.
IT 업계가 고품질 제품을 제공하기 위해 채택한 변화에 따라
점점 더 많은 디자인 패턴이 진화하고 있습니다.
CQRS는 데이터베이스에 삽입 작업을 위한 별도의 서비스, 모델, 데이터베이스가 있는
마이크로서비스 아키텍처에서 사용되는 또 다른 디자인 패턴입니다.
CQRS는 Command Query Responsibility Segregation을 나타내는 디자인 패턴입니다.
환상적인 가능성을 가능하게 하는 단순한 패턴입니다.
Query 및 Command를 사용하여 애플리케이션의 읽기 부분과 쓰기 부분을 분리하기만 하면 됩니다.
- CQRS를 사용하면 읽기 및 쓰기 워크로드를 독립적으로 확장할 수 있으며 잠금(lock) 경합이 줄어들 수 있습니다.
- 읽기 쪽은 쿼리에 최적화된 스키마를 사용할 수 있고 쓰기 쪽은 업데이트에 최적화된 스키마를 사용할 수 있습니다.
- 올바른 도메인 엔터티만 데이터에 대한 쓰기를 수행하고 있는지 확인하는 것이 더 쉽습니다.
- 읽기 및 쓰기 측면을 분리하면 유지 관리가 더 쉽고 유연한 모델이 될 수 있습니다.
- 복잡한 비즈니스 로직의 대부분은 쓰기 모델로 들어갑니다.
- 읽기 모델은 비교적 간단할 수 있습니다.
- 구체화된(materialized) 뷰를 읽기 데이터베이스에 저장함으로써 애플리케이션은 쿼리할 때 복잡한 조인을 피할 수 있습니다.
좋아요, 이제 코딩해봅시다.
우리는 온라인 쇼핑을 사용 사례로 삼아,
주문 시나리오를 CQRS 방식으로 모듈화하는 방법을 알아보겠습니다.
다음은 시스템의 흐름 다이어그램입니다.
npm i -g @nestjs/cli
nest new simple-cqrs-app
cd simple-cqrs-app
npm run start
export class OrderEvent {
constructor(
public readonly orderTransactionGUID: string,
public readonly orderUser: string,
public readonly orderItem: string,
public readonly orderAmount: number,
) { }
}
export class OrderEventSuccess {
constructor(
public readonly orderTransactionGUID: string,
public readonly orderItem: string,
public readonly orderAmount: number,
public readonly user: { email: string, id: string },
) { }
}
export class OrderEventFail {
constructor(
public readonly orderTransactionGUID: string,
public readonly error: object,
) { }
}
import { Controller, Get } from '@nestjs/common';
import { EventBus, QueryBus } from '@nestjs/cqrs';
import * as uuid from 'uuid';
import { OrderEvent } from './order/order.events';
@Controller()
export class AppController {
constructor(private readonly eventBus: EventBus,
private queryBus: QueryBus) { }
@Get()
async bid(): Promise<object> {
const orderTransactionGUID = uuid.v4();
// We are hard-coding values here
// instead of collecting them from a request
this.eventBus.publish(
new OrderEvent(
orderTransactionGUID, 'Daniel Trimson', 'Samsung LED TV', 50000),);
return { status: 'PENDING', };
}
}
this.eventBus.publish(new OrderEvent(
orderTransactionGUID, 'Daniel Trimson', 'Samsung LED TV', 50000),);
export class OrderCommand {
constructor(
public readonly orderTransactionGUID: string,
public readonly orderUserGUID: string,
public readonly orderItem: string,
public readonly orderAmount: number,
) { }
}
import { Injectable } from '@nestjs/common';
import { ICommand, ofType, Saga } from '@nestjs/cqrs';
import { Observable } from 'rxjs';
import { flatMap, map } from 'rxjs/operators';
import { OrderEvent, OrderEventSuccess } from './order.events';
import { OrderCommand } from './order.command';
@Injectable()
export class OrderSaga {
@Saga()
createOrder = (events$: Observable<any>): Observable<ICommand> => {
return events$.pipe(
ofType(OrderEvent),
map((event: OrderEvent) => {
return new OrderCommand(event.orderTransactionGUID, event.orderUser, event.orderItem, event.orderAmount);
}),
);
}
}
import { CommandHandler, EventPublisher, ICommandHandler } from '@nestjs/cqrs';
import { OrderCommand } from './order.commands';
import { ItemRepository } from 'src/item/item.repository';
@CommandHandler(OrderCommand)
export class OrderHandler implements ICommandHandler<OrderCommand> {
constructor(
private readonly itemRepository: ItemRepository,
private readonly publisher: EventPublisher,
) {}
async execute(command: OrderCommand) {
const {
orderTransactionGUID,
orderAmount,
orderItem,
orderUserGUID,
} = command;
// tslint:disable-next-line:no-console
console.log(
`Make a bid on ${orderItem}, with userID: ${orderUserGUID} amount: ${orderAmount}`,
);
// to associate model ( Order ) and publisher, we use code bellow
const item = this.publisher.mergeObjectContext(
await this.itemRepository.getItemById(orderItem),
);
item.orderOnItem(orderTransactionGUID, orderUserGUID, orderAmount);
item.commit();
}
}
주 : EventPublisher클래스와 Item 모델의 apply 메소드 간의 관계를 만들어 줘야 합니다.
상세 설명 : https://docs.nestjs.com/recipes/cqrs#events
export interface IItemInterface {
id: string;
amount?: number;
}
item.model.ts
import { AggregateRoot } from '@nestjs/cqrs';
import { IItemInterface } from './item.interface';
import { OrderEventSuccess, OrderEventFail } from '../order/order.events';
export class ItemModel extends AggregateRoot {
constructor(private readonly item: IItemInterface) {
super();
}
orderOnItem(orderTransactionGUID: string, userID: string, amount: number) {
// validation and etc.
try {
// business logic
// upon successful order, dispatch new event
this.apply(
new OrderEventSuccess(orderTransactionGUID, this.item.id, amount, {
email: 'fake@email.com',
id: userID,
}),
);
} catch (e) {
// dispatch order event fail action
this.apply(new OrderEventFail(orderTransactionGUID, e));
}
}
}
item.repository.ts
import { Injectable } from '@nestjs/common';
import { IItemInterface } from './item.interface';
import { ItemModel } from './item.model';
@Injectable()
export class ItemRepository {
async getItemById(id: string) {
// fetch it from database for example
const item: IItemInterface = {
id,
amount: 50000,
};
return new ItemModel(item);
}
async getAll() {
return [];
}
}
item.orderOnItem(orderTransactionGUID, orderUserGUID, orderAmount);
item.commit();
import { Injectable } from '@nestjs/common';
import { ICommand, ofType, Saga } from '@nestjs/cqrs';
import { Observable } from 'rxjs';
import { flatMap, map } from 'rxjs/operators';
import { OrderEvent, OrderEventSuccess, OrderEventFail } from './order.events';
import { OrderCommand } from './order.commands';
@Injectable()
export class OrderSaga {
@Saga()
createOrder = (events$: Observable<any>): Observable<ICommand> => {
return events$.pipe(
ofType(OrderEvent),
map((event: OrderEvent) => {
return new OrderCommand(
event.orderTransactionGUID,
event.orderUser,
event.orderItem,
event.orderAmount,
);
}),
);
};
@Saga()
createOrderSuccess = (events$: Observable<any>): Observable<ICommand> => {
return events$.pipe(
ofType(OrderEventSuccess),
flatMap((event: OrderEventSuccess) => {
// tslint:disable-next-line:no-console
console.log('Order Placed');
return [];
}),
);
};
@Saga()
createOrderFail = (events$: Observable<any>): Observable<ICommand> => {
return events$.pipe(
ofType(OrderEventFail),
flatMap((event: OrderEventFail) => {
// tslint:disable-next-line:no-console
console.log('Order Placing Failed');
return [];
}),
);
};
}
import { Module } from '@nestjs/common';
import { CqrsModule } from '@nestjs/cqrs';
import { AppController } from './app.controller';
import { OrderHandler } from './order/order.handler';
import { OrderSaga } from './order/order.saga';
import { ItemRepository } from './item/item.repository';
@Module({
imports: [CqrsModule],
controllers: [AppController],
providers: [OrderHandler, OrderSaga, ItemRepository],
})
export class AppModule {}
이제 CQRS 앱을 만들었습니다!
메인 화면 접속 시 화면에 Samsung TV, 콘솔에 주문 목록이 찍히는지 확인하세요!
전체 코드는 here 에서 확인하세요!
해당 예제에서는 커맨드만 다루었지만,
쿼리는 execute를 사용하며,
publlish와 saga를 사용하지 않는다는 것만 빼면 동일합니다.
https://stackoverflow.com/questions/51909358/how-to-do-a-queries-in-nestjs-using-cqrs-module
참고
https://github.com/Sairyss/domain-driven-hexagon#adapters
'FrontEnd' 카테고리의 다른 글
[1일 1 알고리즘] 프론트엔드 JS 알고리즘 문제풀이 : 배열 평탄화(flatten) (0) | 2022.10.25 |
---|---|
헥사고날 아키텍처와 관심사의 분리를 이용한 클린 코드 (0) | 2022.10.24 |
이벤트 루프에 대한 이해도를 파악할 수 있는 면접질문 (0) | 2022.10.23 |
리액트의 의존성 주입 [NestJs의 모듈로 살펴보는] (0) | 2022.10.23 |
의존성 역전 원칙과 NestJS(Dependency Inversion Principle with NestJS) (0) | 2022.10.22 |