๐Ÿš€ ์ฝ๊ธฐ ์ „์šฉ ์„œ๋ฒ„๋ฅผ ๋”ฐ๋กœ ๋‘”๋‹ค๊ณ ?! CQRS ํŒจํ„ด ์‰ฝ๊ฒŒ ์ดํ•ดํ•˜๊ธฐ

์„ํ˜„ยท2025๋…„ 3์›” 4์ผ
0

Insight

๋ชฉ๋ก ๋ณด๊ธฐ
26/43
post-thumbnail

๐Ÿค” CQRS ํŒจํ„ด์ด ๋ญ๊ธธ๋ž˜?

๊ฐœ๋ฐœ์„ ํ•˜๋‹ค ๋ณด๋ฉด ์ด๋Ÿฐ ๊ณ ๋ฏผ์ด ๋“ค ๋•Œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

"์กฐํšŒ ์š”์ฒญ์ด ๋„ˆ๋ฌด ๋งŽ์•„์„œ DB๊ฐ€ ๋ฒ„๋ฒ…๊ฑฐ๋ฆฌ๋Š” ๊ฑธ ํ•ด๊ฒฐํ•  ๋ฐฉ๋ฒ•์ด ์—†์„๊นŒ?"
"๋ช…๋ น๊ณผ ์กฐํšŒ๊ฐ€ ๊ฐ™์€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ๊ณต์œ ํ•˜๋‹ค ๋ณด๋‹ˆ ์„ฑ๋Šฅ์ด ์ €ํ•˜๋˜๋„คโ€ฆ"

์ด๋Ÿฐ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋“ฑ์žฅํ•œ ํŒจํ„ด์ด CQRS(Command Query Responsibility Segregation)์ž…๋‹ˆ๋‹ค.
๋ง ๊ทธ๋Œ€๋กœ, ๋ช…๋ น(Command)๊ณผ ์กฐํšŒ(Query)์˜ ์ฑ…์ž„์„ ๋ถ„๋ฆฌํ•˜๋Š” ์•„ํ‚คํ…์ฒ˜ ํŒจํ„ด์ž…๋‹ˆ๋‹ค.

์ฆ‰, ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” ์š”์ฒญ(๋ช…๋ น)๊ณผ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•˜๋Š” ์š”์ฒญ(์กฐํšŒ)์„ ๋ถ„๋ฆฌํ•ด์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ์‹์ด์ฃ .


๐Ÿ“Œ CQRS ํŒจํ„ด์„ ์™œ ์‚ฌ์šฉํ• ๊นŒ?

๊ธฐ์กด์˜ ์ผ๋ฐ˜์ ์ธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์•„ํ‚คํ…์ฒ˜์—์„œ๋Š” ํ•˜๋‚˜์˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ๋ช…๋ น๊ณผ ์กฐํšŒ๊ฐ€ ํ•จ๊ป˜ ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ์ด๋ ‡๊ฒŒ ๋˜๋ฉด ๋ช‡ ๊ฐ€์ง€ ๋ฌธ์ œ์ ์ด ์ƒ๊น๋‹ˆ๋‹ค.

  • ๐Ÿšจ ์กฐํšŒ ์š”์ฒญ์ด ๋งŽ์œผ๋ฉด DB๊ฐ€ ๋ฒ„๋ฒ…๊ฑฐ๋ฆฐ๋‹ค โ†’ ํŠธ๋ž˜ํ”ฝ์ด ์ฆ๊ฐ€ํ• ์ˆ˜๋ก ์„ฑ๋Šฅ ์ €ํ•˜
  • ๐Ÿ”„ ๋ช…๋ น๊ณผ ์กฐํšŒ๊ฐ€ ๊ฐ™์€ ๋ชจ๋ธ์„ ๊ณต์œ  โ†’ ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ์–ด๋ ต๊ณ , ๋ณ€๊ฒฝ์ด ํž˜๋“ฆ
  • ๐Ÿ”— DB ๋ฝ(Lock) ์ด์Šˆ โ†’ ํŠธ๋žœ์žญ์…˜์„ ์œ ์ง€ํ•ด์•ผ ํ•ด์„œ ์„ฑ๋Šฅ ์ €ํ•˜ ๋ฐœ์ƒ

์ด๋Ÿฐ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด CQRS๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ฝ๊ธฐ์™€ ์“ฐ๊ธฐ๋ฅผ ๋ถ„๋ฆฌํ•˜์—ฌ ์ตœ์ ํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค! ๐Ÿš€


๐Ÿ— CQRS ํŒจํ„ด์˜ ๊ธฐ๋ณธ ๊ตฌ์กฐ

CQRS ํŒจํ„ด์€ ํฌ๊ฒŒ Command(๋ช…๋ น)๊ณผ Query(์กฐํšŒ)๋กœ ๋‚˜๋‰ฉ๋‹ˆ๋‹ค.

1๏ธโƒฃ Command(๋ช…๋ น)

  • ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€๊ฒฝ(์ƒ์„ฑ, ์ˆ˜์ •, ์‚ญ์ œ)ํ•˜๋Š” ์—ญํ• 
  • ์˜ˆ: ์ฃผ๋ฌธ ์ƒ์„ฑ, ์ƒํ’ˆ ๋“ฑ๋ก, ํšŒ์›๊ฐ€์ž…

2๏ธโƒฃ Query(์กฐํšŒ)

  • ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ๊ธฐ ์ „์šฉ์œผ๋กœ ์กฐํšŒํ•˜๋Š” ์—ญํ• 
  • ์˜ˆ: ์ฃผ๋ฌธ ๋‚ด์—ญ ์กฐํšŒ, ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ

๐Ÿ–ผ ์‰ฝ๊ฒŒ ํ‘œํ˜„ํ•˜๋ฉด?

์‚ฌ์šฉ์ž โ†’ Command โ†’ DB (๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ)
์‚ฌ์šฉ์ž โ†’ Query โ†’ ๋ณ„๋„ DB ๋˜๋Š” ์บ์‹œ (๋ฐ์ดํ„ฐ ์กฐํšŒ)

์ด๋ ‡๊ฒŒ ์ฝ๊ธฐ์™€ ์“ฐ๊ธฐ๋ฅผ ๋ถ„๋ฆฌํ•˜๋ฉด, ์กฐํšŒ ์š”์ฒญ์ด ๋งŽ๋”๋ผ๋„ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ์—๋Š” ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š์œผ๋ฉฐ, ์„ฑ๋Šฅ์ด ํ–ฅ์ƒ๋ฉ๋‹ˆ๋‹ค.


โœ๏ธ ์˜ˆ์ œ ์ฝ”๋“œ (Spring Boot)

1๏ธโƒฃ Command(๋ช…๋ น) - ์ฃผ๋ฌธ ์ƒ์„ฑ

@RestController
@RequestMapping("/orders")
public class OrderCommandController {
    private final OrderService orderService;

    public OrderCommandController(OrderService orderService) {
        this.orderService = orderService;
    }

    @PostMapping
    public ResponseEntity<Order> createOrder(@RequestBody OrderRequest request) {
        Order order = orderService.createOrder(request);
        return ResponseEntity.ok(order);
    }
}

2๏ธโƒฃ Query(์กฐํšŒ) - ์ฃผ๋ฌธ ๋‚ด์—ญ ์กฐํšŒ

@RestController
@RequestMapping("/orders")
public class OrderQueryController {
    private final OrderQueryService orderQueryService;

    public OrderQueryController(OrderQueryService orderQueryService) {
        this.orderQueryService = orderQueryService;
    }

    @GetMapping("/{id}")
    public ResponseEntity<OrderResponse> getOrder(@PathVariable Long id) {
        OrderResponse order = orderQueryService.getOrderById(id);
        return ResponseEntity.ok(order);
    }
}

๐Ÿš€ ์ฐจ์ด์ 

  • OrderCommandController โ†’ ์ฃผ๋ฌธ์„ ์ƒ์„ฑํ•˜๋Š” ๋ช…๋ น์šฉ API
  • OrderQueryController โ†’ ์ฃผ๋ฌธ์„ ์กฐํšŒํ•˜๋Š” ์กฐํšŒ์šฉ API

์ฆ‰, ์ฝ๊ธฐ์™€ ์“ฐ๊ธฐ๋ฅผ ์™„์ „ํžˆ ๋ถ„๋ฆฌํ•˜์—ฌ ๊ฐ๊ฐ์˜ ์—ญํ• ์„ ๋‚˜๋ˆˆ ๊ฒƒ์ž…๋‹ˆ๋‹ค.


โœ… CQRS ํŒจํ„ด์˜ ์žฅ์ 

โœ… ์ฝ๊ธฐ/์“ฐ๊ธฐ ์„ฑ๋Šฅ ์ตœ์ ํ™” โ†’ ์กฐํšŒ DB๋ฅผ ๋ถ„๋ฆฌํ•˜์—ฌ ์ฝ๊ธฐ ์„ฑ๋Šฅ์„ ๊ทน๋Œ€ํ™”ํ•  ์ˆ˜ ์žˆ์Œ
โœ… ํ™•์žฅ์„ฑ(Scalability) ํ–ฅ์ƒ โ†’ ์ฝ๊ธฐ์™€ ์“ฐ๊ธฐ๋ฅผ ๋ณ„๋„๋กœ ํ™•์žฅ ๊ฐ€๋Šฅ
โœ… ๋ฐ์ดํ„ฐ ๋ชจ๋ธ ์ตœ์ ํ™” ๊ฐ€๋Šฅ โ†’ ์กฐํšŒ์šฉ DB๋Š” NoSQL, ElasticSearch ๋“ฑ์„ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์Œ
โœ… ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๋‹จ์ˆœํ™” โ†’ ๋ช…๋ น๊ณผ ์กฐํšŒ์˜ ์—ญํ• ์„ ๋ถ„๋ฆฌํ•˜์—ฌ ์œ ์ง€๋ณด์ˆ˜ ์šฉ์ด


โŒ CQRS ํŒจํ„ด์˜ ๋‹จ์ 

โŒ ์„ค๊ณ„ ๋ณต์žก์„ฑ ์ฆ๊ฐ€ โ†’ ๊ธฐ์กด๋ณด๋‹ค ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ๊ฐ€ ๋ณต์žกํ•ด์ง
โŒ ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™” ๋ฌธ์ œ โ†’ ๋ช…๋ น๊ณผ ์กฐํšŒ ๋ฐ์ดํ„ฐ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์„ ๊ฐ€๋Šฅ์„ฑ
โŒ ์ถ”๊ฐ€์ ์ธ ์ธํ”„๋ผ ํ•„์š” โ†’ ์ฝ๊ธฐ/์“ฐ๊ธฐ DB๋ฅผ ๋”ฐ๋กœ ์šด์˜ํ•ด์•ผ ํ•จ
โŒ Replica DB ๋™๊ธฐํ™” ์ง€์—ฐ ๋ฌธ์ œ โ†’ ํŠนํžˆ ๋ฆฌ์ „๋ณ„๋กœ Replica DB๋ฅผ ๊ตฌ์„ฑํ•  ๊ฒฝ์šฐ(์ œ ๊ฒฝํ—˜๋‹ด ์ž…๋‹ˆ๋‹ค), ์˜ˆ๋ฅผ ๋“ค์–ด ๋Ÿฐ๋˜์—์„œ ์ผ๋ณธ๊นŒ์ง€ ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™”์— 1.3์ดˆ ์ด์ƒ์ด ๊ฑธ๋ฆด ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๊ฒฝ์šฐ, Insert ํ›„ ๋ฐ”๋กœ Select๋ฅผ ํ•˜๋ฉด Read DB์—๋Š” ํ•ด๋‹น ๋ฐ์ดํ„ฐ๊ฐ€ ์•„์ง ์กด์žฌํ•˜์ง€ ์•Š์•„ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋ ค๋ฉด ์ฆ‰์‹œ ํ˜ธ์ถœ์„ ์ œํ•œํ•˜๊ฑฐ๋‚˜, Kafka๋ฅผ ํ™œ์šฉํ•œ ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ๋™๊ธฐํ™”, ๋˜๋Š” ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ์ ์ ˆํ•œ ์บ์‹ฑ ์ฒ˜๋ฆฌ๋ฅผ ๊ณ ๋ คํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ๊ณ ์„ฑ๋Šฅ์ด ํ•„์š”ํ•œ ์‹œ์Šคํ…œ(์˜ˆ: ์ „์ž์ƒ๊ฑฐ๋ž˜, ๊ธˆ์œต, SNS ๋“ฑ)์—์„œ๋Š” CQRS ํŒจํ„ด์ด ๋งค์šฐ ์œ ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค! ๐Ÿ†


๐Ÿ“Œ CQRS ํŒจํ„ด์„ ์–ธ์ œ ์‚ฌ์šฉํ•ด์•ผ ํ• ๊นŒ?

๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ƒํ™ฉ์—์„œ CQRS ํŒจํ„ด์„ ๊ณ ๋ คํ•˜๋ฉด ์ข‹์Šต๋‹ˆ๋‹ค.

โœ” ์ฝ๊ธฐ/์“ฐ๊ธฐ ํŠธ๋ž˜ํ”ฝ์ด ๋น„๋Œ€์นญ์ ์ธ ๊ฒฝ์šฐ โ†’ ์ฝ๊ธฐ ์š”์ฒญ์ด ํ›จ์”ฌ ๋งŽ์€ ์„œ๋น„์Šค (ex: ๋‰ด์Šค ์‚ฌ์ดํŠธ, ๊ฒ€์ƒ‰ ์—”์ง„)
โœ” ๊ณ ์„ฑ๋Šฅ ์กฐํšŒ๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ โ†’ ElasticSearch, Redis ๋“ฑ์„ ํ™œ์šฉํ•œ ๊ฒ€์ƒ‰ ์„œ๋น„์Šค
โœ” ๋ณต์žกํ•œ ๋„๋ฉ”์ธ ๋กœ์ง์ด ์žˆ๋Š” ๊ฒฝ์šฐ โ†’ ์ด๋ฒคํŠธ ์†Œ์‹ฑ(Event Sourcing)๊ณผ ๊ฒฐํ•ฉํ•˜์—ฌ ์‚ฌ์šฉ
โœ” ํ™•์žฅ์„ฑ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ โ†’ ์ฝ๊ธฐ ์„œ๋ฒ„์™€ ์“ฐ๊ธฐ ์„œ๋ฒ„๋ฅผ ๊ฐœ๋ณ„์ ์œผ๋กœ ํ™•์žฅ ๊ฐ€๋Šฅ


๐ŸŽฏ ์ •๋ฆฌ

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

๐Ÿš€ CQRS ํŒจํ„ด์„ ์ ์šฉํ•˜๋ฉด ๋” ๋น ๋ฅด๊ณ  ์•ˆ์ •์ ์ธ ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋ชจ๋“  ํ”„๋กœ์ ํŠธ์— ์ ํ•ฉํ•œ ๊ฒƒ์€ ์•„๋‹ˆ๋ฏ€๋กœ, ์‹œ์Šคํ…œ์˜ ์š”๊ตฌ์‚ฌํ•ญ์— ๋”ฐ๋ผ ์‹ ์ค‘ํ•˜๊ฒŒ ๋„์ž…ํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค! ๐Ÿ˜Š

0๊ฐœ์˜ ๋Œ“๊ธ€