Sam Baek, The Dev's Corner

๐Ÿš€ NestJS + CQRS ์™„๋ฒฝ ๊ฐ€์ด๋“œ (Command, Query, Event Sourcing)

12 Nov 2025

CQRS๋ž€ ๋ฌด์—‡์ธ๊ฐ€


์‹๋‹น์„ ์ƒ์ƒํ•ด๋ณด์ž.
์ฃผ๋ฌธ๋ฐ›๋Š” ์ง์›๊ณผ ์Œ์‹ ์„œ๋น™ํ•˜๋Š” ์ง์›์ด ๋”ฐ๋กœ ์žˆ๋‹ค.

์™œ ๋ถ„๋ฆฌํ• ๊นŒ?
์ฃผ๋ฌธ์€ ๋น ๋ฅด๊ฒŒ, ์„œ๋น™์€ ์ •ํ™•ํ•˜๊ฒŒ ํ•ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

CQRS(Command Query Responsibility Segregation)๋Š”
๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” ์ž‘์—…(Command)๊ณผ
๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•˜๋Š” ์ž‘์—…(Query)์„ ๋ถ„๋ฆฌํ•˜๋Š” ํŒจํ„ด์ด๋‹ค.

๋งˆ์น˜ ์‹๋‹น์—์„œ ์—ญํ• ์„ ๋‚˜๋ˆ„๋“ฏ,
์‹œ์Šคํ…œ๋„ ์“ฐ๊ธฐ์™€ ์ฝ๊ธฐ๋ฅผ ๋ถ„๋ฆฌํ•˜๋ฉด
๊ฐ๊ฐ ์ตœ์ ํ™”ํ•  ์ˆ˜ ์žˆ๋‹ค.

์™œ CQRS๋ฅผ ๋ฐฐ์›Œ์•ผ ํ• ๊นŒ?


์ด์œ  1: ์„ฑ๋Šฅ ์ตœ์ ํ™”
์ฝ๊ธฐ์™€ ์“ฐ๊ธฐ๋ฅผ ๊ฐ๊ฐ ์ตœ์ ํ™” ๊ฐ€๋Šฅ

์ด์œ  2: ํ™•์žฅ์„ฑ
์ฝ๊ธฐ ์„œ๋ฒ„์™€ ์“ฐ๊ธฐ ์„œ๋ฒ„๋ฅผ ๋…๋ฆฝ์ ์œผ๋กœ ํ™•์žฅ

์ด์œ  3: ๋ณต์žกํ•œ ๋„๋ฉ”์ธ ์ฒ˜๋ฆฌ
DDD์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋ฉด ๊ฐ•๋ ฅํ•จ

์ด์œ  4: ๋ฉด์ ‘ ํ•„์ˆ˜
CQRS, Event Sourcing์€ ์‹œ๋‹ˆ์–ด ๋ฉด์ ‘ ๋‹จ๊ณจ

๊ธฐ๋ณธ ๊ฐœ๋… ์š”์•ฝ


๐Ÿท๏ธ CQRS ํ•ต์‹ฌ ๊ตฌ์กฐ


[ํด๋ผ์ด์–ธํŠธ]
    โ”‚
    โ”œโ”€โ”€ Command (์“ฐ๊ธฐ) โ”€โ”€โ†’ [Command Handler] โ”€โ”€โ†’ [Write DB]
    โ”‚                              โ”‚
    โ”‚                              โ†“ (์ด๋ฒคํŠธ ๋ฐœํ–‰)
    โ”‚                         [Event Bus]
    โ”‚                              โ”‚
    โ”‚                              โ†“
    โ””โ”€โ”€ Query (์ฝ๊ธฐ) โ”€โ”€โ”€โ†’ [Query Handler] โ”€โ”€โ”€โ†’ [Read DB]


Command (๋ช…๋ น)


๊ฐœ๋…: ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” ์š”์ฒญ

ํŠน์ง•:

  • ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•จ
  • ๋ฐ˜ํ™˜๊ฐ’ ์—†๊ฑฐ๋‚˜ ์ตœ์†Œํ™”
  • ์˜ˆ: ์ฃผ๋ฌธ ์ƒ์„ฑ, ๊ฒฐ์ œ ์ฒ˜๋ฆฌ


Query (์กฐํšŒ)


๊ฐœ๋…: ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•˜๋Š” ์š”์ฒญ

ํŠน์ง•:

  • ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š์Œ
  • ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜
  • ์˜ˆ: ์ฃผ๋ฌธ ๋ชฉ๋ก ์กฐํšŒ, ์ƒํ’ˆ ๊ฒ€์ƒ‰


Event (์ด๋ฒคํŠธ)


๊ฐœ๋…: ์‹œ์Šคํ…œ์—์„œ ๋ฐœ์ƒํ•œ ์‚ฌ์‹ค

ํŠน์ง•:

  • ๊ณผ๊ฑฐํ˜•์œผ๋กœ ๋ช…๋ช… (OrderCreated)
  • ๋ถˆ๋ณ€
  • ๋‹ค๋ฅธ ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ๋ฐ˜์‘


๐Ÿท๏ธ ์ „ํ†ต์  CRUD vs CQRS


๊ตฌ๋ถ„ CRUD CQRS
๋ชจ๋ธ ๋‹จ์ผ ๋ชจ๋ธ ์ฝ๊ธฐ/์“ฐ๊ธฐ ๋ถ„๋ฆฌ
DB ํ•˜๋‚˜์˜ DB ๋ถ„๋ฆฌ ๊ฐ€๋Šฅ
๋ณต์žก๋„ ๋‹จ์ˆœ ๋ณต์žก
ํ™•์žฅ์„ฑ ์ œํ•œ์  ๋†’์Œ
์ ํ•ฉํ•œ ๊ฒฝ์šฐ ๋‹จ์ˆœ CRUD ๋ณต์žกํ•œ ๋„๋ฉ”์ธ


๐Ÿท๏ธ NestJS CQRS ๋ชจ๋“ˆ ๊ตฌ์กฐ


src/
โ”œโ”€โ”€ orders/
โ”‚   โ”œโ”€โ”€ commands/
โ”‚   โ”‚   โ”œโ”€โ”€ impl/
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ create-order.command.ts
โ”‚   โ”‚   โ””โ”€โ”€ handlers/
โ”‚   โ”‚       โ””โ”€โ”€ create-order.handler.ts
โ”‚   โ”œโ”€โ”€ queries/
โ”‚   โ”‚   โ”œโ”€โ”€ impl/
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ get-order.query.ts
โ”‚   โ”‚   โ””โ”€โ”€ handlers/
โ”‚   โ”‚       โ””โ”€โ”€ get-order.handler.ts
โ”‚   โ”œโ”€โ”€ events/
โ”‚   โ”‚   โ”œโ”€โ”€ impl/
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ order-created.event.ts
โ”‚   โ”‚   โ””โ”€โ”€ handlers/
โ”‚   โ”‚       โ””โ”€โ”€ order-created.handler.ts
โ”‚   โ”œโ”€โ”€ dto/
โ”‚   โ”œโ”€โ”€ entities/
โ”‚   โ””โ”€โ”€ orders.module.ts


์‹ค์ „ ์˜ˆ์‹œ


๐Ÿท๏ธ ํ”„๋กœ์ ํŠธ ์„ค์ •


# NestJS ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ
npm i -g @nestjs/cli
nest new nestjs-cqrs-demo

# CQRS ํŒจํ‚ค์ง€ ์„ค์น˜
npm install @nestjs/cqrs

# TypeORM ์„ค์น˜ (์„ ํƒ)
npm install @nestjs/typeorm typeorm pg


๐Ÿท๏ธ Command ๊ตฌํ˜„


1. Command ์ •์˜


// src/orders/commands/impl/create-order.command.ts
export class CreateOrderCommand {
  constructor(
    public readonly userId: string,
    public readonly items: OrderItemDto[],
    public readonly shippingAddress: string
  ) {}
}

// src/orders/commands/impl/cancel-order.command.ts
export class CancelOrderCommand {
  constructor(
    public readonly orderId: string,
    public readonly reason: string
  ) {}
}


2. Command Handler ๊ตฌํ˜„


// src/orders/commands/handlers/create-order.handler.ts
import { CommandHandler, ICommandHandler, EventBus } from "@nestjs/cqrs";
import { CreateOrderCommand } from "../impl/create-order.command";
import { OrderCreatedEvent } from "../../events/impl/order-created.event";
import { OrderRepository } from "../../repositories/order.repository";
import { Order } from "../../entities/order.entity";

@CommandHandler(CreateOrderCommand)
export class CreateOrderHandler implements ICommandHandler<CreateOrderCommand> {
  constructor(
    private readonly orderRepository: OrderRepository,
    private readonly eventBus: EventBus
  ) {}

  async execute(command: CreateOrderCommand): Promise<string> {
    const { userId, items, shippingAddress } = command;

    // 1. ์ฃผ๋ฌธ ์ƒ์„ฑ
    const order = Order.create({
      userId,
      items,
      shippingAddress,
      status: "PENDING",
    });

    // 2. ์ €์žฅ
    await this.orderRepository.save(order);

    // 3. ์ด๋ฒคํŠธ ๋ฐœํ–‰
    this.eventBus.publish(
      new OrderCreatedEvent(order.id, order.userId, order.totalAmount)
    );

    return order.id;
  }
}

// src/orders/commands/handlers/cancel-order.handler.ts
@CommandHandler(CancelOrderCommand)
export class CancelOrderHandler implements ICommandHandler<CancelOrderCommand> {
  constructor(
    private readonly orderRepository: OrderRepository,
    private readonly eventBus: EventBus
  ) {}

  async execute(command: CancelOrderCommand): Promise<void> {
    const { orderId, reason } = command;

    // 1. ์ฃผ๋ฌธ ์กฐํšŒ
    const order = await this.orderRepository.findById(orderId);
    if (!order) {
      throw new NotFoundException("์ฃผ๋ฌธ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค");
    }

    // 2. ์ทจ์†Œ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ ํ™•์ธ
    if (order.status === "SHIPPED") {
      throw new BadRequestException("๋ฐฐ์†ก๋œ ์ฃผ๋ฌธ์€ ์ทจ์†Œํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค");
    }

    // 3. ์ฃผ๋ฌธ ์ทจ์†Œ
    order.cancel(reason);
    await this.orderRepository.save(order);

    // 4. ์ด๋ฒคํŠธ ๋ฐœํ–‰
    this.eventBus.publish(new OrderCancelledEvent(orderId, reason));
  }
}


๐Ÿท๏ธ Query ๊ตฌํ˜„


1. Query ์ •์˜


// src/orders/queries/impl/get-order.query.ts
export class GetOrderQuery {
  constructor(public readonly orderId: string) {}
}

// src/orders/queries/impl/get-orders-by-user.query.ts
export class GetOrdersByUserQuery {
  constructor(
    public readonly userId: string,
    public readonly page: number = 1,
    public readonly limit: number = 10
  ) {}
}


2. Query Handler ๊ตฌํ˜„


// src/orders/queries/handlers/get-order.handler.ts
import { IQueryHandler, QueryHandler } from "@nestjs/cqrs";
import { GetOrderQuery } from "../impl/get-order.query";
import { OrderReadRepository } from "../../repositories/order-read.repository";

@QueryHandler(GetOrderQuery)
export class GetOrderHandler implements IQueryHandler<GetOrderQuery> {
  constructor(private readonly orderReadRepository: OrderReadRepository) {}

  async execute(query: GetOrderQuery): Promise<OrderDto> {
    const { orderId } = query;

    const order = await this.orderReadRepository.findById(orderId);
    if (!order) {
      throw new NotFoundException("์ฃผ๋ฌธ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค");
    }

    return OrderDto.from(order);
  }
}

// src/orders/queries/handlers/get-orders-by-user.handler.ts
@QueryHandler(GetOrdersByUserQuery)
export class GetOrdersByUserHandler
  implements IQueryHandler<GetOrdersByUserQuery>
{
  constructor(private readonly orderReadRepository: OrderReadRepository) {}

  async execute(query: GetOrdersByUserQuery): Promise<PaginatedOrdersDto> {
    const { userId, page, limit } = query;

    const [orders, total] = await this.orderReadRepository.findByUserId(
      userId,
      page,
      limit
    );

    return {
      data: orders.map(OrderDto.from),
      total,
      page,
      limit,
      totalPages: Math.ceil(total / limit),
    };
  }
}


๐Ÿท๏ธ Event ๊ตฌํ˜„


1. Event ์ •์˜


// src/orders/events/impl/order-created.event.ts
export class OrderCreatedEvent {
  constructor(
    public readonly orderId: string,
    public readonly userId: string,
    public readonly totalAmount: number
  ) {}
}

// src/orders/events/impl/order-cancelled.event.ts
export class OrderCancelledEvent {
  constructor(
    public readonly orderId: string,
    public readonly reason: string
  ) {}
}


2. Event Handler ๊ตฌํ˜„


// src/orders/events/handlers/order-created.handler.ts
import { EventsHandler, IEventHandler } from "@nestjs/cqrs";
import { OrderCreatedEvent } from "../impl/order-created.event";

@EventsHandler(OrderCreatedEvent)
export class OrderCreatedHandler implements IEventHandler<OrderCreatedEvent> {
  constructor(
    private readonly notificationService: NotificationService,
    private readonly analyticsService: AnalyticsService
  ) {}

  async handle(event: OrderCreatedEvent): Promise<void> {
    const { orderId, userId, totalAmount } = event;

    // 1. ์•Œ๋ฆผ ์ „์†ก
    await this.notificationService.sendOrderConfirmation(userId, orderId);

    // 2. ๋ถ„์„ ๋ฐ์ดํ„ฐ ์ €์žฅ
    await this.analyticsService.trackOrder(orderId, totalAmount);

    console.log(`์ฃผ๋ฌธ ์ƒ์„ฑ๋จ: ${orderId}, ๊ธˆ์•ก: ${totalAmount}`);
  }
}

// src/orders/events/handlers/order-cancelled.handler.ts
@EventsHandler(OrderCancelledEvent)
export class OrderCancelledHandler
  implements IEventHandler<OrderCancelledEvent>
{
  constructor(
    private readonly inventoryService: InventoryService,
    private readonly paymentService: PaymentService
  ) {}

  async handle(event: OrderCancelledEvent): Promise<void> {
    const { orderId, reason } = event;

    // 1. ์žฌ๊ณ  ๋ณต๊ตฌ
    await this.inventoryService.restoreStock(orderId);

    // 2. ํ™˜๋ถˆ ์ฒ˜๋ฆฌ
    await this.paymentService.refund(orderId);

    console.log(`์ฃผ๋ฌธ ์ทจ์†Œ๋จ: ${orderId}, ์‚ฌ์œ : ${reason}`);
  }
}


๐Ÿท๏ธ Controller ๊ตฌํ˜„


// src/orders/orders.controller.ts
import { Controller, Post, Get, Body, Param, Query } from "@nestjs/common";
import { CommandBus, QueryBus } from "@nestjs/cqrs";
import { CreateOrderCommand } from "./commands/impl/create-order.command";
import { CancelOrderCommand } from "./commands/impl/cancel-order.command";
import { GetOrderQuery } from "./queries/impl/get-order.query";
import { GetOrdersByUserQuery } from "./queries/impl/get-orders-by-user.query";

@Controller("orders")
export class OrdersController {
  constructor(
    private readonly commandBus: CommandBus,
    private readonly queryBus: QueryBus
  ) {}

  // Command: ์ฃผ๋ฌธ ์ƒ์„ฑ
  @Post()
  async createOrder(@Body() dto: CreateOrderDto): Promise<{ orderId: string }> {
    const orderId = await this.commandBus.execute(
      new CreateOrderCommand(dto.userId, dto.items, dto.shippingAddress)
    );
    return { orderId };
  }

  // Command: ์ฃผ๋ฌธ ์ทจ์†Œ
  @Post(":id/cancel")
  async cancelOrder(
    @Param("id") orderId: string,
    @Body() dto: CancelOrderDto
  ): Promise<void> {
    await this.commandBus.execute(new CancelOrderCommand(orderId, dto.reason));
  }

  // Query: ์ฃผ๋ฌธ ์กฐํšŒ
  @Get(":id")
  async getOrder(@Param("id") orderId: string): Promise<OrderDto> {
    return this.queryBus.execute(new GetOrderQuery(orderId));
  }

  // Query: ์‚ฌ์šฉ์ž ์ฃผ๋ฌธ ๋ชฉ๋ก
  @Get("user/:userId")
  async getOrdersByUser(
    @Param("userId") userId: string,
    @Query("page") page: number = 1,
    @Query("limit") limit: number = 10
  ): Promise<PaginatedOrdersDto> {
    return this.queryBus.execute(new GetOrdersByUserQuery(userId, page, limit));
  }
}


๐Ÿท๏ธ Module ์„ค์ •


// src/orders/orders.module.ts
import { Module } from "@nestjs/common";
import { CqrsModule } from "@nestjs/cqrs";
import { TypeOrmModule } from "@nestjs/typeorm";
import { OrdersController } from "./orders.controller";
import { Order } from "./entities/order.entity";

// Command Handlers
import { CreateOrderHandler } from "./commands/handlers/create-order.handler";
import { CancelOrderHandler } from "./commands/handlers/cancel-order.handler";

// Query Handlers
import { GetOrderHandler } from "./queries/handlers/get-order.handler";
import { GetOrdersByUserHandler } from "./queries/handlers/get-orders-by-user.handler";

// Event Handlers
import { OrderCreatedHandler } from "./events/handlers/order-created.handler";
import { OrderCancelledHandler } from "./events/handlers/order-cancelled.handler";

// Repositories
import { OrderRepository } from "./repositories/order.repository";
import { OrderReadRepository } from "./repositories/order-read.repository";

const CommandHandlers = [CreateOrderHandler, CancelOrderHandler];
const QueryHandlers = [GetOrderHandler, GetOrdersByUserHandler];
const EventHandlers = [OrderCreatedHandler, OrderCancelledHandler];

@Module({
  imports: [CqrsModule, TypeOrmModule.forFeature([Order])],
  controllers: [OrdersController],
  providers: [
    ...CommandHandlers,
    ...QueryHandlers,
    ...EventHandlers,
    OrderRepository,
    OrderReadRepository,
  ],
})
export class OrdersModule {}


๐Ÿท๏ธ Entity์™€ Repository


// src/orders/entities/order.entity.ts
import { AggregateRoot } from "@nestjs/cqrs";

@Entity()
export class Order extends AggregateRoot {
  @PrimaryGeneratedColumn("uuid")
  id: string;

  @Column()
  userId: string;

  @Column("jsonb")
  items: OrderItem[];

  @Column()
  shippingAddress: string;

  @Column()
  status: OrderStatus;

  @Column("decimal")
  totalAmount: number;

  @CreateDateColumn()
  createdAt: Date;

  static create(props: CreateOrderProps): Order {
    const order = new Order();
    order.userId = props.userId;
    order.items = props.items;
    order.shippingAddress = props.shippingAddress;
    order.status = "PENDING";
    order.totalAmount = props.items.reduce(
      (sum, item) => sum + item.price * item.quantity,
      0
    );
    return order;
  }

  cancel(reason: string): void {
    if (this.status === "SHIPPED") {
      throw new Error("๋ฐฐ์†ก๋œ ์ฃผ๋ฌธ์€ ์ทจ์†Œ ๋ถˆ๊ฐ€");
    }
    this.status = "CANCELLED";
  }
}

// src/orders/repositories/order.repository.ts
@Injectable()
export class OrderRepository {
  constructor(
    @InjectRepository(Order)
    private readonly repository: Repository<Order>
  ) {}

  async save(order: Order): Promise<Order> {
    return this.repository.save(order);
  }

  async findById(id: string): Promise<Order | null> {
    return this.repository.findOne({ where: { id } });
  }
}

// src/orders/repositories/order-read.repository.ts
// ์ฝ๊ธฐ ์ „์šฉ - ์ตœ์ ํ™”๋œ ์ฟผ๋ฆฌ
@Injectable()
export class OrderReadRepository {
  constructor(
    @InjectRepository(Order)
    private readonly repository: Repository<Order>
  ) {}

  async findById(id: string): Promise<Order | null> {
    return this.repository
      .createQueryBuilder("order")
      .select(["order.id", "order.status", "order.totalAmount"])
      .where("order.id = :id", { id })
      .getOne();
  }

  async findByUserId(
    userId: string,
    page: number,
    limit: number
  ): Promise<[Order[], number]> {
    return this.repository.findAndCount({
      where: { userId },
      skip: (page - 1) * limit,
      take: limit,
      order: { createdAt: "DESC" },
    });
  }
}


๐Ÿท๏ธ Event Sourcing (์„ ํƒ)


// src/orders/events/order-event-store.ts
@Injectable()
export class OrderEventStore {
  constructor(
    @InjectRepository(OrderEvent)
    private readonly repository: Repository<OrderEvent>
  ) {}

  async save(event: DomainEvent): Promise<void> {
    const orderEvent = new OrderEvent();
    orderEvent.aggregateId = event.aggregateId;
    orderEvent.eventType = event.constructor.name;
    orderEvent.payload = JSON.stringify(event);
    orderEvent.occurredAt = new Date();

    await this.repository.save(orderEvent);
  }

  async getEvents(aggregateId: string): Promise<DomainEvent[]> {
    const events = await this.repository.find({
      where: { aggregateId },
      order: { occurredAt: "ASC" },
    });

    return events.map((e) => JSON.parse(e.payload));
  }

  // ์ด๋ฒคํŠธ๋กœ๋ถ€ํ„ฐ ์ƒํƒœ ๋ณต์›
  async rehydrate(aggregateId: string): Promise<Order> {
    const events = await this.getEvents(aggregateId);
    const order = new Order();

    events.forEach((event) => {
      order.apply(event);
    });

    return order;
  }
}


์‹ค์ „ ์ฒดํฌ๋ฆฌ์ŠคํŠธ


โœ… Command ์„ค๊ณ„


  • Command๋Š” ์˜๋„๋ฅผ ๋ช…ํ™•ํžˆ ํ‘œํ˜„
  • ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋งŒ ํฌํ•จ
  • ๋ฐ˜ํ™˜๊ฐ’ ์ตœ์†Œํ™”
  • ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ํฌํ•จ


โœ… Query ์„ค๊ณ„


  • ์ฝ๊ธฐ ์ „์šฉ Repository ๋ถ„๋ฆฌ
  • ํ•„์š”ํ•œ ํ•„๋“œ๋งŒ ์กฐํšŒ
  • ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ ์šฉ
  • ์บ์‹ฑ ๊ณ ๋ ค


โœ… Event ์„ค๊ณ„


  • ๊ณผ๊ฑฐํ˜• ์ด๋ฆ„ ์‚ฌ์šฉ
  • ๋ถˆ๋ณ€ ๊ฐ์ฒด๋กœ ์„ค๊ณ„
  • ํ•„์š”ํ•œ ์ •๋ณด๋งŒ ํฌํ•จ
  • ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ๊ณ ๋ ค


โœ… ์•„ํ‚คํ…์ฒ˜


  • ๋ชจ๋“ˆ๋ณ„ ๋ถ„๋ฆฌ
  • Handler ๋‹จ์ผ ์ฑ…์ž„
  • ์˜์กด์„ฑ ์ฃผ์ž… ํ™œ์šฉ
  • ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ ํ™•๋ณด


์š”์•ฝ


CQRS๋Š” ์ฝ๊ธฐ์™€ ์“ฐ๊ธฐ๋ฅผ ๋ถ„๋ฆฌํ•˜์—ฌ ๊ฐ๊ฐ ์ตœ์ ํ™”ํ•˜๋Š” ํŒจํ„ด์ด๋‹ค.

๐Ÿ’Ž ํ•ต์‹ฌ ํฌ์ธํŠธ:

  1. Command: ์ƒํƒœ ๋ณ€๊ฒฝ ์š”์ฒญ
  2. Query: ๋ฐ์ดํ„ฐ ์กฐํšŒ ์š”์ฒญ
  3. Event: ๋ฐœ์ƒํ•œ ์‚ฌ์‹ค ์•Œ๋ฆผ
  4. Handler: ๊ฐ ์š”์ฒญ ์ฒ˜๋ฆฌ
  5. EventBus: ์ด๋ฒคํŠธ ์ „๋‹ฌ
  6. ๋ถ„๋ฆฌ๋œ Repository: ์ฝ๊ธฐ/์“ฐ๊ธฐ ์ตœ์ ํ™”


๐Ÿ“Œ NestJS CQRS ํ๋ฆ„:

๋‹จ๊ณ„ ์„ค๋ช…
1. Controller ์š”์ฒญ ์ˆ˜์‹ 
2. CommandBus/QueryBus ์ ์ ˆํ•œ Handler๋กœ ์ „๋‹ฌ
3. Handler ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์‹คํ–‰
4. EventBus ์ด๋ฒคํŠธ ๋ฐœํ–‰
5. EventHandler ๋ถ€๊ฐ€ ์ž‘์—… ์ฒ˜๋ฆฌ


๐Ÿš€ Best Practices:

  • ๋‹จ์ˆœ CRUD๋Š” CQRS ๋ถˆํ•„์š”
  • ๋ณต์žกํ•œ ๋„๋ฉ”์ธ์— ์ ์šฉ
  • Event Sourcing์€ ์„ ํƒ
  • ์ฝ๊ธฐ ๋ชจ๋ธ ์บ์‹ฑ ํ™œ์šฉ
  • ๋น„๋™๊ธฐ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ
  • ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ํ•„์ˆ˜


CQRS๋Š” ๋ณต์žกํ•œ ๋„๋ฉ”์ธ์—์„œ ๋น›์„ ๋ฐœํ•œ๋‹ค.
๋‹จ์ˆœํ•œ CRUD ์•ฑ์—๋Š” ๊ณผํ•  ์ˆ˜ ์žˆ์ง€๋งŒ,
ํ™•์žฅ์„ฑ๊ณผ ์œ ์ง€๋ณด์ˆ˜์„ฑ์ด ์ค‘์š”ํ•˜๋‹ค๋ฉด ๊ณ ๋ คํ•˜์ž.