CQRS
가장 간단한 CRUD 응용 프로그램의 흐름은 다음 단계를 사용하여 설명할 수 있습니다.
컨트롤러 계층은 HTTP 요청 을 처리하고 작업을 서비스에 위임합니다.
서비스 계층은 대부분의 비즈니스 로직이 수행되는 장소입니다.
서비스 는 저장소 /DAO를 사용하여 엔티티를 변경/지속합니다.
엔티티는 setter 및 getter와 함께 값의 컨테이너 역할을합니다.
대부분의 경우 중소 규모의 응용 프로그램을 더 복잡하게 만들 이유가 없습니다. 그러나 때로는 충분하지 않으며 우리의 요구가 더 정교 해지고 있을 때 데이터 흐름이 간단한 확장 가능한 시스템을 원합니다.
따라서 아래에서 설명하는 요소인 경량 CQRS 모듈 을 제공합니다.
Commands
응용 프로그램을 이해하기 쉽게하려면 각 변경 사항 앞에 Command 가 있어야합니다. 명령이 전달될 때 응용 프로그램이 응답해야 합니다. 명령은 서비스 (또는 컨트롤러/게이트웨이에서 직접)에서 발송되고 해당하는 명령 처리기 에서 사용될 수 있습니다.
Copy @@ filename (heroes - game .service)
@ Injectable ()
export class HeroesGameService {
constructor ( private readonly commandBus : CommandBus ) {}
async killDragon (heroId : string , killDragonDto : KillDragonDto ) {
return this . commandBus .execute (
new KillDragonCommand (heroId , killDragonDto .dragonId)
);
}
}
@@switch
@ Injectable ()
@ Dependencies (CommandBus)
export class HeroesGameService {
constructor (commandBus) {
this .commandBus = commandBus;
}
async killDragon (heroId , killDragonDto) {
return this . commandBus .execute (
new KillDragonCommand (heroId , killDragonDto .dragonId)
);
}
}
다음은 KillDragonCommand
를 전달하는 샘플 서비스입니다. 명령이 어떻게 보이는지 봅시다:
Copy @@ filename (kill - dragon .command)
export class KillDragonCommand {
constructor (
public readonly heroId : string ,
public readonly dragonId : string ,
) {}
}
@@switch
export class KillDragonCommand {
constructor (heroId , dragonId) {
this .heroId = heroId;
this .dragonId = dragonId;
}
}
CommandBus
는 스트림 명령입니다. 동등한 핸들러에 명령을 위임합니다. 각 명령에는 해당 명령 처리기 가 있어야합니다.
Copy @@ filename (kill - dragon .handler)
@ CommandHandler (KillDragonCommand)
export class KillDragonHandler implements ICommandHandler < KillDragonCommand > {
constructor ( private readonly repository : HeroRepository ) {}
async execute (command : KillDragonCommand ) {
const { heroId , dragonId } = command;
const hero = this . repository .findOneById ( + heroId);
hero .killEnemy (dragonId);
await this . repository .persist (hero);
}
}
@@switch
@ CommandHandler (KillDragonCommand)
@ Dependencies (HeroRepository)
export class KillDragonHandler {
constructor (repository) {
this .repository = repository;
}
async execute (command) {
const { heroId , dragonId } = command;
const hero = this . repository .findOneById ( + heroId);
hero .killEnemy (dragonId);
await this . repository .persist (hero);
}
}
이제 모든 애플리케이션 상태 변경은 Command 발생의 결과입니다. 로직은 핸들러에 캡슐화됩니다. 원하는 경우 여기에 로깅을 추가하거나 그 이상을 추가할 수 있으며 데이터베이스에 명령을 유지할 수 있습니다 (예: 진단 목적).
Events
처리기에 명령을 캡슐화했기 때문에 명령 구조 간의 상호 작용을 막을 수 있습니다. 응용 프로그램 구조는 응답 이 아니라 유연하지 않습니다. 해결책은 이벤트 를 사용하는 것입니다.
Copy @@ filename (hero - killed - dragon .event)
export class HeroKilledDragonEvent {
constructor (
public readonly heroId : string ,
public readonly dragonId : string ,
) {}
}
@@switch
export class HeroKilledDragonEvent {
constructor (heroId , dragonId) {
this .heroId = heroId;
this .dragonId = dragonId;
}
}
이벤트는 비동기적입니다. models 또는 EventBus
를 사용하여 직접 발송됩니다. 이벤트를 전달하려면 모델이 AggregateRoot
클래스를 확장해야 합니다.
Copy @@ filename ( hero .model)
export class Hero extends AggregateRoot {
constructor ( private readonly id : string ) {
super ();
}
killEnemy (enemyId : string ) {
// logic
this .apply ( new HeroKilledDragonEvent ( this .id , enemyId));
}
}
@@switch
export class Hero extends AggregateRoot {
constructor (id) {
super ();
this .id = id;
}
killEnemy (enemyId) {
// logic
this .apply ( new HeroKilledDragonEvent ( this .id , enemyId));
}
}
apply()
메소드는 모델과 EventPublisher
클래스 사이에 관계가 없기 때문에 아직 이벤트를 전달하지 않습니다. 모델과 게시자를 연결하는 방법은 무엇입니까? 커맨드 핸들러내에서 퍼블리셔 mergeObjectContext()
메소드를 사용해야합니다.
Copy @@ filename (kill - dragon .handler)
@ CommandHandler (KillDragonCommand)
export class KillDragonHandler implements ICommandHandler < KillDragonCommand > {
constructor (
private readonly repository : HeroRepository ,
private readonly publisher : EventPublisher ,
) {}
async execute (command : KillDragonCommand ) {
const { heroId , dragonId } = command;
const hero = this . publisher .mergeObjectContext (
await this . repository .findOneById ( + heroId) ,
);
hero .killEnemy (dragonId);
hero .commit ();
}
}
@@switch
@ CommandHandler (KillDragonCommand)
@ Dependencies (HeroRepository , EventPublisher)
export class KillDragonHandler {
constructor (repository , publisher) {
this .repository = repository;
this .publisher = publisher;
}
async execute (command) {
const { heroId , dragonId } = command;
const hero = this . publisher .mergeObjectContext (
await this . repository .findOneById ( + heroId) ,
);
hero .killEnemy (dragonId);
hero .commit ();
}
}
이제 모든 것이 예상대로 작동합니다. 이벤트가 즉시 전달되지 않으므로 commit()
이벤트가 필요합니다. 분명히, 객체는 선재할 필요가 없습니다. 타입 컨텍스트도 쉽게 병합할 수 있습니다 :
Copy const HeroModel = this . publisher .mergeContext (Hero);
new HeroModel ( 'id' );
그게 전부입니다. 모델은 이제 이벤트를 게시할 수 있습니다. 그리고 우리는 그것들을 처리해야 합니다. 또한, 우리는 EventBus
를 사용하여 수동으로 이벤트를 생성할 수 있습니다 :
Copy this . eventBus .publish ( new HeroKilledDragonEvent ());
info 힌트 이벤트 버스
는 주입 가능한 클래스입니다.
각 이벤트에는 여러 개의 이벤트 핸들러 가 있을 수 있습니다.
Copy @@ filename (hero - killed - dragon .handler)
@ EventsHandler (HeroKilledDragonEvent)
export class HeroKilledDragonHandler implements IEventHandler < HeroKilledDragonEvent > {
constructor ( private readonly repository : HeroRepository ) {}
handle (event : HeroKilledDragonEvent ) {
// logic
}
}
이제 write logic 을 이벤트 핸들러로 옮길 수 있습니다.
Sagas
이 유형의 이벤트 중심 아키텍처 는 응용 프로그램 응답성 및 확장성 을 향상시킵니다. 이제 이벤트가 있을 때 다양한 방식으로 간단하게 반응할 수 있습니다. Sagas 는 아키텍처 관점에서 마지막 빌딩 블록입니다.
Sagas는 엄청나게 강력한 기능입니다. Single saga는 1..* 이벤트를 수신 할 수 있습니다. [... ] 이벤트 스트림을 결합, 병합, 필터링 할 수 있습니다. RxJS 라이브러리는 마법의 근원입니다. 간단히 말해서, 각 saga는 명령이 포함된 Observable을 반환해야 합니다. 이 명령은 비동기적으로 전달됩니다.
Copy @@ filename (heroes - game .saga)
@ Injectable ()
export class HeroesGameSagas {
@ Saga ()
dragonKilled = (events$ : Observable < any >) : Observable < ICommand > => {
return events$ .pipe (
ofType (HeroKilledDragonEvent) ,
map ((event) => new DropAncientItemCommand ( event .heroId , fakeItemID)) ,
);
}
}
@@switch
@ Injectable ()
export class HeroesGameSagas {
@ Saga ()
dragonKilled = (events$) => {
return events$ .pipe (
ofType (HeroKilledDragonEvent) ,
map ((event) => new DropAncientItemCommand ( event .heroId , fakeItemID)) ,
);
}
}
info 힌트 ofType
연산자는@nestjs/cqrs
패키지에서 가져옵니다.
우리는 규칙을 선언했습니다 - 어떤 영웅이 용을 죽이면 고대 아이템이 떨어집니다. 그런 다음 적절한 처리기가 DropAncientItemCommand
를 전달하고 처리합니다.
Queries
CqrsModule
은 쿼리 처리에도 유용할 수 있습니다. QueryBus
는 CommandsBus
와 동일하게 작동합니다. 또한 쿼리 핸들러는 IQueryHandler
인터페이스를 구현하고 @QueryHandler()
데코레이터로 표시해야 합니다.
Setup
우리가 마지막으로 돌봐야 할 것은 전체 메커니즘을 설정하는 것입니다.
Copy @@ filename (heroes - game .module)
export const CommandHandlers = [KillDragonHandler , DropAncientItemHandler];
export const EventHandlers = [HeroKilledDragonHandler , HeroFoundItemHandler];
@ Module ({
imports : [CqrsModule] ,
controllers : [HeroesGameController] ,
providers : [
HeroesGameService ,
HeroesGameSagas ,
... CommandHandlers ,
... EventHandlers ,
HeroRepository ,
]
})
export class HeroesGameModule {}
Summary
CommandBus
, QueryBus
및 EventBus
는 관찰 가능(Observables) 입니다. 이벤트 소싱 을 통해 전체 스트림을 쉽게 구독하고 응용 프로그램을 보강할 수 있습니다.
실제 사례는 여기 에서 확인할 수 있습니다.