๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
ํ”„๋กœ์ ํŠธ ํšŒ๊ณ /ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…

์กฐํšŒ ๋ชจ๋ธ(Read Model)์˜ ๊ฐ€์‹œ์„ฑ ์ฒ˜๋ฆฌ ์ „๋žต: ์‹ค์‹œ๊ฐ„ ๋ฐ˜์˜ vs ์ž์ • ๋ฐฐ์น˜์˜ Trade-off์™€ ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์•„ํ‚คํ…์ฒ˜ ๊ตฌ์ถ•

by ์ ผ์ ผ39 2026. 6. 5.
  • ๋ถ„์•ผ: MSA, CQRS Pattern, Read Model Optimization, Performance Tuning, Hybrid Visibility Strategy
  • ๊ด€๋ จ ํŒŒ์ผ: ProductViewCommandService.java, ProductViewRepository.java, StockEventConsumer.java, DailyStockResetEvent.java

1. ๋ฐœ๋‹จ: ์กฐํšŒ ์„œ๋น„์Šค(Catalog)์˜ '๊ฐ€์‹œ์„ฑ ๋ณด์žฅ'๊ณผ 'I/O ๋ถ€ํ•˜'์˜ ๋”œ๋ ˆ๋งˆ

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

  • ๊ธฐ์กด ๊ตฌ์กฐ์˜ ๋ณ‘๋ชฉ: ์ƒํ’ˆ ์ •๋ณด ์ˆ˜์ •, ํ’ˆ์ ˆ, ์ž์ • ์žฌ๊ณ  ์ดˆ๊ธฐํ™” ๋“ฑ์˜ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•  ๋•Œ๋งˆ๋‹ค ๋ชฝ๊ณ DB์— ์‹ค์‹œ๊ฐ„ Write(CUD) ์—ฐ์‚ฐ์ด ์Ÿ์•„์ง

→ ์ด๋กœ ์ธํ•ด Read ์ „์šฉ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—ฌ์•ผ ํ•  ๋ชฝ๊ณ DB์˜ I/O ์ž์›์ด ๋Œ€๋Ÿ‰์˜ Write ํŠธ๋ž˜ํ”ฝ์— ์ ์œ ๋‹นํ•˜๋ฉด์„œ, ์ •์ž‘ ๋ฉ”์ธ ํ™”๋ฉด์„ ์กฐํšŒํ•˜๋Š” ๊ณ ๊ฐ๋“ค์˜ ์กฐํšŒ ์„ฑ๋Šฅ์ด ์ €ํ•˜๋˜๋Š” ์—ญ์„ค์ ์ธ ํ˜„์ƒ์ด ๋ฐœ์ƒํ•จ.

2. ๋ฌธ์ œ ์ธ์‹ ๋ฐ ๋Œ€์•ˆ๋ณ„ Trade-off ๋ถ„์„

์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ์นดํƒˆ๋กœ๊ทธ์˜ ๊ฐ€์‹œ์„ฑ ์—…๋ฐ์ดํŠธ ์ฃผ๊ธฐ๋ฅผ ์กฐ์ •ํ•˜๋Š” ์„ธ ๊ฐ€์ง€ ์•„ํ‚คํ…์ฒ˜ ๋Œ€์•ˆ์„ ๊ฒ€ํ† ํ•จ.

์•„ํ‚คํ…์ฒ˜ ๋Œ€์•ˆ ๊ตฌํ˜„ ํŠน์ง• ์žฅ์  ์น˜๋ช…์ ์ธ ๋‹จ์  (Trade-off)
๋Œ€์•ˆ A: ์‹ค์‹œ๊ฐ„ ์ „๋‹จ์ง€
(๊ธฐ์กด ์ƒํƒœ)
์ด๋ฒคํŠธ๋ฅผ ๋ฐ›๋Š” ์ฆ‰์‹œ ๋ชฝ๊ณ DB ์ƒํƒœ(isVisible, status)๋ฅผ ๋ฌด์กฐ๊ฑด ์‹ค์‹œ๊ฐ„ ์ˆ˜์ • ๋ฐ ๋ฐ˜์˜ ๋ฐ์ดํ„ฐ ์ •ํ•ฉ์„ฑ์ด ๊ฑฐ์˜ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์œ ์ง€๋จ ๋Œ€๊ทœ๋ชจ ํŠธ๋ž˜ํ”ฝ ์‹œ ์ฃผ๋ฌธ/์žฌ๊ณ  ์„œ๋น„์Šค์˜ ์ด๋ฒคํŠธ ํญ์ฃผ๋กœ ๋ชฝ๊ณ DB Write ๋ณ‘๋ชฉ ⇒ ์กฐํšŒ ์„ฑ๋Šฅ ์ €ํ•˜
๋Œ€์•ˆ B: ๊ณ ์ • ์ „๋‹จ์ง€st ์‹ค์‹œ๊ฐ„ ์ด๋ฒคํŠธ ์ˆ˜์‹ ์„ ์™„์ „ํžˆ ๋ฐฐ์ œํ•˜๊ณ , ์ž์ • ๋ฐฐ์น˜ ์Šค์ผ€์ค„๋Ÿฌ๋ฅผ ํ†ตํ•ด์„œ๋งŒ ์ผ๊ด„ ์—…๋ฐ์ดํŠธ ์นดํƒˆ๋กœ๊ทธ ์„œ๋น„์Šค๊ฐ€ ๊ทน๋„๋กœ ๊ฐ€๋ฒผ์›Œ์ง€๋ฉฐ ์ตœ๊ณ ์˜ ์กฐํšŒ ์„ฑ๋Šฅ ํ™•๋ณด ๋‚ฎ 11์‹œ์— ํ’ˆ์ ˆ๋œ ์ƒํ’ˆ์ด ๋ฐค 11์‹œ 59๋ถ„๊นŒ์ง€ ๋ฉ”์ธ์— ๋…ธ์ถœ๋จ. ๊ณ ๊ฐ์ด ๊ฒฐ์ œ์ฐฝ๊นŒ์ง€ ๊ฐ€์•ผ ํŠ•๊น€
๋Œ€์•ˆ C: ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์ „๋žต
(์ตœ์ข… ์ฑ„ํƒ)
1. ์ƒํƒœ๊ฐ’ ๋ณ€๊ฒฝ(๊ธฐ๊ฐ„ ๋งŒ๋ฃŒ ๋“ฑ): ์ž์ • ๋ฐฐ์น˜๋กœ ๋ฌด๊ฒ๊ฒŒ ๊ฐฑ์‹ 
2. ํ’ˆ์ ˆ/๋ณต๊ตฌ: ์ด๋ฒคํŠธ ์ˆ˜์‹ ํ•˜๋˜ ์บ์‹œ ๋ฌดํšจํ™”๋กœ Read ๋ถ€ํ•˜ ์ปท
Read ๋ถ€ํ•˜๋ฅผ ์ตœ์†Œํ™”ํ•˜๋ฉด์„œ ๊ณ ๊ฐ์—๊ฒŒ ํ’ˆ์ ˆ ๋ฐ์ดํ„ฐ ํŒŒํŽธํ™”๋ฅผ ๋ณด์—ฌ์ฃผ์ง€ ์•Š์Œ ๋ช…๋ น(Command)๊ณผ ์กฐํšŒ(Query) ํŒŒ์ดํ”„๋ผ์ธ์˜ ๋ณต์žก๋„ ์ฆ๊ฐ€

 

3. ํ•ด๊ฒฐ ๊ณผ์ •: ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ๊ฐ€์‹œ์„ฑ ์ „๋žต์„ ์œ„ํ•œ ๋ชฝ๊ณ DB ๋ฐฐ์น˜ ์ฟผ๋ฆฌ ์ตœ์ ํ™”

๋Œ€์•ˆ C(ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์ „๋žต)๋ฅผ ์œ„ํ•ด ๋จผ์ €, ์ž์ • ๋ฐฐ์น˜๊ฐ€ ๋Œ ๋•Œ ๋ชฝ๊ณ DB์˜ ์ˆ˜๋งŒ ๊ฑด ๋ฌธ์„œ ์ „์ฒด๋ฅผ ํ’€ ์Šค์บ”(findAll())ํ•˜์—ฌ ์žฌ๊ณ ๋ฅผ ๋ฆฌ์…‹ํ•˜๋˜ ๊ธฐ์กด์˜ ๋ฌด๊ฑฐ์šด ๋กœ์ง์„ ์ˆ˜์ •ํ–ˆ์Œ. ์ˆจ๊น€(HIDDEN) ์ฒ˜๋ฆฌ๋œ ํœด๋ฉด ์ƒํ’ˆ๊นŒ์ง€ ์ „๋ถ€ ๋ฉ”๋ชจ๋ฆฌ์— ์˜ฌ๋ ค OOM์„ ์œ ๋ฐœํ•˜๋˜ ์ฝ”๋“œ๋ฅผ ๊ฑท์–ด๋ƒ„.

1) ํŠน์ • ์ƒํƒœ ํƒ€๊ฒŸํŒ… ์ฟผ๋ฆฌ ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€ (ProductViewRepository.java)

// [Before] ๊ธฐ์กด ์ „์ฒด ์กฐํšŒ ์Šค์บ”
Page<ProductView> findAll(Pageable pageable);

// [After] ์ธ๋ฑ์Šค ๊ธฐ๋ฐ˜ ํŠน์ • ์ƒํƒœ ํƒ€๊ฒŸํŒ… ์ฟผ๋ฆฌ๋กœ ๊ฐœ์„ 
Page<ProductView> findByStatusIn(List<String> statuses, Pageable pageable);

2) ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ๋ฒ”์œ„ ์ถ•์†Œ ๋ฐ ๋ฏธ์‚ฌ์šฉ ํ•„๋“œ ์†Œ๊ฑฐ (ProductViewCommandService.java)

์ž์ • ์žฌ๊ณ  ์ดˆ๊ธฐํ™” ์‹œ, ๋ฉ”์ธ ํ™”๋ฉด ๋…ธ์ถœ๊ณผ ๋ฐ€์ ‘ํ•œ ACTIVE, SOLDOUT ์ƒํƒœ์˜ ๋„ํ๋จผํŠธ๋งŒ ํ•€ํฌ์ธํŠธ๋กœ ํ•ด์‹ฑํ•˜์—ฌ Chunk ๋‹จ์œ„(100๊ฑด)๋กœ ์•ˆ์ „ํ•˜๊ฒŒ ๋Š์–ด ์ฒ˜๋ฆฌํ•˜๋„๋ก ๋กœ์ง์„ ๋ฆฌํŒฉํ„ฐ๋งํ–ˆ์Œ.

public void resetDailyStock(DailyStockResetEvent event) {
    List<String> targetStatuses = List.of("ACTIVE", "SOLDOUT");
    PageRequest pageRequest = PageRequest.of(0, 100);
    Page<ProductView> page;

    do {
        page = productViewRepository.findByStatusIn(targetStatuses, pageRequest); // ์ตœ์ ํ™” ์ฟผ๋ฆฌ ํ˜ธ์ถœ!
        // ... ์•ˆ์ „ํ•œ ๋ฌถ์Œ ์ €์žฅ(saveAll) ์ง„ํ–‰
        pageRequest = pageRequest.next();
    } while (page.hasNext());
}

4. ์ตœ์ข… ์„ฑ๊ณผ

  • I/O ๋ฐ ๋„คํŠธ์›Œํฌ ๋น„์šฉ ์ ˆ๊ฐ: ๋ถˆํ•„์š”ํ•œ ๋ฏธ์‚ฌ์šฉ ํ•„๋“œ(updatedOptionCount)๋ฅผ DTO์—์„œ ์ œ๊ฑฐํ•˜์—ฌ ์นดํ”„์นด ๋„คํŠธ์›Œํฌ ํŽ˜์ด๋กœ๋“œ ๋Œ€์—ญํญ ์ตœ์ ํ™”
  • ํ’€ ์Šค์บ” ์ฐจ๋‹จ: ์ž์ • ์žฌ๊ณ  ๋ฆฌ์…‹ ์Šค์ผ€์ค„๋Ÿฌ ๊ตฌ๋™ ์‹œ ์ˆ˜์ฒœ/์ˆ˜๋งŒ ๊ฑด์˜ ํœด๋ฉด ๋„ํ๋จผํŠธ๋ฅผ ๋ฌด์‹œํ•˜๊ณ  ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋งŒ ์กฐํšŒํ•จ์œผ๋กœ์จ ๋ฐฐ์น˜ IOPS ์„ฑ๋Šฅ ๋Œ€ํญ ํ–ฅ์ƒ ๋ฐ OOM ์›์ฒœ ์ฐจ๋‹จ