NestJS์์ CQRS ํจํด ์๋ฒฝ ๊ฐ์ด๋: ํ์ฅ ๊ฐ๋ฅํ ์ํคํ ์ฒ ๊ตฌ์ถํ๊ธฐ
๋ณต์กํ ๋น์ฆ๋์ค ๋ก์ง๊ณผ ๋์ ์ฑ๋ฅ์ ์๊ตฌํ๋ ๋ฐฑ์๋ ์์คํ ์ ๊ฐ๋ฐํ๋ค ๋ณด๋ฉด ๊ธฐ์กด์ CRUD ํจํด์ด ํ๊ณ๋ฅผ ๋๋ฌ๋ด๋ ๊ฒฝ์ฐ๊ฐ ๋ง์ต๋๋ค. ์ด๋ CQRS(Command Query Responsibility Segregation) ํจํด์ด ํด๊ฒฐ์ฑ ์ด ๋ ์ ์์ต๋๋ค. ์ด ๊ฐ์ด๋์์๋ NestJS์์ CQRS ํจํด์ ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ ๋จ๊ณ๋ณ๋ก ์ดํด๋ณด๊ณ , ์ค์ ํ๋ก์ ํธ์ ์ ์ฉํ ์ ์๋ ์ค๋ฌด ๋ ธํ์ฐ๋ฅผ ๊ณต์ ํ๊ฒ ์ต๋๋ค.
CQRS ํจํด์ ํต์ฌ ๊ฐ๋ ๋ถํฐ NestJS์์์ ์ค์ ๊ตฌํ, ํ ์คํ ์ ๋ต๊น์ง ์ข ํฉ์ ์ผ๋ก ๋ค๋ฃจ์ด ์ฌ๋ฌ๋ถ์ ์ ํ๋ฆฌ์ผ์ด์ ์ํคํ ์ฒ๋ฅผ ํ ๋จ๊ณ ๋์ด์ฌ๋ฆด ์ ์๋๋ก ๋์๋๋ฆฝ๋๋ค.
CQRS ํจํด์ด๋? ๊ธฐ์กด CRUD์์ ์ฐจ์ด์
CQRS(Command Query Responsibility Segregation)๋ ๋ช ๋ น(Command)๊ณผ ์กฐํ(Query)์ ์ฑ ์์ ๋ถ๋ฆฌํ๋ ์ํคํ ์ฒ ํจํด์ ๋๋ค. ์ด ํจํด์ ์ฝ๊ธฐ์ ์ฐ๊ธฐ ์์ ์ ์์ ํ ๋ณ๋์ ๋ชจ๋ธ๋ก ๋ถ๋ฆฌํ์ฌ ๊ฐ๊ฐ์ ๋ ๋ฆฝ์ ์ผ๋ก ์ต์ ํํ ์ ์๊ฒ ํด์ค๋๋ค.
์ ํต์ ์ธ CRUD ๋ฐฉ์์ ํ๊ณ
๊ธฐ์กด์ CRUD ์ํคํ ์ฒ์์๋ ํ๋์ ์๋น์ค์์ ๋ชจ๋ ์์ ์ ์ฒ๋ฆฌํฉ๋๋ค:
@Injectable()
export class UserService {
createUser(userData: CreateUserDto) { /* ... */ }
getUser(id: string) { /* ... */ }
updateUser(id: string, userData: UpdateUserDto) { /* ... */ }
deleteUser(id: string) { /* ... */ }
}
์ด๋ฐ ๊ตฌ์กฐ๋ ์์ ํ๋ก์ ํธ์์๋ ๋ฌธ์ ์์ง๋ง, ๋น์ฆ๋์ค ๋ก์ง์ด ๋ณต์กํด์ง๋ฉด์ ๋ค์๊ณผ ๊ฐ์ ๋ฌธ์ ๋ค์ด ๋ฐ์ํฉ๋๋ค:
- ๋น์ฆ๋์ค ๋ก์ง์ ์ฝํ: ์ฝ๊ธฐ์ ์ฐ๊ธฐ ๋ก์ง์ด ํ๋์ ์๋น์ค์ ํผ์ฌ
- ํ์ฅ์ฑ ์ ์ฝ: ์ฝ๊ธฐ์ ์ฐ๊ธฐ ์์ ์ ์ฑ๋ฅ ์๊ตฌ์ฌํญ์ด ๋ค๋ฅธ๋ฐ๋ ํจ๊ป ๋ฌถ์ฌ ์์
- ํ ์คํธ ๋ณต์ก์ฑ: ๋ชจ๋ ๊ธฐ๋ฅ์ด ํ๋์ ํด๋์ค์ ์์ด ๋จ์ ํ ์คํธ๊ฐ ์ด๋ ค์
CQRS๊ฐ ์ ๊ณตํ๋ ํด๊ฒฐ์ฑ
CQRS ํจํด์ ์ด๋ฌํ ๋ฌธ์ ๋ฅผ ๋ค์๊ณผ ๊ฐ์ด ํด๊ฒฐํฉ๋๋ค:
- ์ฑ ์ ๋ถ๋ฆฌ: ์ํ ๋ณ๊ฒฝ ์์ (Command)๊ณผ ๋ฐ์ดํฐ ์กฐํ ์์ (Query)์ ๋ช ํํ ๋ถ๋ฆฌ
- ๋ ๋ฆฝ์ ํ์ฅ: ์ฝ๊ธฐ์ ์ฐ๊ธฐ ์์ ์ ๊ฐ๊ฐ ์ต์ ํํ๊ณ ๋ ๋ฆฝ์ ์ผ๋ก ํ์ฅ ๊ฐ๋ฅ
- ๋ช ํํ ํ๋ก์ฐ: "๋ฌด์์ ํด์ผ ํ๋์ง"๊ฐ ๋ช ํํ๊ฒ ๋๋ฌ๋จ
- ํ ์คํธ ์ฉ์ด์ฑ: ๊ฐ ํธ๋ค๋ฌ๊ฐ ๋ ๋ฆฝ์ ์ด์ด์ ๋จ์ ํ ์คํธ๊ฐ ์ฌ์์ง
NestJS CQRS ๋ชจ๋ ์ค์ ํ๊ธฐ
NestJS์์ CQRS ํจํด์ ๊ตฌํํ๋ ค๋ฉด @nestjs/cqrs ํจํค์ง๋ฅผ ์ฌ์ฉํฉ๋๋ค. ์ด ํจํค์ง๋ CommandBus, QueryBus, EventBus ๋ฑ CQRS ๊ตฌํ์ ํ์ํ ํต์ฌ ์ปดํฌ๋ํธ๋ค์ ์ ๊ณตํฉ๋๋ค.
ํ๋ก์ ํธ ์ค์
๋จผ์ ํ์ํ ํจํค์ง๋ฅผ ์ค์นํฉ๋๋ค:
npm install --save @nestjs/cqrs
๋ค์์ผ๋ก ๋ฃจํธ ๋ชจ๋์ CqrsModule์ ๋ฑ๋กํฉ๋๋ค:
// app.module.ts
import { Module } from '@nestjs/common';
import { CqrsModule } from '@nestjs/cqrs';
import { UserModule } from './user/user.module';
@Module({
imports: [
CqrsModule.forRoot(),
UserModule,
],
})
export class AppModule {}
CqrsModule์ CommandBus, QueryBus, EventBus๋ฅผ ์๋์ผ๋ก ์ฃผ์ ํ์ฌ CQRS ํจํด์ ๊ธฐ๋ฐ์ ์ ๊ณตํฉ๋๋ค.
Command์ Query ๊ตฌํํ๊ธฐ
CQRS์ ํต์ฌ์ Command์ Query๋ฅผ ๋ช ํํ ๋ถ๋ฆฌํ๋ ๊ฒ์ ๋๋ค.
- Command โ ์ํ๋ฅผ ๋ณ๊ฒฝํ๋ ์์ (Create, Update, Delete)
- Query โ ๋ฐ์ดํฐ๋ฅผ ์ฝ๋ ์์ (Read)
๊ฐ๊ฐ์ ์์๋ฅผ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
// commands/create-user.command.ts
export class CreateUserCommand {
constructor(
public readonly name: string,
public readonly email: string,
) {}
}
// queries/get-user.query.ts
export class GetUserQuery {
constructor(public readonly id: string) {}
}
Command & Query Handler ๊ตฌํ
ํธ๋ค๋ฌ๋ ์ค์ ๋ก์ง์ ์ํํ๋ ํต์ฌ ๋ถ๋ถ์ ๋๋ค.
// commands/handlers/create-user.handler.ts
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { CreateUserCommand } from '../create-user.command';
import { UserService } from '../../user.service';
@CommandHandler(CreateUserCommand)
export class CreateUserHandler implements ICommandHandler<CreateUserCommand> {
constructor(private readonly userService: UserService) {}
async execute(command: CreateUserCommand): Promise<any> {
const { name, email } = command;
return this.userService.create({ name, email });
}
}
// queries/handlers/get-user.handler.ts
import { QueryHandler, IQueryHandler } from '@nestjs/cqrs';
import { GetUserQuery } from '../get-user.query';
import { UserService } from '../../user.service';
@QueryHandler(GetUserQuery)
export class GetUserHandler implements IQueryHandler<GetUserQuery> {
constructor(private readonly userService: UserService) {}
async execute(query: GetUserQuery): Promise<any> {
return this.userService.findById(query.id);
}
}
ํธ๋ค๋ฌ๋ ๋จ์ผ ์ฑ ์ ์์น(SRP)์ ๋ฐ๋ผ ๊ฐ๊ฐ์ Command๋ Query๋ง ๋ด๋นํด์ผ ํฉ๋๋ค.
Controller์์ CQRS ์ ์ฉ
Controller๋ CommandBus์ QueryBus๋ฅผ ํตํด ๊ฐ ํธ๋ค๋ฌ๋ก ์์ฒญ์ ์์ํฉ๋๋ค.
@Controller('users')
export class UserController {
constructor(
private readonly commandBus: CommandBus,
private readonly queryBus: QueryBus,
) {}
@Post()
async createUser(@Body() userData: { name: string; email: string }) {
return this.commandBus.execute(
new CreateUserCommand(userData.name, userData.email)
);
}
@Get(':id')
async getUser(@Param('id') id: string) {
return this.queryBus.execute(new GetUserQuery(id));
}
}
Controller๋ CQRS ํ๊ฒฝ์์ โ์ ๋ ฅ ํฌํธโ ์ญํ ๋ง ์ํํ๋ฉฐ, ๋ก์ง์ ๋ชจ๋ ํธ๋ค๋ฌ๋ก ์์๋ฉ๋๋ค.
Event Handling๊ณผ Sagas
CQRS์ ์ง์ ํ ํ์ Event ์์คํ ๊ณผ ๊ฒฐํฉ๋ ๋ ๋ํ๋ฉ๋๋ค.
// events/user-created.event.ts
export class UserCreatedEvent {
constructor(
public readonly userId: string,
public readonly email: string,
) {}
}
// events/handlers/user-created.handler.ts
@EventsHandler(UserCreatedEvent)
export class UserCreatedHandler implements IEventHandler<UserCreatedEvent> {
handle(event: UserCreatedEvent) {
console.log(`์ฌ์ฉ์๊ฐ ์์ฑ๋์์ต๋๋ค: ${event.userId}`);
}
}
Event๋ Command/Query์ ์คํ ๊ฒฐ๊ณผ๋ก ๋ฐ์ํ๋ฉฐ, ์์คํ ์ ๋ค๋ฅธ ๋ถ๋ถ๋ค์ด ์ด๋ฅผ ๊ฐ์งํ๊ณ ๋ฐ์ํ ์ ์์ต๋๋ค.
CQRS ๊ตฌํ ํ ์คํ
CQRS๋ ํธ๋ค๋ฌ ๋จ์ ํ ์คํธ๊ฐ ๋งค์ฐ ์ฝ์ต๋๋ค.
describe('CreateUserHandler', () => {
it('userService.create๋ฅผ ์ฌ๋ฐ๋ฅด๊ฒ ํธ์ถํด์ผ ํจ', async () => {
const command = new CreateUserCommand('Alice', 'alice@email.com');
const result = await handler.execute(command);
expect(result).toBeDefined();
});
});
๊ฐ ํธ๋ค๋ฌ๋ ๋ ๋ฆฝ์ ์ธ ์ ๋์ด๊ธฐ ๋๋ฌธ์, ์ธ๋ถ ์์กด์ฑ์ Mockingํ์ฌ ์์ฝ๊ฒ ํ ์คํธํ ์ ์์ต๋๋ค.
CQRS Best Practices
- Command์ Query๋ฅผ ๋ฐ๋์ ๋ถ๋ฆฌํ ๊ฒ
- EventBus๋ฅผ ์ ๊ทน์ ์ผ๋ก ํ์ฉํด ๋น๋๊ธฐ ํ๋ก์ฐ ๊ตฌ์ฑ
- ํธ๋ค๋ฌ ๋จ์ ํ ์คํธ๋ฅผ ์ต๊ดํํ ๊ฒ
- ํธ๋ค๋ฌ ๋ด๋ถ์์ ๋น์ฆ๋์ค ๋ก์ง ์ต์ํ: ์๋น์ค ๊ณ์ธต์ ์์
CQRS๋ ์์ ํ๋ก์ ํธ์๋ ๊ณผ๋ํ ์ ์์ต๋๋ค. ๋ณต์กํ ๋น์ฆ๋์ค ๋ก์ง, ์ด๋ฒคํธ ์ค์ฌ ์ํคํ ์ฒ, ๋๊ท๋ชจ ํธ๋ํฝ ํ๊ฒฝ์ผ ๋ ๋์ ์ด ์ ์ ํฉ๋๋ค.