[Daily morning study] CQRS(Command Query Responsibility Segregation) ํŒจํ„ด

#daily morning study

Image


CQRS๋ž€?

CQRS(Command Query Responsibility Segregation)๋Š” ๋ช…๋ น(Command) ๊ณผ ์กฐํšŒ(Query) ์˜ ์ฑ…์ž„์„ ๋ถ„๋ฆฌํ•˜๋Š” ์•„ํ‚คํ…์ฒ˜ ํŒจํ„ด์ด๋‹ค.

์ „ํ†ต์ ์ธ CRUD ๋ชจ๋ธ์—์„œ๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ๊ณ  ์“ฐ๋Š” ๋ชจ๋ธ์ด ํ•˜๋‚˜๋‹ค. CQRS๋Š” ์ด๋ฅผ ๋‘˜๋กœ ๋‚˜๋ˆˆ๋‹ค.

  • Command: ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” ์ž‘์—… (์ƒ์„ฑ, ์ˆ˜์ •, ์‚ญ์ œ). ๋ฐ˜ํ™˜๊ฐ’ ์—†์Œ(๋˜๋Š” ID๋งŒ ๋ฐ˜ํ™˜).
  • Query: ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ๋Š” ์ž‘์—…. ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ ์—†์Œ.

์ด ์•„์ด๋””์–ด๋Š” Bertrand Meyer์˜ CQS(Command Query Separation) ์›์น™์—์„œ ์ถœ๋ฐœํ–ˆ๊ณ , Greg Young์ด ์ด๋ฅผ ์•„ํ‚คํ…์ฒ˜ ์ˆ˜์ค€์œผ๋กœ ๋ฐœ์ „์‹œ์ผœ CQRS๋ผ๊ณ  ๋ช…๋ช…ํ–ˆ๋‹ค.


์™œ ๋ถ„๋ฆฌํ•˜๋Š”๊ฐ€?

์ฝ๊ธฐ์™€ ์“ฐ๊ธฐ์˜ ํŠน์„ฑ์ด ๋‹ค๋ฅด๋‹ค

ํŠน์„ฑCommand (์“ฐ๊ธฐ)Query (์ฝ๊ธฐ)
๋นˆ๋„์ƒ๋Œ€์ ์œผ๋กœ ๋‚ฎ์Œ์ƒ๋Œ€์ ์œผ๋กœ ๋†’์Œ
๋ณต์žก๋„๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™, ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋ณต์žกํ•œ ์กฐ์ธ, ์ง‘๊ณ„
ํ™•์žฅ ๋ฐฉ์‹ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ ์ตœ์ ํ™”์ฝ๊ธฐ ์„ฑ๋Šฅ ์ตœ์ ํ™”
๋ชจ๋ธ ํ˜•ํƒœ๋„๋ฉ”์ธ ์ค‘์‹ฌํ™”๋ฉด/๋ณด๊ณ ์„œ ์ค‘์‹ฌ

์ฝ๊ธฐ ํŠธ๋ž˜ํ”ฝ์ด ์“ฐ๊ธฐ๋ณด๋‹ค 10๋ฐฐ, 100๋ฐฐ ๋งŽ์€ ๊ฒฝ์šฐ๊ฐ€ ํ”ํ•˜๋‹ค. ๊ฐ™์€ ๋ชจ๋ธ์„ ๊ณต์œ ํ•˜๋ฉด ๊ฐ๊ฐ์— ๋งž๋Š” ์ตœ์ ํ™”๊ฐ€ ์–ด๋ ต๋‹ค.


๊ธฐ๋ณธ ๊ตฌ์กฐ

Client
  โ”‚
  โ”œโ”€ Command โ†’ Command Handler โ†’ Write Model (DB)
  โ”‚
  โ””โ”€ Query  โ†’ Query Handler  โ†’ Read Model (DB or View)

Command ์ชฝ

  • Command ๊ฐ์ฒด: ์˜๋„๋ฅผ ๋‹ด์€ DTO (CreateOrderCommand, CancelOrderCommand)
  • Command Handler: ๋„๋ฉ”์ธ ๋ชจ๋ธ์„ ํ†ตํ•ด ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์ฒ˜๋ฆฌ ํ›„ Write DB์— ๋ฐ˜์˜
  • ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•ด์„œ Read Model์„ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๊ฒฝ์šฐ๋„ ๋งŽ์Œ

Query ์ชฝ

  • Query ๊ฐ์ฒด: ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ์™€ ํ•„ํ„ฐ ์กฐ๊ฑด (GetOrdersByUserQuery)
  • Query Handler: Read DB์—์„œ ์ง์ ‘ DTO๋ฅผ ์กฐํšŒํ•ด ๋ฐ˜ํ™˜
  • ORM์˜ ๋ณต์žกํ•œ ๋„๋ฉ”์ธ ๋ชจ๋ธ ์—†์ด raw SQL์ด๋‚˜ ๊ฐ„๋‹จํ•œ ์ฟผ๋ฆฌ๋กœ ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ

์ฝ”๋“œ ์˜ˆ์‹œ (Node.js / TypeScript)

Command

// command
class CreateOrderCommand {
  constructor(
    public readonly userId: string,
    public readonly items: OrderItem[],
  ) {}
}

// handler
class CreateOrderHandler {
  async execute(command: CreateOrderCommand): Promise<string> {
    const order = Order.create(command.userId, command.items);
    await this.orderRepository.save(order);
    await this.eventBus.publish(new OrderCreatedEvent(order.id));
    return order.id;
  }
}

Query

// query
class GetUserOrdersQuery {
  constructor(public readonly userId: string) {}
}

// handler โ€” Read DB์—์„œ ๋ฐ”๋กœ flat DTO ์กฐํšŒ
class GetUserOrdersHandler {
  async execute(query: GetUserOrdersQuery): Promise<OrderSummaryDto[]> {
    return this.db.query(
      `SELECT id, status, total_price, created_at
       FROM orders_view
       WHERE user_id = $1
       ORDER BY created_at DESC`,
      [query.userId],
    );
  }
}

Query ํ•ธ๋“ค๋Ÿฌ๋Š” ๋„๋ฉ”์ธ ๋ชจ๋ธ(Order ํด๋ž˜์Šค)์„ ์ „ํ˜€ ๊ฑฐ์น˜์ง€ ์•Š๋Š”๋‹ค. ํ™”๋ฉด์— ํ•„์š”ํ•œ ํ˜•ํƒœ ๊ทธ๋Œ€๋กœ ๊ฐ€์ ธ์˜จ๋‹ค.


Read Model ๋™๊ธฐํ™” ๋ฐฉ๋ฒ•

Write DB์™€ Read DB๋ฅผ ๋ถ„๋ฆฌํ•  ๋•Œ ๋‘ ๋ชจ๋ธ์„ ์–ด๋–ป๊ฒŒ ๋™๊ธฐํ™”ํ•˜๋А๋ƒ๊ฐ€ ํ•ต์‹ฌ ๊ณผ์ œ๋‹ค.

1. ๋™๊ธฐ ์—…๋ฐ์ดํŠธ

Command๊ฐ€ ์ฒ˜๋ฆฌ๋œ ์งํ›„ ๊ฐ™์€ ํŠธ๋žœ์žญ์…˜ ๋˜๋Š” ์งํ›„ ํ˜ธ์ถœ๋กœ Read Model์„ ์—…๋ฐ์ดํŠธ.

  • ์žฅ์ : ๊ตฌํ˜„์ด ๋‹จ์ˆœ, ์ผ๊ด€์„ฑ ๋ณด์žฅ
  • ๋‹จ์ : Command ์ฒ˜๋ฆฌ ์„ฑ๋Šฅ์— ์˜ํ–ฅ

2. ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ๋น„๋™๊ธฐ ์—…๋ฐ์ดํŠธ

๋„๋ฉ”์ธ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•˜๊ณ , ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๊ฐ€ Read Model์„ ๋ณ„๋„๋กœ ๊ฐฑ์‹ .

Order ์ €์žฅ
  โ””โ”€ OrderCreatedEvent ๋ฐœํ–‰
       โ””โ”€ (๋น„๋™๊ธฐ) OrderReadModelUpdater โ†’ Read DB ๊ฐฑ์‹ 
  • ์žฅ์ : Command ์„ฑ๋Šฅ์— ์˜ํ–ฅ ์—†์Œ, Read/Write DB๋ฅผ ์™„์ „ํžˆ ๋‹ค๋ฅธ ์ €์žฅ์†Œ(์˜ˆ: MySQL โ†” Elasticsearch)๋กœ ๋ถ„๋ฆฌ ๊ฐ€๋Šฅ
  • ๋‹จ์ : ์ตœ์ข… ์ผ๊ด€์„ฑ(Eventual Consistency) โ€” ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ์ „๊นŒ์ง€ Read Model์ด ์˜ค๋ž˜๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•  ์ˆ˜ ์žˆ์Œ

3. Event Sourcing๊ณผ ๊ฒฐํ•ฉ

Write Model์€ ํ˜„์žฌ ์ƒํƒœ ๋Œ€์‹  ์ด๋ฒคํŠธ ๋กœ๊ทธ๋งŒ ์ €์žฅ. Read Model์€ ์ด๋ฒคํŠธ๋ฅผ ์žฌ์ƒ(replay)ํ•ด์„œ ๋ทฐ๋ฅผ ๋งŒ๋“ ๋‹ค.

CQRS์™€ Event Sourcing์€ ๋…๋ฆฝ์ ์ธ ํŒจํ„ด์ด์ง€๋งŒ ํ•จ๊ป˜ ์ž์ฃผ ์‚ฌ์šฉ๋œ๋‹ค.


์–ธ์ œ ๋„์ž…ํ•˜๋ฉด ์ข‹์€๊ฐ€

CQRS๊ฐ€ ์ ํ•ฉํ•œ ๊ฒฝ์šฐ:

  • ์ฝ๊ธฐ/์“ฐ๊ธฐ ํŠธ๋ž˜ํ”ฝ ๋ถˆ๊ท ํ˜•์ด ์‹ฌํ•œ ์„œ๋น„์Šค
  • ์กฐํšŒ ์š”๊ตฌ์‚ฌํ•ญ์ด ๋ณต์žกํ•˜๊ณ  ๋‹ค์–‘ํ•œ ๊ฒฝ์šฐ (๋Œ€์‹œ๋ณด๋“œ, ๊ฒ€์ƒ‰, ๋ฆฌํฌํŠธ)
  • MSA์—์„œ ์„œ๋น„์Šค๋ณ„๋กœ ์ฝ๊ธฐ ์ „์šฉ ๋ชจ๋ธ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ
  • Event Sourcing ๋„์ž…์„ ๊ณ ๋ ค ์ค‘์ธ ๊ฒฝ์šฐ

CQRS๊ฐ€ ๊ณผํ•œ ๊ฒฝ์šฐ:

  • ๋‹จ์ˆœ CRUD ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜
  • ํŒ€ ๊ทœ๋ชจ๊ฐ€ ์ž‘๊ณ  ๋ณต์žก๋„๋ฅผ ๊ฐ๋‹นํ•˜๊ธฐ ์–ด๋ ค์šด ๊ฒฝ์šฐ
  • ๊ฐ•ํ•œ ์ผ๊ด€์„ฑ์ด ๋ฐ˜๋“œ์‹œ ํ•„์š”ํ•œ ๊ฒฝ์šฐ (์ตœ์ข… ์ผ๊ด€์„ฑ์„ ํ—ˆ์šฉํ•˜๊ธฐ ์–ด๋ ค์šธ ๋•Œ)

์žฅ๋‹จ์  ์ •๋ฆฌ

์žฅ์ 

  • ์ฝ๊ธฐ ์„ฑ๋Šฅ ์ตœ์ ํ™”: Read Model์„ ์กฐํšŒ์— ํŠนํ™”๋œ ํ˜•ํƒœ๋กœ ์„ค๊ณ„ ๊ฐ€๋Šฅ
  • ๋„๋ฉ”์ธ ๋ชจ๋ธ ๋‹จ์ˆœํ™”: Command ์ชฝ ๋„๋ฉ”์ธ ๋ชจ๋ธ์ด ์กฐํšŒ ์š”๊ฑด์— ์˜ค์—ผ๋˜์ง€ ์•Š์Œ
  • ๋…๋ฆฝ์  ํ™•์žฅ: Read/Write๋ฅผ ๋ณ„๋„ ์ธํ”„๋ผ๋กœ ์Šค์ผ€์ผ ์•„์›ƒ ๊ฐ€๋Šฅ
  • ๋ช…ํ™•ํ•œ ์˜๋„ ํ‘œํ˜„: Command ์ด๋ฆ„์ด ๋น„์ฆˆ๋‹ˆ์Šค ์–ธ์–ด๋ฅผ ์ง์ ‘ ํ‘œํ˜„ (PlaceOrder, CancelReservation)

๋‹จ์ 

  • ๋ณต์žก๋„ ์ฆ๊ฐ€: ํด๋ž˜์Šค, ํ•ธ๋“ค๋Ÿฌ, ์ด๋ฒคํŠธ ์ˆ˜๊ฐ€ ๋Š˜์–ด๋‚จ
  • ์ตœ์ข… ์ผ๊ด€์„ฑ: ๋น„๋™๊ธฐ ๋™๊ธฐํ™” ์‹œ Read Model ์ง€์—ฐ ๋ฐœ์ƒ ๊ฐ€๋Šฅ
  • ์šด์˜ ๋ณต์žก๋„: Write/Read DB๊ฐ€ ๋ถ„๋ฆฌ๋˜๋ฉด ๊ด€๋ฆฌ ํฌ์ธํŠธ ์ฆ๊ฐ€
  • ํ•™์Šต ๋น„์šฉ: ํŒ€ ์ „์ฒด๊ฐ€ ํŒจํ„ด์„ ์ดํ•ดํ•ด์•ผ ํšจ๊ณผ์ ์œผ๋กœ ๋™์ž‘

์š”์•ฝ

CQRS๋Š” ์ฝ๊ธฐ์™€ ์“ฐ๊ธฐ์˜ ๊ด€์‹ฌ์‚ฌ๋ฅผ ๋ถ„๋ฆฌํ•จ์œผ๋กœ์จ ๊ฐ๊ฐ์— ๋งž๋Š” ์ตœ์ ํ™”๋ฅผ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•˜๋Š” ํŒจํ„ด์ด๋‹ค. ๋‹จ์ˆœํ•œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—๋Š” ์˜ค๋ฒ„์—”์ง€๋‹ˆ์–ด๋ง์ด์ง€๋งŒ, ๋ณต์žกํ•œ ๋„๋ฉ”์ธ ๋กœ์ง๊ณผ ๋‹ค์–‘ํ•œ ์กฐํšŒ ์š”๊ฑด์ด ์ถฉ๋Œํ•˜๋Š” ์‹œ์Šคํ…œ์—์„œ๋Š” ์œ ์ง€๋ณด์ˆ˜์„ฑ๊ณผ ์„ฑ๋Šฅ ๋ชจ๋‘๋ฅผ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ๋‹ค.