본문 바로가기

FrontEnd

Nest JS와 CQRS [CQRS Explained With Nest JS]

반응형

Nest JS와 함께 CQRS를 알아봅니다.

원문입니다 : https://medium.com/swlh/cqrs-explained-with-nest-js-1bcf83c5c839

 

CQRS Explained With Nest JS

We will be developing a simple CQRS app in NEST JS

medium.com

 

반응형 프로그래밍은 모바일, 웹 또는 백엔드 애플리케이션 개발 여부에 관계없이 점점 더 인기를 얻고 있습니다.
IT 업계가 고품질 제품을 제공하기 위해 채택한 변화에 따라
점점 더 많은 디자인 패턴이 진화하고 있습니다.

CQRS는 데이터베이스에 삽입 작업을 위한 별도의 서비스, 모델, 데이터베이스가 있는
마이크로서비스 아키텍처에서 사용되는 또 다른 디자인 패턴입니다.

CQRS는 Command Query Responsibility Segregation을 나타내는 디자인 패턴입니다.

환상적인 가능성을 가능하게 하는 단순한 패턴입니다.

Query 및 Command를 사용하여 애플리케이션의 읽기 부분과 쓰기 부분을 분리하기만 하면 됩니다.

CQRS에 익숙하지 않은 경우 아래 링크를 참조하십시오.
 
CQRS의 이점은 다음과 같습니다.
 
  • CQRS를 사용하면 읽기 및 쓰기 워크로드를 독립적으로 확장할 수 있으며 잠금(lock) 경합이 줄어들 수 있습니다.
  • 읽기 쪽은 쿼리에 최적화된 스키마를 사용할 수 있고 쓰기 쪽은 업데이트에 최적화된 스키마를 사용할 수 있습니다.
  • 올바른 도메인 엔터티만 데이터에 대한 쓰기를 수행하고 있는지 확인하는 것이 더 쉽습니다.
  • 읽기 및 쓰기 측면을 분리하면 유지 관리가 더 쉽고 유연한 모델이 될 수 있습니다.
    • 복잡한 비즈니스 로직의 대부분은 쓰기 모델로 들어갑니다.
    • 읽기 모델은 비교적 간단할 수 있습니다.
  • 구체화된(materialized) 뷰를 읽기 데이터베이스에 저장함으로써 애플리케이션은 쿼리할 때 복잡한 조인을 피할 수 있습니다.

좋아요, 이제 코딩해봅시다.

우리는 온라인 쇼핑을 사용 사례로 삼아,
주문 시나리오를 CQRS 방식으로 모듈화하는 방법을 알아보겠습니다.

다음은 시스템의 흐름 다이어그램입니다.

application flow
이 이벤트 중심 시스템은 측면 지향 프로그래밍 패러다임을 가능하게 합니다.
이것은 기본적으로 기존 기능을 변경하지 않고 소프트웨어에 추가 기능을 추가할 수 있음을 의미합니다.
이 경우 새로운 커맨드과 커맨드 핸들러를 이벤트와 연결하는 것을 의미합니다.
 
Nest JS는 CQRS를 기본적으로 지원하는 프레임워크 입니다.
따라서 Nest JS를 사용하겠습니다.
 
먼저 Nest JS가 아직 설치되지 않은 경우 설치해야 합니다.
설치를 위해 npm i -g @nestjs/cli 를 실행합니다.
 
이후 아래 스크립트를 사용해 새로운 프로젝트를 생성 후 실행합니다.
npm i -g @nestjs/cli
nest new simple-cqrs-app
cd simple-cqrs-app
npm run start

 

그런 다음 브라우저를 열고 http://localhost:3000/으로 이동합니다. "Hello World"라는 메시지가 표시됩니다.
프레임워크를 이용해 프로젝트 기본 설정을 완료했습니다.
 
src 디렉토리를 열고 주문 관련 항목을 넣을 폴더 "order"를 만듭니다.
다음 order.events.ts 파일을 만들고 아래와 같은 코드를 입력합니다.
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,
) { }
}
 
아시다시피 CQRS는 이벤트 기반 프로그래밍을 위해 개발되었기 때문에
애플리케이션 내의 모든 모듈은 이벤트 생성을 통해 통신합니다.
그래서 여기에서는 order 모듈과 관련한 3가지 가능한 이벤트를 작성했습니다.
 
다음 app.controller.ts를 열고 코드를 아래와 같이 바꿉니다.
 
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', };
  }
}​
QueryBus 및 EventBus는 Observable입니다.
이는 애플리케이션 모듈이 전역 스트림을 쉽게 구독할 수 있고 이벤트를 통해 모듈 간의 통신을 풍부하게 할 수 있음을 의미합니다.
 
사용자가 앱 컨트롤러에 도달하면 Order을 위한 새 이벤트가 아래와 같이 시작됩니다.
this.eventBus.publish(new OrderEvent(
orderTransactionGUID, 'Daniel Trimson', 'Samsung LED TV',    50000),);
 
이 이벤트가 시스템에서 어떻게 주문을 생성하는지 봅시다.
 
order 디렉토리로 이동하여 order.command.ts 파일을 만들고 아래 코드를 붙여넣습니다.

 

export class OrderCommand {
  constructor(
   public readonly orderTransactionGUID: string,
   public readonly orderUserGUID: string,
   public readonly orderItem: string,
   public readonly orderAmount: number,
 ) { }
}
 
애플리케이션을 더 쉽게 이해할 수 있도록
각 변경 사항 앞에 Command가 와야 합니다.
Command가 전달되면 애플리케이션은 이에 반응해야 합니다.
Command는 서비스에서(또는 컨트롤러/게이트웨이에서 직접) 디스패치하고 해당 Command 핸들러에서 사용할 수 있습니다.
 
여기에서 고객이 주문할 때 발송되어야 하는 커맨드를 정의했습니다.
시스템 내에서 어떻게 발송되는지 알아보겠습니다.
 
order.saga.ts 파일을 만들고 아래에 제공된 코드를 붙여넣습니다.
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);
  }),
  );
 }
}​
이러한 타입의 이벤트 기반 아키텍처(Event-Driven Architecture)는 애플리케이션 반응성 및 확장성을 향상시킵니다.
이제 이벤트가 발생하면 다양한 방식으로 이벤트에 대응할 수 있습니다.
Saga는 아키텍처 관점에서 볼 때 마지막 빌딩 블록입니다.
 
Saga는 Command를 반환하는 특별한 이벤트 핸들러로 생각할 수 있습니다.
Saga는 이벤트 버스에 게시된 모든 이벤트를 수신하고 반응하기 위해 RxJS를 활용하여 이를 수행합니다.
 
OrderEvent 사가에 의해  OrderCommand가 생성되고
이것은 Command 핸들러에 의해 처리됩니다.
 
 
order.handler.ts에 Command 핸들러를 생성합니다.
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();
  }
}
 
생성된 OrderCommand는 여기의 OrderHandler에 의해 처리됩니다.
주문에 성공하려면 OrderHandler는
CQRS 조건에 따라 order와과 직접적인 관계가 없는 Item 모델과 상호작용해야 합니다.
그렇다면 Item 모델과 이벤트 publisher를 어떻게 연결할까요?
Command 핸들러 내에서 publisher mergeObjectContext() 메서드를 사용해야 합니다.
(mergeObjectContext 메서드는 AggregateRoot 인스턴스를 인수로 받아 몇 가지 작업을 수행합니다.)
주 : EventPublisher클래스와 Item 모델의 apply 메소드 간의 관계를 만들어 줘야 합니다.
상세 설명 : https://docs.nestjs.com/recipes/cqrs#events
 
 
여기에서는 주문에 필요한 mergeObjectContext()의 파라미터로 사용할 Item을
ItemRepository의 인스턴스를 이용해 찾고 있습니다.
 
item.interface.ts, item.model.ts 및 item.repository.ts 파일이 있는 item 폴더를 만들어 보겠습니다.
 
item.interface.ts
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 [];
  }
}
 
따라서 ItemRepository는 OrderHandler에 대한 Item을 가져와 아래와 같이 주문을 완료합니다.
item.orderOnItem(orderTransactionGUID, orderUserGUID, orderAmount);
item.commit();
 
이제 모든 것이 예상대로 작동합니다.
이벤트가 즉시 전달되지 않기 때문에 commit() 이벤트가 필요함을 주목하세요.
따라서 commit()이 완료되면 orderOnItem() 메서드는 OrderEventSuccess 또는 OrderEventFail의 두 이벤트 중 하나를 생성합니다.
따라서 예상대로 주문 주기를 종료하려면 이벤트를 처리해야 합니다.
order.saga.ts로 이동하여 이 두 saga를 거기에 추가하십시오.
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 [];
      }),
    );
  };
}
 
따라서 주문이 성공하면 콘솔에 "Order Placed" 메시지가 표시되고,
그렇지 않으면 "Order Placing Failed"가 표시됩니다.
 
마지막으로 app.module.ts 파일로 이동하여 아래와 같이 업데이트합니다.
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

 

How to do a queries in nestjs using cqrs module?

I am using nestjs in my app backend.I use the cqrs module https://github.com/nestjs/cqrs, I read that cqrs have a commands to write operations and queries to read operation but nestjs documentation (

stackoverflow.com

 

참고

https://github.com/Sairyss/domain-driven-hexagon#adapters

 

GitHub - Sairyss/domain-driven-hexagon: Learn Domain-Driven Design, software architecture, design patterns, best practices. Code

Learn Domain-Driven Design, software architecture, design patterns, best practices. Code examples included - GitHub - Sairyss/domain-driven-hexagon: Learn Domain-Driven Design, software architectur...

github.com

https://wikidocs.net/158670

 

16장 CQRS를 이용한 관심사 분리

.

wikidocs.net

 

반응형