From e11010f414ae23c73e35f80dec153b3820341704 Mon Sep 17 00:00:00 2001 From: katiekim17 Date: Mon, 30 Mar 2026 02:23:59 +0900 Subject: [PATCH] =?UTF-8?q?doc:=20=EC=84=A4=EA=B3=84=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CONTEXT.md | 313 +++++++++++++++++++++ docs/black-friday-simulator.md | 326 ++++++++++++++++++++++ docs/concepts/01-sorted-set-basic.mermaid | 26 ++ docs/concepts/README.md | 20 ++ http/commerce-api/seed-black-friday.http | 64 +++++ k6/breakpoint.js | 142 ++++++++++ 6 files changed, 891 insertions(+) create mode 100644 CONTEXT.md create mode 100644 docs/black-friday-simulator.md create mode 100644 docs/concepts/01-sorted-set-basic.mermaid create mode 100644 docs/concepts/README.md create mode 100644 http/commerce-api/seed-black-friday.http create mode 100644 k6/breakpoint.js diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 000000000..4c0aeca0c --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,313 @@ +# ๐Ÿ“š Weekly Study Context + +> ์ด ํŒŒ์ผ์€ ์ง‘/ํšŒ์‚ฌ ์–ด๋””์„œ๋“  ๊ฐ™์€ ์ปจํ…์ŠคํŠธ๋ฅผ ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•œ ํŒŒ์ผ์ž…๋‹ˆ๋‹ค. +> ์„ธ์…˜ ์‹œ์ž‘ ์‹œ "์ปจํ…์ŠคํŠธ ๋ถˆ๋Ÿฌ์™€์ค˜" ๋ผ๊ณ  ๋งํ•˜๋ฉด Claude๊ฐ€ ์ด ํŒŒ์ผ์„ ์ฝ๊ณ  ๋ฐ”๋กœ ์ด์–ด์ง‘๋‹ˆ๋‹ค. + +--- + +## ๐ŸŽ“ ํ•™์Šต ์ง„ํ–‰ ๋ฐฉ์‹ + +### ์ˆœ์„œ +``` +1๋‹จ๊ณ„: ๊ฐœ๋… ์žก๊ธฐ โ†’ ๋ชจ๋ฅด๋ฉด Claudeํ•œํ…Œ ๋ฌผ์–ด๋ณด๊ธฐ, Mermaid๋กœ ์ •๋ฆฌ +2๋‹จ๊ณ„: ๊ฐœ๋… ํ€ด์ฆˆ โ†’ ์ค‘๊ฐ„์ค‘๊ฐ„ Claude๊ฐ€ ์งˆ๋ฌธ โ†’ ๋‚ด๊ฐ€ ๋‹ต๋ณ€ โ†’ ํ™•์ธ +3๋‹จ๊ณ„: ์‹œ๋ฎฌ๋ ˆ์ด์…˜ โ†’ Phase 1/2 ์‹คํ–‰ (๋ฌธ์ œ ์ง์ ‘ ๊ฒช๊ธฐ) +4๋‹จ๊ณ„: ์„ค๊ณ„ โ†’ ๊ณ ๋ฏผ ๋ชฉ๋ก โ†’ ๊ฒฐ์ • ์‚ฌํ•ญ์œผ๋กœ ์ด๋™ +5๋‹จ๊ณ„: ์ฝ”๋“œ ๊ตฌํ˜„ โ†’ TDD (Red โ†’ Green โ†’ Refactor) +6๋‹จ๊ณ„: Phase 3 ๊ฒ€์ฆ โ†’ Before/After ์ˆ˜์น˜ ๋น„๊ต +7๋‹จ๊ณ„: PR + ๋ธ”๋กœ๊ทธ + WIL +``` + +### ๊ฐœ๋… ํ•™์Šต ๊ทœ์น™ +- ๊ฐœ๋… ์ •๋ฆฌ ์š”์ฒญ ์‹œ โ†’ Claude๊ฐ€ Mermaid ๋‹ค์ด์–ด๊ทธ๋žจ์œผ๋กœ `docs/concepts/` ์— ์ €์žฅ +- ๊ฐ ๊ฐœ๋… ๋๋‚˜๋ฉด โ†’ Claude๊ฐ€ ํ€ด์ฆˆ 1~2๊ฐœ ์ถœ์ œ, ๋‚ด๊ฐ€ ๋‹ต๋ณ€ ํ›„ ํ™•์ธ +- ํ€ด์ฆˆ ํ†ต๊ณผ ๊ธฐ์ค€ โ†’ ์•„๋ž˜ ๊ฐœ๋… ์ฒดํฌ๋ฆฌ์ŠคํŠธ์— โœ… ํ‘œ์‹œ + +### ๐Ÿ“– ๊ฐœ๋… ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +#### Redis ์ž๋ฃŒ๊ตฌ์กฐ +- [ ] **Sorted Set ๊ธฐ๋ณธ** โ€” ZADD, ZRANK, ZREM, ZCARD, ZSCORE ๋™์ž‘ ์›๋ฆฌ +- [ ] **ZADD NX ์˜ต์…˜** โ€” ์ค‘๋ณต ๋ฐฉ์ง€ ์›๋ฆฌ (์™œ NX๊ฐ€ ๋ฉฑ๋“ฑ์„ฑ์„ ๋ณด์žฅํ•˜๋Š”๊ฐ€) +- [ ] **score ์„ค๊ณ„** โ€” ์ง„์ž… ์ˆœ์„œ ๋ณด์žฅ์„ ์œ„ํ•œ score ์ „๋žต (timestamp vs sequence) +- [ ] **TOCTOU ๋ฌธ์ œ** โ€” ์กฐํšŒ ํ›„ ์กฐ๊ฑด ๋ถ„๊ธฐ ์‹œ ๋ฐœ์ƒํ•˜๋Š” race condition + +#### ๋Œ€๊ธฐ์—ด ์‹œ์Šคํ…œ ์„ค๊ณ„ +- [ ] **๋Œ€๊ธฐ์—ด vs Rate Limiting** โ€” ์–ธ์ œ ์–ด๋–ค ์ „๋žต์„ ์“ฐ๋Š”๊ฐ€ +- [ ] **Thundering Herd** โ€” ๋Œ€๊ธฐ์—ด ์—ด๋ฆด ๋•Œ ๋™์‹œ ์ง„์ž… ํญ๋ฐœ ๋ฌธ์ œ์™€ ๋Œ€์‘ +- [ ] **๋ฐฐ์น˜ ํฌ๊ธฐ N ์‚ฐ์ •** โ€” `N = ์ปค๋„ฅ์…˜ ์ˆ˜ ร— (์ฃผ๊ธฐ / ํ‰๊ท  ์ฒ˜๋ฆฌ ์‹œ๊ฐ„)` ๊ณต์‹ ๊ทผ๊ฑฐ + +#### ํ† ํฐ & TTL +- [ ] **์ž…์žฅ ํ† ํฐ ์„ค๊ณ„** โ€” Redis String + TTL์ด ์™œ ์ ํ•ฉํ•œ๊ฐ€ +- [ ] **TTL ๊ธฐ์ค€ ์‚ฐ์ •** โ€” ๋ช‡ ๋ถ„์œผ๋กœ ์„ค์ •ํ•ด์•ผ ํ•˜๋Š”๊ฐ€, ๊ทธ ๊ทผ๊ฑฐ +- [ ] **ํ† ํฐ ๊ฒ€์ฆ ์œ„์น˜** โ€” Filter vs Interceptor vs AOP ๋น„๊ต + +#### ์‹ค์‹œ๊ฐ„ ์ˆœ๋ฒˆ ์กฐํšŒ +- [ ] **Polling ๊ตฌ์กฐ** โ€” ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์–ด๋–ป๊ฒŒ ์ฃผ๊ธฐ์ ์œผ๋กœ ์กฐํšŒํ•˜๋Š”๊ฐ€ +- [ ] **Polling vs SSE** โ€” ๊ฐ๊ฐ ์–ธ์ œ ์“ฐ๋Š”๊ฐ€, ์™œ Polling์„ ์„ ํƒํ–ˆ๋Š”๊ฐ€ +- [ ] **์˜ˆ์ƒ ๋Œ€๊ธฐ ์‹œ๊ฐ„ ๊ณ„์‚ฐ** โ€” ๊ณต์‹๊ณผ ํ•œ๊ณ„ + +#### ์‹œ์Šคํ…œ ์•ˆ์ •์„ฑ +- [ ] **HikariCP ์ปค๋„ฅ์…˜ ํ’€** โ€” ๊ณ ๊ฐˆ์ด ์™œ ์ƒ๊ธฐ๊ณ  ๋Œ€๊ธฐ์—ด์ด ์–ด๋–ป๊ฒŒ ํ•ด๊ฒฐํ•˜๋Š”๊ฐ€ +- [ ] **Redis ์žฅ์•  ์‹œ๋‚˜๋ฆฌ์˜ค** โ€” Redis ์ฃฝ์œผ๋ฉด ์–ด๋–ป๊ฒŒ ํ•ด์•ผ ํ•˜๋Š”๊ฐ€ + +--- + +## ๐Ÿ—“๏ธ ์ด๋ฒˆ ์ฃผ (2026-03-30 ~ 2026-04-03) + +### ๋งˆ๊ฐ +- **์ฝ”๋“œ PR**: 2026-04-03 (๊ธˆ) 18:00 +- **๋ธ”๋กœ๊ทธ (Technical Writing)**: 2026-04-03 (๊ธˆ) 18:00 +- **WIL (Weekly I Learned)**: 2026-04-03 (๊ธˆ) 18:00 + +### ์ฃผ์ œ +**Redis ๊ธฐ๋ฐ˜ ๋Œ€๊ธฐ์—ด ์‹œ์Šคํ…œ** (Black Friday ์ฃผ๋ฌธ API ์•ž๋‹จ) + +--- + +## ๐Ÿงช Phase 0 โ€” ์‹œ๋ฎฌ๋ ˆ์ด์…˜ (๋ฌธ์ œ ์žฌํ˜„ ๋จผ์ €) + + +### ์‹œ๋ฎฌ๋ ˆ์ด์…˜ 1 โ€” ๋Œ€๊ธฐ์—ด ์—†์ด ์ฃผ๋ฌธ API์— ๋™์‹œ ์š”์ฒญ ํญํƒ„ +> ๋ชฉ์ : Rate Limiting๋„ ์—†๊ณ  ๋Œ€๊ธฐ์—ด๋„ ์—†์œผ๋ฉด ์–ด๋–ป๊ฒŒ ํ„ฐ์ง€๋Š”์ง€ ํ™•์ธ + +**์žฌํ˜„ ๋ฐฉ๋ฒ•** +```bash +# k6๋กœ 200 VU๊ฐ€ ๋™์‹œ์— ์ฃผ๋ฌธ API ์ง์ ‘ ํ˜ธ์ถœ +k6 run --vus 200 --duration 10s simulate/no-queue.js +``` + +**ํ™•์ธ ํฌ์ธํŠธ** +- [ ] DB ์ปค๋„ฅ์…˜ ํ’€(HikariCP) ๊ณ ๊ฐˆ โ†’ `Connection is not available` ์—๋Ÿฌ +- [ ] ์‘๋‹ต ์‹œ๊ฐ„ ๊ธ‰๊ฒฉํžˆ ์ฆ๊ฐ€ (์ •์ƒ 20ms โ†’ ์ˆ˜ ์ดˆ) +- [ ] ์žฌ๊ณ  ์Œ์ˆ˜ or ์ค‘๋ณต ์ฒ˜๋ฆฌ ๋ฐœ์ƒ ์—ฌ๋ถ€ + +**์˜ˆ์ƒ ๊ฒฐ๋ก **: โ†’ ๋Œ€๊ธฐ์—ด์ด ์™œ ํ•„์š”ํ•œ์ง€ ์ฒด๊ฐ + +--- + +### ์‹œ๋ฎฌ๋ ˆ์ด์…˜ 2 โ€” Sorted Set ์—†์ด List๋กœ ๋Œ€๊ธฐ์—ด ๊ตฌํ˜„ +> ๋ชฉ์ : ์ˆœ์„œ ๋ณด์žฅ๊ณผ ์ค‘๋ณต ๋ฐฉ์ง€๊ฐ€ ์™œ Sorted Set์ด์–ด์•ผ ํ•˜๋Š”์ง€ ํ™•์ธ + +**์žฌํ˜„ ๋ฐฉ๋ฒ•** +- `LPUSH queue:waiting userId` ๋กœ ๋‹จ์ˆœ List ๊ตฌํ˜„ +- ๋™์‹œ์— ๊ฐ™์€ userId๋กœ ์—ฌ๋Ÿฌ ๋ฒˆ ์ง„์ž… ์‹œ๋„ + +**ํ™•์ธ ํฌ์ธํŠธ** +- [ ] ๋™์ผ userId๊ฐ€ List์— ์ค‘๋ณต ๋“ฑ๋ก๋˜๋Š”์ง€ +- [ ] ์ˆœ์„œ๊ฐ€ ๋’ค์ง‘ํžˆ๋Š” ์ผ€์ด์Šค ๋ฐœ์ƒํ•˜๋Š”์ง€ (LPUSH vs RPUSH) +- [ ] ์ˆœ๋ฒˆ ์กฐํšŒ ์‹œ O(N) ์Šค์บ” ์„ฑ๋Šฅ ๋ฌธ์ œ + +**์˜ˆ์ƒ ๊ฒฐ๋ก **: โ†’ Sorted Set์˜ `score ๊ธฐ๋ฐ˜ ์ˆœ์„œ ๋ณด์žฅ + ์ค‘๋ณต ๋ฐฉ์ง€(ZADD NX)`๊ฐ€ ์™œ ๋งž๋Š”์ง€ + +--- + +### ์‹œ๋ฎฌ๋ ˆ์ด์…˜ 3 โ€” ํ† ํฐ ์—†์ด ์ฃผ๋ฌธ API ๋ฐ”๋กœ ์—ด์–ด๋‘๊ธฐ +> ๋ชฉ์ : ๋Œ€๊ธฐ์—ด ํ†ต๊ณผ ๊ฒ€์ฆ ์—†์œผ๋ฉด ์–ด๋–ค ์ผ์ด ์ƒ๊ธฐ๋Š”์ง€ ํ™•์ธ + +**์žฌํ˜„ ๋ฐฉ๋ฒ•** +- ํ† ํฐ ๊ฒ€์ฆ ์ธํ„ฐ์…‰ํ„ฐ ๋น„ํ™œ์„ฑํ™” ์ƒํƒœ์—์„œ +- ๋Œ€๊ธฐ์—ด ์ง„์ž… ์•ˆ ํ•œ ์œ ์ €๊ฐ€ ์ฃผ๋ฌธ API ์ง์ ‘ ํ˜ธ์ถœ + +**ํ™•์ธ ํฌ์ธํŠธ** +- [ ] ๋Œ€๊ธฐ์—ด ์šฐํšŒ ๊ฐ€๋Šฅํ•œ์ง€ +- [ ] ๋Œ€๊ธฐ ์ค‘์ธ ์œ ์ €๋“ค์ด ์†ํ•ด ๋ณด๋Š”์ง€ + +**์˜ˆ์ƒ ๊ฒฐ๋ก **: โ†’ ํ† ํฐ ๊ธฐ๋ฐ˜ ์ž…์žฅ ์ œ์–ด๊ฐ€ ์™œ ํ•„์š”ํ•œ์ง€ + +--- + +### ์‹œ๋ฎฌ๋ ˆ์ด์…˜ 4 โ€” TTL ์—†๋Š” ํ† ํฐ +> ๋ชฉ์ : ํ† ํฐ์— ๋งŒ๋ฃŒ ์‹œ๊ฐ„์ด ์—†์œผ๋ฉด ์–ด๋–ค ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธฐ๋Š”์ง€ ํ™•์ธ + +**์žฌํ˜„ ๋ฐฉ๋ฒ•** +- Redis์— TTL ์—†์ด ํ† ํฐ ์ €์žฅ +- ์˜ค๋ž˜๋œ ํ† ํฐ์œผ๋กœ ์ฃผ๋ฌธ API ์žฌ์‹œ๋„ + +**ํ™•์ธ ํฌ์ธํŠธ** +- [ ] ์ˆ˜ ์‹œ๊ฐ„ ํ›„์—๋„ ํ† ํฐ์œผ๋กœ ์ฃผ๋ฌธ ๊ฐ€๋Šฅํ•œ์ง€ +- [ ] ๋Œ€๊ธฐ์—ด ๊ณต์ •์„ฑ ํ›ผ์† ์—ฌ๋ถ€ (๋Œ€๊ธฐ ์•ˆ ํ•˜๊ณ  ๋‚˜์ค‘์— ์‚ฌ์šฉ) + +**์˜ˆ์ƒ ๊ฒฐ๋ก **: โ†’ TTL ์„ค๊ณ„ ๊ธฐ์ค€์˜ ์˜๋ฏธ + +--- + +### ์‹œ๋ฎฌ๋ ˆ์ด์…˜ 5 โ€” ์Šค์ผ€์ค„๋Ÿฌ ๋ฐฐ์น˜ ํฌ๊ธฐ๋ฅผ ๋„ˆ๋ฌด ํฌ๊ฒŒ ์žก์•˜์„ ๋•Œ +> ๋ชฉ์ : ๋ฐฐ์น˜ ํฌ๊ธฐ N์ด DB ์ปค๋„ฅ์…˜ ํ’€๋ณด๋‹ค ํฌ๋ฉด ์–ด๋–ป๊ฒŒ ๋˜๋Š”์ง€ ํ™•์ธ + +**์žฌํ˜„ ๋ฐฉ๋ฒ•** +- HikariCP `maximum-pool-size=10` ์„ค์ • +- ์Šค์ผ€์ค„๋Ÿฌ ๋ฐฐ์น˜ ํฌ๊ธฐ N=50์œผ๋กœ ์„ค์ • ํ›„ ์‹คํ–‰ + +**ํ™•์ธ ํฌ์ธํŠธ** +- [ ] ์ปค๋„ฅ์…˜ ๋Œ€๊ธฐ ํƒ€์ž„์•„์›ƒ ๋ฐœ์ƒ ์—ฌ๋ถ€ +- [ ] ๋‹ค๋ฅธ API ์‘๋‹ต ์ง€์—ฐ ์—ฌ๋ถ€ + +**์˜ˆ์ƒ ๊ฒฐ๋ก **: โ†’ ๋ฐฐ์น˜ ํฌ๊ธฐ ์‚ฐ์ • ๊ทผ๊ฑฐ (`N โ‰ค ์ปค๋„ฅ์…˜ ํ’€ ํฌ๊ธฐ ร— ์ฃผ๊ธฐ / ์ฒ˜๋ฆฌ์‹œ๊ฐ„`) + +--- + +## โœ… Checklist + +### ๐Ÿšช Step 1 โ€” ๋Œ€๊ธฐ์—ด +- [ ] `POST /queue/enter` โ€” Redis Sorted Set ๊ธฐ๋ฐ˜ ๋Œ€๊ธฐ์—ด ์ง„์ž… API +- [ ] `GET /queue/position` โ€” ์ˆœ๋ฒˆ ์กฐํšŒ API +- [ ] userId ๊ธฐ๋ฐ˜ ์ค‘๋ณต ์ง„์ž… ๋ฐฉ์ง€ +- [ ] ์ „์ฒด ๋Œ€๊ธฐ ์ธ์› ์กฐํšŒ + +### ๐ŸŽซ Step 2 โ€” ์ž…์žฅ ํ† ํฐ & ์Šค์ผ€์ค„๋Ÿฌ +- [ ] ์Šค์ผ€์ค„๋Ÿฌ: ์ฃผ๊ธฐ์ ์œผ๋กœ ๋Œ€๊ธฐ์—ด์—์„œ N๋ช… ๊บผ๋‚ด ์ž…์žฅ ํ† ํฐ ๋ฐœ๊ธ‰ +- [ ] ํ† ํฐ TTL ์„ค์ • (e.g. 5๋ถ„) +- [ ] ์ฃผ๋ฌธ API ์ง„์ž… ์‹œ ํ† ํฐ ๊ฒ€์ฆ +- [ ] ์ฃผ๋ฌธ ์™„๋ฃŒ ํ›„ ํ† ํฐ ์‚ญ์ œ +- [ ] ์ฒ˜๋ฆฌ๋Ÿ‰ ๊ธฐ์ค€ ์Šค์ผ€์ค„๋Ÿฌ ๋ฐฐ์น˜ ํฌ๊ธฐ ์‚ฐ์ • ๊ทผ๊ฑฐ ๋ฌธ์„œํ™” + +### ๐Ÿ“ก Step 3 โ€” ์‹ค์‹œ๊ฐ„ ์ˆœ๋ฒˆ ์กฐํšŒ +- [ ] ์˜ˆ์ƒ ๋Œ€๊ธฐ ์‹œ๊ฐ„ ๊ณ„์‚ฐ ๋กœ์ง +- [ ] Polling ๊ธฐ๋ฐ˜ ์ˆœ๋ฒˆ + ์˜ˆ์ƒ ๋Œ€๊ธฐ ์‹œ๊ฐ„ ์‘๋‹ต +- [ ] ํ† ํฐ ๋ฐœ๊ธ‰ ์‹œ ์ˆœ๋ฒˆ ์กฐํšŒ ์‘๋‹ต์— ํ† ํฐ ํฌํ•จ + +### ๐Ÿงช ๊ฒ€์ฆ +- [ ] ๋™์‹œ ์ง„์ž… ํ…Œ์ŠคํŠธ โ€” ๋Œ€๊ธฐ์—ด ์ˆœ์„œ ๋ณด์žฅ ํ™•์ธ +- [ ] ํ† ํฐ ๋งŒ๋ฃŒ ํ…Œ์ŠคํŠธ โ€” TTL ์ดˆ๊ณผ ์‹œ ๋ฌดํšจํ™” ํ™•์ธ +- [ ] ์ฒ˜๋ฆฌ๋Ÿ‰ ์ดˆ๊ณผ ํ…Œ์ŠคํŠธ โ€” ๋ฐฐ์น˜ ํฌ๊ธฐ ์ดˆ๊ณผ ์š”์ฒญ์—๋„ ์•ˆ์ •์ ์ธ์ง€ ํ™•์ธ + +--- + +## ๐Ÿค” ์„ค๊ณ„ ๊ณ ๋ฏผ ๋ชฉ๋ก (๋ฏธ๊ฒฐ) + + +### Step 1 โ€” ๋Œ€๊ธฐ์—ด +- [ ] **Redis Sorted Set score ๊ธฐ์ค€**: `System.currentTimeMillis()` vs `AtomicLong` ์‹œํ€€์Šค? + - ๋ฐ€๋ฆฌ์ดˆ ์ถฉ๋Œ ๊ฐ€๋Šฅ์„ฑ ์žˆ์Œ โ†’ ๋™์‹œ ์ง„์ž… ์‹œ ์ˆœ์„œ ๋ณด์žฅ ์–ด๋–ป๊ฒŒ? +- [ ] **์ค‘๋ณต ์ง„์ž… ๋ฐฉ์ง€ ๊ตฌํ˜„ ๋ฐฉ์‹**: `ZSCORE` ์กฐํšŒ ํ›„ ์กฐ๊ฑด ๋ถ„๊ธฐ vs Lua ์Šคํฌ๋ฆฝํŠธ๋กœ ์›์ž์  ์ฒ˜๋ฆฌ? + - ๋ถ„๋ฆฌํ•˜๋ฉด TOCTOU(Time-of-Check-Time-of-Use) ๋ฌธ์ œ ๊ฐ€๋Šฅ์„ฑ ์žˆ์Œ +- [ ] **๋Œ€๊ธฐ์—ด ๋งŒ๋ฃŒ ์ฒ˜๋ฆฌ**: ๋Œ€๊ธฐ ์ค‘ ์—ฐ๊ฒฐ ๋Š๊ธด ์œ ์ €๋Š” ์–ด๋–ป๊ฒŒ ์ฒ˜๋ฆฌ? + - ๋ฌด๊ธฐํ•œ ๋Œ€๊ธฐ vs TTL ์„ค์ • ํ›„ ์ž๋™ ์ œ๊ฑฐ + +### Step 2 โ€” ์ž…์žฅ ํ† ํฐ & ์Šค์ผ€์ค„๋Ÿฌ +- [ ] **ํ† ํฐ ์ €์žฅ์†Œ**: Redis String (TTL ํ™œ์šฉ) vs DB? + - Redis ์„ ํƒ ์‹œ โ†’ ์žฅ์•  ์‹œ ํ† ํฐ ์ผ๊ด„ ์†Œ๋ฉธ ๋ฌธ์ œ +- [ ] **์Šค์ผ€์ค„๋Ÿฌ ๋ฐฐ์น˜ ํฌ๊ธฐ N ์‚ฐ์ • ๊ทผ๊ฑฐ**: + - DB ์ปค๋„ฅ์…˜ ํ’€ ๊ธฐ๋ณธ๊ฐ’(HikariCP default=10), ํ‰๊ท  ์ฃผ๋ฌธ ์ฒ˜๋ฆฌ ์‹œ๊ฐ„ ๊ธฐ์ค€์œผ๋กœ ๊ณ„์‚ฐํ•ด์•ผ ํ•จ + - `N = ์ปค๋„ฅ์…˜ ์ˆ˜ ร— (์Šค์ผ€์ค„ ์ฃผ๊ธฐ / ํ‰๊ท  ์ฒ˜๋ฆฌ ์‹œ๊ฐ„)` ๊ณต์‹ ๊ฒ€ํ†  +- [ ] **ํ† ํฐ ๊ฒ€์ฆ ์œ„์น˜**: Filter vs Interceptor vs AOP? +- [ ] **์Šค์ผ€์ค„๋Ÿฌ ์ค‘๋ณต ์‹คํ–‰ ๋ฐฉ์ง€**: ๋‹จ์ผ ์ธ์Šคํ„ด์Šค๋ฉด ๊ดœ์ฐฎ์ง€๋งŒ, ๋ฉ€ํ‹ฐ ์ธ์Šคํ„ด์Šค ์‹œ ๋ถ„์‚ฐ ๋ฝ ํ•„์š”? + +### Step 3 โ€” ์‹ค์‹œ๊ฐ„ ์ˆœ๋ฒˆ ์กฐํšŒ +- [ ] **์˜ˆ์ƒ ๋Œ€๊ธฐ ์‹œ๊ฐ„ ๊ณ„์‚ฐ ๊ณต์‹**: + - `์˜ˆ์ƒ ๋Œ€๊ธฐ ์‹œ๊ฐ„ = ๋‚ด ์ˆœ๋ฒˆ ร— (์Šค์ผ€์ค„ ์ฃผ๊ธฐ / ๋ฐฐ์น˜ ํฌ๊ธฐ N)`? + - ๋” ์ •๊ตํ•œ ๊ณ„์‚ฐ ๋ฐฉ์‹ ์žˆ๋Š”์ง€ ๊ฒ€ํ†  +- [ ] **Polling ์ฃผ๊ธฐ**: ๋ช‡ ์ดˆ๋งˆ๋‹ค ํ˜ธ์ถœ? ํด๋ผ์ด์–ธํŠธ์—์„œ ๊ฒฐ์ •ํ•˜๋Š”๊ฐ€? + - ๋Œ€๊ธฐ ์ธ์› ๋งŽ์„ ๋•Œ ์ฃผ๊ธฐ ๋Š˜๋ฆฌ๋Š” Adaptive Polling ๊ณ ๋ ค ์—ฌ๋ถ€ +- [ ] **Polling ๋ถ€ํ•˜ ์™„ํ™”**: Redis๋งŒ ์กฐํšŒํ•˜๋ฉด ์ถฉ๋ถ„? DB ์กฐํšŒ ์—†์ด ๊ฐ€๋Šฅ? + +--- + +## ๐Ÿ’ญ ์„ค๊ณ„ ๊ฒฐ์ • ์‚ฌํ•ญ (ํ™•์ •) + + +| ํ•ญ๋ชฉ | ๊ฒฐ์ • | ์ด์œ  | +|------|------|------| +| Polling vs SSE | - | ๋ฏธ๊ฒฐ | +| ํ† ํฐ TTL | - | ๋ฏธ๊ฒฐ | +| ์Šค์ผ€์ค„๋Ÿฌ ๋ฐฐ์น˜ ํฌ๊ธฐ N | - | ๋ฏธ๊ฒฐ | +| Redis ์žฅ์•  ์‹œ fallback | - | ๋ฏธ๊ฒฐ | + +--- + +## ๐Ÿ“ ์ง„ํ–‰ ๋กœ๊ทธ + + +### 2026-03-30 (์›”) +- ํ”„๋กœ์ ํŠธ ์„ธํŒ… ๋ฐ ์ปจํ…์ŠคํŠธ ํŒŒ์ผ ์ƒ์„ฑ +- ์ž‘์—… ์‹œ์ž‘ ์˜ˆ์ • + +--- + +## ๐Ÿ”– ๋‹ค์Œ ์„ธ์…˜์— ์ด์–ด์„œ ํ•  ์ผ + +- Step 1 ์„ค๊ณ„๋ถ€ํ„ฐ ์‹œ์ž‘: Redis Sorted Set ๊ตฌ์กฐ ๊ฒฐ์ •, ํŒจํ‚ค์ง€ ๊ตฌ์กฐ ์„ค๊ณ„ + +--- + +## โœ๏ธ ๋ธ”๋กœ๊ทธ & WIL ์†Œ์žฌ ๋ฉ”๋ชจ + + +### ๋ธ”๋กœ๊ทธ ํ›„๋ณด ์ฃผ์ œ (Technical Writing) +- Rate Limiting์œผ๋กœ ๊ฑฐ๋ถ€ํ•˜๋Š” ๊ฒƒ vs ๋Œ€๊ธฐ์—ด๋กœ ์ค„ ์„ธ์šฐ๋Š” ๊ฒƒ: ์–ด๋–ค ์ƒํ™ฉ์—์„œ? +- ์Šค์ผ€์ค„๋Ÿฌ ๋ฐฐ์น˜ ํฌ๊ธฐ ์‚ฐ์ • ๊ทผ๊ฑฐ (DB ์ปค๋„ฅ์…˜ ํ’€, ํ‰๊ท  ์ฒ˜๋ฆฌ ์‹œ๊ฐ„ ๊ธฐ์ค€) +- Thundering Herd ๋Œ€์‘ +- Redis ์žฅ์•  ์‹œ ์„œ๋น„์Šค๋Š” ์–ด๋–ป๊ฒŒ ๋˜์–ด์•ผ ํ•˜๋Š”๊ฐ€? +- Polling vs SSE โ€” ์™œ ๊ทธ ๋ฐฉ์‹์„ ์„ ํƒํ–ˆ๋Š”๊ฐ€? +- ํ† ํฐ TTL์„ ๋ช‡ ๋ถ„์œผ๋กœ ์„ค์ •ํ–ˆ๊ณ , ๊ทธ ๊ธฐ์ค€์€? + +### WIL (Weekly I Learned) ์ž‘์„ฑ ๊ฐ€์ด๋“œ +> WIL์€ ๋ธ”๋กœ๊ทธ๋ณด๋‹ค ๊ฐ€๋ณ๊ฒŒ, "์ด๋ฒˆ ์ฃผ ๋‚ด๊ฐ€ ๋ฐฐ์šด ๊ฒƒ"์„ ์†”์งํ•˜๊ฒŒ ์ •๋ฆฌํ•˜๋Š” ๊ธ€ + +**์ž‘์„ฑ ํ•ญ๋ชฉ** +- ์ด๋ฒˆ ์ฃผ ๋ฐฐ์šด ๊ธฐ์ˆ /๊ฐœ๋… (Redis Sorted Set, ํ† ํฐ TTL ์„ค๊ณ„, Polling ๋ถ€ํ•˜ ๋“ฑ) +- ๊ฐ€์žฅ ์–ด๋ ค์› ๋˜ ๋ถ€๋ถ„๊ณผ ํ•ด๊ฒฐ ๊ณผ์ • +- ์ž˜ ๋๋˜ ๊ฒƒ vs ์•„์‰ฌ์› ๋˜ ๊ฒƒ +- ๋‹ค์Œ ์ฃผ์— ๊ฐ€์ ธ๊ฐˆ ์ธ์‚ฌ์ดํŠธ + +--- + +## ๐Ÿ“‹ PR ์ž‘์„ฑ ์–‘์‹ (์ฐธ๊ณ  ํ…œํ”Œ๋ฆฟ) + + +``` +๐Ÿ“Œ Summary +ํ•œ ๋‹จ๋ฝ์œผ๋กœ ์ด๋ฒˆ PR์˜ ํ•ต์‹ฌ์„ ์š”์•ฝ (๋ฌด์—‡์„, ์™œ, ํ•ต์‹ฌ ๊ฒฐ๊ณผ) +๋ถˆ๋ฆฟ์œผ๋กœ ํ•ต์‹ฌ ํฌ์ธํŠธ 3~5๊ฐœ + +๐Ÿงญ Context & Decision +๋ฌธ์ œ ์ •์˜ (์ฝ”๋“œ ๋ธ”๋ก์œผ๋กœ Before ๊ตฌ์กฐ ํ‘œํ˜„) +๋ฆฌ์Šคํฌ ํ…Œ์ด๋ธ” +ํ•ต์‹ฌ ๊ฒฐ์ • ์š”์•ฝ ํ…Œ์ด๋ธ” (#, ๊ฒฐ์ • ํ•ญ๋ชฉ, ์ตœ์ข… ์„ ํƒ, ํ•ต์‹ฌ ๊ทผ๊ฑฐ) + +๊ฐ ๊ฒฐ์ •๋ณ„: + - ์˜๋„ (์™œ ์ด ๊ฒฐ์ •์„ ๋‚ด๋ ธ๋Š”๊ฐ€) + - ์ฝ”๋“œ ์Šค๋‹ˆํŽซ (Before/After ๋น„๊ต) + - ์„ค๊ณ„๋„ (Mermaid ๋‹ค์ด์–ด๊ทธ๋žจ) โ† ์ค‘๊ฐ„์ค‘๊ฐ„ ํ๋ฆ„๋„ ํ•„์ˆ˜! + - ๋Œ€์•ˆ ๋น„๊ต ํ…Œ์ด๋ธ” (์ „๋žต / ์žฅ๋‹จ์  / ์„ ํƒ ์ด์œ ) + - ๊ด€๋ จ ํด๋ž˜์Šค ํ…Œ์ด๋ธ” (์ปดํฌ๋„ŒํŠธ / ํŒŒ์ผ๊ฒฝ๋กœ / ๋ฉ”์„œ๋“œ / ์—ญํ• ) + +๐Ÿ—๏ธ Design Overview +๋ณ€๊ฒฝ ๋ฒ”์œ„ (๊ธฐ์กด ๋ณ€๊ฒฝ / ์‹ ๊ทœ ์ถ”๊ฐ€) +์ฃผ์š” ์ปดํฌ๋„ŒํŠธ & ํ•ต์‹ฌ ๋ฉ”์„œ๋“œ ํ…Œ์ด๋ธ” + +๐Ÿ” Flow Diagram +Mermaid๋กœ ์ „์ฒด ํ๋ฆ„ ๋‹ค์ด์–ด๊ทธ๋žจ โ† ์„ค๊ณ„๋„ ํ•„์ˆ˜! + +โœ… Checklist +Step๋ณ„ ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +๐Ÿ” ๋ฆฌ๋ทฐ ํฌ์ธํŠธ +๋ฉ˜ํ† /๋ฆฌ๋ทฐ์–ด์—๊ฒŒ ๋ฌป๊ณ  ์‹ถ์€ ํŒ๋‹จ 2~3๊ฐœ + - ํ•ด๊ฒฐํ•˜๋ ค๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ๋ฌธ์ œ + - ์‹œ๋„ํ•œ ์ ‘๊ทผ + - ์ฝ”๋“œ ์œ„์น˜ + - ์ด๋ ‡๊ฒŒ ์„ ํƒํ•œ ์ด์œ  + - ์ธ์ง€ํ•˜๊ณ  ์žˆ๋Š” ํŠธ๋ ˆ์ด๋“œ์˜คํ”„ + - ์—ฌ์ญ™๊ณ  ์‹ถ์€ ์  +``` + +### PR ์ž‘์„ฑ ์‹œ ํ•ต์‹ฌ ์›์น™ +- "๋ฌด์—‡์„ ํ–ˆ๋‹ค" ๊ฐ€ ์•„๋‹ˆ๋ผ **"์™œ ๊ทธ๋ ‡๊ฒŒ ํŒ๋‹จํ–ˆ๋Š”๊ฐ€"** ์ค‘์‹ฌ +- ๊ฐ ๊ฒฐ์ •๋งˆ๋‹ค **๋Œ€์•ˆ ๋น„๊ต ํ…Œ์ด๋ธ”** ํฌํ•จ (์™œ ์ด๊ฑธ ์„ ํƒํ–ˆ๊ณ  ๋‚˜๋จธ์ง€๋Š” ์™œ ๋ฒ„๋ ธ๋Š”์ง€) +- **Mermaid ์„ค๊ณ„๋„** ์ค‘๊ฐ„์ค‘๊ฐ„ ์‚ฝ์ž… (ํ๋ฆ„๋„, ์ƒํƒœ ๋จธ์‹ , ์‹œํ€€์Šค ๋‹ค์ด์–ด๊ทธ๋žจ) +- ๋ฆฌ๋ทฐ ํฌ์ธํŠธ์—์„œ **์†”์งํ•˜๊ฒŒ ํŒ๋‹จ์ด ์„œ์ง€ ์•Š๋Š” ๋ถ€๋ถ„** ์งˆ๋ฌธ + +--- + +## ๐Ÿ—๏ธ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ (์ฐธ๊ณ ) +``` +apps/commerce-api/src/main/java/com/loopers/ +โ”œโ”€โ”€ application/ # Facade (usecase ์กฐํ•ฉ) +โ”œโ”€โ”€ domain/ # Entity, Service, Repository interface +โ”œโ”€โ”€ infrastructure/ # Repository ๊ตฌํ˜„์ฒด (JPA, Redis) +โ”œโ”€โ”€ interfaces/api/ # Controller, DTO, ApiSpec +โ””โ”€โ”€ support/ # CoreException, ErrorType +``` + +**Tech Stack**: Java 21, Spring Boot 3.4.4, Redis, MySQL 8.0, Kafka diff --git a/docs/black-friday-simulator.md b/docs/black-friday-simulator.md new file mode 100644 index 000000000..9acf65380 --- /dev/null +++ b/docs/black-friday-simulator.md @@ -0,0 +1,326 @@ +# ๋ธ”๋ž™ํ”„๋ผ์ด๋ฐ์ด ์ฃผ๋ฌธ ๋Œ€๊ธฐ์—ด ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ + +> ์ž‘์„ฑ ๋ชฉ์ : ์ฃผ๋ฌธ ๋Œ€๊ธฐ์—ด ์‹œ์Šคํ…œ ํ•™์Šต ์ „, ๋ฌธ์ œ๋ฅผ ๋จผ์ € ์žฌํ˜„ํ•˜๊ณ  ๊ฐœ์„  ํšจ๊ณผ๋ฅผ ์ˆ˜์น˜๋กœ ๊ฒ€์ฆํ•œ๋‹ค. + +--- + +## ๋ฐฐ๊ฒฝ ๋ฐ ๋ชฉ์  + +๋ธ”๋ž™ํ”„๋ผ์ด๋ฐ์ด์ฒ˜๋Ÿผ ์งง์€ ์‹œ๊ฐ„์— ์ฃผ๋ฌธ์ด ํญ๋ฐœ์ ์œผ๋กœ ๋ชฐ๋ฆฌ๋Š” ์ƒํ™ฉ์„ ์ง์ ‘ ์žฌํ˜„ํ•ด, +ํ˜„์žฌ ์‹œ์Šคํ…œ์ด ์–ด๋””์„œ ๋ฌด๋„ˆ์ง€๋Š”์ง€ ์ˆ˜์น˜๋กœ ํ™•์ธํ•œ๋‹ค. + +**ํ•™์Šต ํ๋ฆ„** + +``` +[Phase 1] ํ•œ๊ณ„์  ํƒ์ƒ‰ โ†’ ํ˜„์žฌ ์ฝ”๋“œ๊ฐ€ ๋ช‡ ๋ช…๊นŒ์ง€ ๋ฒ„ํ‹ฐ๋Š”์ง€ P99 ๊ธฐ์ค€์œผ๋กœ ์ธก์ • +[Phase 2] ๋ธ”ํ”„ spike ์žฌํ˜„ โ†’ ํ•œ๊ณ„๋ฅผ ๋„˜์—ˆ์„ ๋•Œ ์‹ค์ œ๋กœ ๋ฌด์Šจ ์ผ์ด ์ƒ๊ธฐ๋Š”์ง€ ํ™•์ธ + โ†“ + [๋‹ค์Œ ์ฃผ] ์ฃผ๋ฌธ ๋Œ€๊ธฐ์—ด ์‹œ์Šคํ…œ ๊ตฌํ˜„ + โ†“ +[Phase 3] ๊ฐœ์„  ๊ฒ€์ฆ โ†’ ๊ฐ™์€ ๋ถ€ํ•˜๋กœ ์žฌ์‹คํ–‰, ์ˆ˜์น˜ ๋น„๊ต +``` + +--- + +## ํ˜„์žฌ ์ฝ”๋“œ ๋ถ„์„ (์™œ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธฐ๋Š”๊ฐ€) + +### ์ฃผ๋ฌธ ํŠธ๋žœ์žญ์…˜ ํ๋ฆ„ + +``` +POST /api/v1/orders + โ””โ”€โ”€ OrderFacade.createOrder() @Transactional โ† ํŠธ๋žœ์žญ์…˜ ์‹œ์ž‘ + โ”œโ”€โ”€ userService.authenticate() โ† DB ์กฐํšŒ + โ”œโ”€โ”€ productService.getProducts() โ† DB ์กฐํšŒ + โ”œโ”€โ”€ productService.getBrands() โ† DB ์กฐํšŒ + โ”œโ”€โ”€ userCouponService.validateAndUse() โ† DB ์กฐํšŒ + UPDATE + โ””โ”€โ”€ orderService.createOrder() + โ””โ”€โ”€ StockDeductionService.deductAll() @Transactional + โ””โ”€โ”€ findByProductIdWithLock() โ† PESSIMISTIC_WRITE ๋ฝ ํš๋“ + โ””โ”€โ”€ stock.deduct() โ† ์žฌ๊ณ  ์ฐจ๊ฐ + โ””โ”€โ”€ ์ฃผ๋ฌธ/์ฃผ๋ฌธ์•„์ดํ…œ INSERT + โ† ํŠธ๋žœ์žญ์…˜ ์ปค๋ฐ‹ (๋ฝ ํ•ด์ œ) +``` + +**ํ•ต์‹ฌ ์ฝ”๋“œ ์œ„์น˜** +- `StockDeductionService` โ€” ๋น„๊ด€์  ๋ฝ ํš๋“ ๋ฐ ์žฌ๊ณ  ์ฐจ๊ฐ +- `StockJpaRepository` โ€” `@Lock(LockModeType.PESSIMISTIC_WRITE)` +- `OrderFacade.createOrder()` โ€” ์ „์ฒด ์ฃผ๋ฌธ ํ๋ฆ„ ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜ (`@Transactional`) + +### ํ˜„์žฌ ๋ฐฉ์–ด ๋ฉ”์ปค๋‹ˆ์ฆ˜ + +| ๋ฉ”์ปค๋‹ˆ์ฆ˜ | ์—ญํ•  | ํ•œ๊ณ„ | +|---|---|---| +| `PESSIMISTIC_WRITE` ๋ฝ | ์žฌ๊ณ  ์ •ํ•ฉ์„ฑ ๋ณด์žฅ (oversell ๋ฐฉ์ง€) | ๋™์‹œ ์š”์ฒญ ๋ชจ๋‘ ์ง๋ ฌํ™” โ†’ ๋Œ€๊ธฐ ์‹œ๊ฐ„ ๊ธ‰์ฆ | +| productId ์ •๋ ฌ ํ›„ ๋ฝ ํš๋“ | ๋ฐ๋“œ๋ฝ ๋ฐฉ์ง€ | โ€” | +| `CoreException(BAD_REQUEST)` | ์žฌ๊ณ  ๋ถ€์กฑ ์‹œ 400 ๋ฐ˜ํ™˜ | โ€” | + +### ๋ฌธ์ œ: ์ •ํ•ฉ์„ฑ์€ ์ง€ํ‚ค์ง€๋งŒ ์„ฑ๋Šฅ์€ ๋ชป ์ง€ํ‚จ๋‹ค + +``` +ํŠธ๋žœ์žญ์…˜ 1 โ”€โ”€โ”€ [๋ฝ ํš๋“] โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ [์ปค๋ฐ‹/๋ฝ ํ•ด์ œ] +ํŠธ๋žœ์žญ์…˜ 2 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ [๋ฝ ๋Œ€๊ธฐ ์ค‘...] โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +ํŠธ๋žœ์žญ์…˜ 3 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ [๋ฝ ๋Œ€๊ธฐ ์ค‘...] โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +...N๋ช… ๋™์‹œ +``` + +N์ด ์ปค์งˆ์ˆ˜๋ก: +- DB ์ปค๋„ฅ์…˜์ด ๋ชจ๋‘ ๋ฝ ๋Œ€๊ธฐ ์ƒํƒœ๋กœ ์ ์œ ๋จ +- HikariCP ์ปค๋„ฅ์…˜ ํ’€ ๊ณ ๊ฐˆ โ†’ ์ƒˆ ์š”์ฒญ์€ ์ปค๋„ฅ์…˜์กฐ์ฐจ ๋ชป ์–ป๊ณ  ์—๋Ÿฌ +- ๋ฝ ๋Œ€๊ธฐ ํƒ€์ž„์•„์›ƒ โ†’ ์žฌ๊ณ ๊ฐ€ ์žˆ์–ด๋„ ์ฃผ๋ฌธ ์‹คํŒจ +- **๊ฒฐ๊ณผ**: ์žฌ๊ณ  50๊ฐœ์ธ๋ฐ ์‹ค์ œ ์„ฑ๊ณต ์ฃผ๋ฌธ์ด 50๊ฐœ ๋ฏธ๋งŒ + +--- + +## ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ๊ตฌ์„ฑ + +์Šคํฌ๋ฆฝํŠธ 2๊ฐœ๋ฅผ ์ˆœ์„œ๋Œ€๋กœ ์‹คํ–‰ํ•œ๋‹ค. + +| ์Šคํฌ๋ฆฝํŠธ | ๋ชฉ์  | ํŠน์ง• | +|---|---|---| +| `k6/breakpoint.js` | ํ•œ๊ณ„์  ํƒ์ƒ‰ | VU๋ฅผ ๋‹จ๊ณ„์ ์œผ๋กœ ์ฆ๊ฐ€์‹œ์ผœ P99๊ฐ€ ๊บพ์ด๋Š” ์ง€์  ์ฐพ๊ธฐ | +| `k6/black-friday.js` | ๋ธ”ํ”„ ์žฌํ˜„ | 5์ดˆ ๋งŒ์— 200๋ช… spike, ๋ฌด์Šจ ์ผ์ด ์ƒ๊ธฐ๋Š”์ง€ ํ™•์ธ | + +--- + +## Phase 1: ํ•œ๊ณ„์  ํƒ์ƒ‰ (`breakpoint.js`) + +### ๋ชฉํ‘œ +"P99 < 2000ms ๋ฅผ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ๋Š” ์ตœ๋Œ€ ๋™์‹œ ์š”์ฒญ ์ˆ˜"๋ฅผ ์ฐพ๋Š”๋‹ค. + +### VU ์ฆ๊ฐ€ ๋‹จ๊ณ„ + +| ๋‹จ๊ณ„ | VU ์ˆ˜ | ์œ ์ง€ ์‹œ๊ฐ„ | ์˜ˆ์ƒ ์ƒํƒœ | +|---|---|---|---| +| 1 | 10 | 30s | ์ •์ƒ (๋ฒ ์ด์Šค๋ผ์ธ) | +| 2 | 30 | 30s | ์ •์ƒ | +| 3 | 50 | 30s | ์ฃผ์˜ | +| 4 | 100 | 30s | **ํ•œ๊ณ„์  ์ง„์ž… ์˜ˆ์ƒ** โš ๏ธ | +| 5 | 150 | 30s | ํ•œ๊ณ„ ์ดˆ๊ณผ ์˜ˆ์ƒ | +| 6 | 200 | 30s | ๋ธ”๋ž™ํ”„๋ผ์ด๋ฐ์ด ์ˆ˜์ค€ | +| 7 | 300 | 30s | ๊ณผ๋ถ€ํ•˜ ํ™•์ธ | + +๊ฐ ๋‹จ๊ณ„ ์ „ํ™˜ ์‹œ 5์ดˆ ramp (๊ธ‰๊ฒฉํ•œ spike ๋ฐฐ์ œ, ๋ถ€ํ•˜ ์ž์ฒด์—๋งŒ ์ง‘์ค‘) + +### ํ•œ๊ณ„์  ํŒ๋‹จ ๊ธฐ์ค€ + +๋‹ค์Œ ์ค‘ ํ•˜๋‚˜๋ผ๋„ ๋งŒ์กฑํ•˜๋ฉด ๊ทธ VU ์ˆ˜๋ฅผ "ํ•œ๊ณ„์ "์œผ๋กœ ๊ธฐ๋ก: +- P99 > 2000ms (์‘๋‹ต์‹œ๊ฐ„ SLO ์œ„๋ฐ˜) +- ๋น„์ •์ƒ ์—๋Ÿฌ์œจ > 5% (500, ํƒ€์ž„์•„์›ƒ ๋“ฑ ์žฌ๊ณ  ๋ถ€์กฑ์ด ์•„๋‹Œ ์—๋Ÿฌ) +- `hikari.connections.pending` > 0 (Grafana์—์„œ ํ™•์ธ) + +### ๊ธฐ๋Œ€ ๊ฒฐ๊ณผ ์˜ˆ์‹œ + +``` + 10 VU โ†’ P99: 120ms ๋น„์ •์ƒ ์—๋Ÿฌ: 0% โœ… + 30 VU โ†’ P99: 340ms ๋น„์ •์ƒ ์—๋Ÿฌ: 0% โœ… + 50 VU โ†’ P99: 890ms ๋น„์ •์ƒ ์—๋Ÿฌ: 0% โœ… +100 VU โ†’ P99: 2800ms ๋น„์ •์ƒ ์—๋Ÿฌ: 2% โš ๏ธ ํ•œ๊ณ„์  +150 VU โ†’ P99: 7200ms ๋น„์ •์ƒ ์—๋Ÿฌ: 18% โŒ +200 VU โ†’ P99: ํƒ€์ž„์•„์›ƒ ๋น„์ •์ƒ ์—๋Ÿฌ: 35% โŒ +``` +*(์‹ค์ œ ์ˆ˜์น˜๋Š” ์‹คํ–‰ ํ™˜๊ฒฝ์— ๋”ฐ๋ผ ๋‹ค๋ฆ„)* + +### ์ƒํ’ˆ ์„ค์ • + +์žฌ๊ณ  ๊ณ ๊ฐˆ๋กœ ์ธํ•œ ๋…ธ์ด์ฆˆ ์ œ๊ฑฐ๋ฅผ ์œ„ํ•ด **์žฌ๊ณ  10,000๊ฐœ** ์ƒํ’ˆ์„ ์‚ฌ์šฉํ•œ๋‹ค. +โ†’ ์žฌ๊ณ  ๋ถ€์กฑ์ด ์•„๋‹Œ ์ˆœ์ˆ˜ ๋ถ€ํ•˜ ์••๋ฐ•์— ์˜ํ•œ P99 ๋ณ€ํ™”๋งŒ ๊ด€์ฐฐ + +--- + +## Phase 2: ๋ธ”๋ž™ํ”„๋ผ์ด๋ฐ์ด ์žฌํ˜„ (`black-friday.js`) + +### ๋ชฉํ‘œ +ํ•œ๊ณ„์ ์„ ์•Œ๊ณ  ์žˆ๋Š” ์ƒํƒœ์—์„œ, ๊ทธ ํ•œ๊ณ„๋ฅผ **5์ดˆ ๋งŒ์— ์ดˆ๊ณผ**ํ–ˆ์„ ๋•Œ ๋ฌด์Šจ ์ผ์ด ์ƒ๊ธฐ๋Š”์ง€ ํ™•์ธํ•œ๋‹ค. + +### ์‹œ๋‚˜๋ฆฌ์˜ค + +``` +[ํ•œ์ •ํŒ ์ƒํ’ˆ] ์žฌ๊ณ : 50๊ฐœ +[๋™์‹œ ์š”์ฒญ] 200๋ช… (ํ•œ๊ณ„์  ์ดˆ๊ณผ ์ˆ˜์ค€) +[์ด์ƒ์  ๊ฒฐ๊ณผ] 50๋ช… ์„ฑ๊ณต, 150๋ช… ์žฌ๊ณ  ๋ถ€์กฑ 400 +[์˜ˆ์ƒ ์‹ค์ œ] 50๋ช… ๋ฏธ๋งŒ ์„ฑ๊ณต + ๋‹ค์ˆ˜ 500/ํƒ€์ž„์•„์›ƒ +``` + +### ํŠธ๋ž˜ํ”ฝ ํŒจํ„ด + +``` + 0s โ†’ 5s : 0 โ†’ 200 VU (์ •๊ฐ ๋ชฐ๋ฆผ: ๊ธ‰๊ฒฉํ•œ spike) + 5s โ†’ 35s : 200 VU (์ง€์† ์••๋ฐ•) +35s โ†’ 40s : 200 โ†’ 0 VU (์ข…๋ฃŒ) +``` + +### ๊ฒฐ๊ณผ ๋ถ„๋ฅ˜ + +| ์‘๋‹ต | ๋ถ„๋ฅ˜ | ์˜๋ฏธ | +|---|---|---| +| 201 Created | `order_success` | ์ •์ƒ ์„ฑ๊ณต (์žฌ๊ณ  50๊ฐœ ํ•œ๋„ ๋‚ด) | +| 400 + "์žฌ๊ณ ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค" | `stock_exhausted` | ์˜ˆ์ƒ๋œ ์‹คํŒจ (์ •์ƒ) | +| 400 ๊ธฐํƒ€ / 500 / ํƒ€์ž„์•„์›ƒ | `sys_error` | **๋น„์ •์ƒ ์—๋Ÿฌ โ€” ์ด๊ฒŒ ๋ฌธ์ œ** | + +--- + +## ์ปค์Šคํ…€ ๋ฉ”ํŠธ๋ฆญ + +๋‘ ์Šคํฌ๋ฆฝํŠธ ๊ณตํ†ต์œผ๋กœ ์•„๋ž˜ ๋ฉ”ํŠธ๋ฆญ์„ ์ˆ˜์ง‘ํ•œ๋‹ค. + +| ๋ฉ”ํŠธ๋ฆญ | ์ข…๋ฅ˜ | ์„ค๋ช… | +|---|---|---| +| `order_duration` | Trend | ์ฃผ๋ฌธ API ์‘๋‹ต์‹œ๊ฐ„ (P50/P95/P99 ๋ถ„์„์šฉ) | +| `order_success` | Counter | ์„ฑ๊ณตํ•œ ์ฃผ๋ฌธ ์ˆ˜ | +| `stock_exhausted` | Counter | ์žฌ๊ณ  ๋ถ€์กฑ์œผ๋กœ ์‹คํŒจํ•œ ์ˆ˜ (์˜ˆ์ƒ๋œ ์‹คํŒจ) | +| `sys_error` | Counter | ๋น„์ •์ƒ ์—๋Ÿฌ ์ˆ˜ (500, ํƒ€์ž„์•„์›ƒ ๋“ฑ) | +| `abnormal_error_rate` | Rate | ๋น„์ •์ƒ ์—๋Ÿฌ ๋น„์œจ | + +--- + +## ์›น ๋Œ€์‹œ๋ณด๋“œ ๊ตฌ์„ฑ + +### ์„ ํƒ: InfluxDB + Grafana + +**์ด์œ **: Spring Boot ๋ฉ”ํŠธ๋ฆญ(DB ์ปค๋„ฅ์…˜ ํ’€ ์ƒํƒœ)๊ณผ k6 ๋ฉ”ํŠธ๋ฆญ(P99, ์—๋Ÿฌ์œจ)์„ ๊ฐ™์€ ํ™”๋ฉด์—์„œ ๋ณด๋ฉด ์ธ๊ณผ๊ด€๊ณ„๋ฅผ ํ•œ๋ˆˆ์— ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค. + +``` +k6 โ”€โ”€(--out influxdb)โ”€โ”€โ†’ InfluxDB (8086) โ”€โ”€โ†’ Grafana (3000) + โ†‘ +Spring Boot actuator โ”€โ”€โ†’ Prometheus (9090) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### Grafana ๋Œ€์‹œ๋ณด๋“œ ๊ตฌ์„ฑ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ๋ธ”๋ž™ํ”„๋ผ์ด๋ฐ์ด ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ k6 ๋ฉ”ํŠธ๋ฆญ โ”‚ Spring Boot ๋ฉ”ํŠธ๋ฆญ โ”‚ +โ”‚ (InfluxDB) โ”‚ (Prometheus) โ”‚ +โ”‚ โ”‚ โ”‚ +โ”‚ VU ์ˆ˜ ์ถ”์ด โ”‚ hikari.connections.active โ”‚ +โ”‚ P99 ์‘๋‹ต์‹œ๊ฐ„ ์ถ”์ด โ”‚ hikari.connections.pending โ”‚ +โ”‚ ๋น„์ •์ƒ ์—๋Ÿฌ์œจ โ”‚ http_server_requests โ”‚ +โ”‚ ์„ฑ๊ณต/์žฌ๊ณ ๋ถ€์กฑ/์—๋Ÿฌ โ”‚ jvm.threads.live โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ†‘ ๋‘ ๊ทธ๋ž˜ํ”„๊ฐ€ ๊ฐ™์€ ์‹œ์ ์— ๊บพ์ด๋Š” ์ง€์  = ํ•œ๊ณ„์  +``` + +k6 ๊ณต์‹ Grafana ๋Œ€์‹œ๋ณด๋“œ: **ID 2587** (Grafana UI์—์„œ import) + +### ์ˆ˜์ •์ด ํ•„์š”ํ•œ ํŒŒ์ผ + +| ํŒŒ์ผ | ์ˆ˜์ • ๋‚ด์šฉ | +|---|---| +| `docker/monitoring-compose.yml` | InfluxDB ์„œ๋น„์Šค ์ถ”๊ฐ€ (influxdb:1.8, ํฌํŠธ 8086) | +| `docker/grafana/provisioning/datasources/datasource.yml` | InfluxDB ๋ฐ์ดํ„ฐ์†Œ์Šค ์ถ”๊ฐ€ | +| `docker/load-test-compose.yml` | k6 ์‹คํ–‰ ์‹œ `--out influxdb` ์˜ต์…˜ ์ถ”๊ฐ€ | + +--- + +## Seed ๋ฐ์ดํ„ฐ ์ „๋žต + +### ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ + +``` +๋ธŒ๋žœ๋“œ 1๊ฐœ (ACTIVE) + โ””โ”€โ”€ ์ƒํ’ˆ A: ์žฌ๊ณ  10,000 โ†’ breakpoint.js ์ „์šฉ (์žฌ๊ณ  ๊ณ ๊ฐˆ ๋…ธ์ด์ฆˆ ์ œ๊ฑฐ) + โ””โ”€โ”€ ์ƒํ’ˆ B: ์žฌ๊ณ  50 โ†’ black-friday.js ์ „์šฉ (ํ•œ์ • ์ˆ˜๋Ÿ‰ ์žฌํ˜„) + +์œ ์ € 300๋ช… (bp_user_001 ~ bp_user_300) + โ†’ k6 setup() ๋‹จ๊ณ„์—์„œ ์ž๋™ ๋“ฑ๋ก + โ†’ ์ด๋ฏธ ์กด์žฌํ•˜๋ฉด ๋ฌด์‹œ (๋ฉฑ๋“ฑ์„ฑ) +``` + +### ์žฌ๊ณ  ๊ด€๋ฆฌ + +Admin API์— ์žฌ๊ณ  ์ˆ˜์ • ์—”๋“œํฌ์ธํŠธ ์—†์Œ โ†’ ์ง์ ‘ SQL ์‚ฌ์šฉ + +```sql +-- ์žฌ๊ณ  ๋ฆฌ์…‹ (ํ…Œ์ŠคํŠธ ์žฌ์‹คํ–‰ ์‹œ) +UPDATE stock SET quantity = 50 WHERE product_id = {productId_B}; +UPDATE stock SET quantity = 10000 WHERE product_id = {productId_A}; +``` + +### ๊ด€๋ จ ํŒŒ์ผ +- `http/commerce-api/seed-black-friday.http` โ€” ๋ธŒ๋žœ๋“œ/์ƒํ’ˆ ์ƒ์„ฑ ์š”์ฒญ ๋ชจ์Œ + +--- + +## ์‹คํ–‰ ์ˆœ์„œ + +```bash +# 1. ์ธํ”„๋ผ ์‹คํ–‰ +docker-compose -f docker/infra-compose.yml up -d + +# 2. ๋ชจ๋‹ˆํ„ฐ๋ง ์‹คํ–‰ (InfluxDB ์ถ”๊ฐ€ ํ›„) +docker-compose -f docker/monitoring-compose.yml up -d + +# 3. API ์„œ๋ฒ„ ์‹คํ–‰ +./gradlew :apps:commerce-api:bootRun + +# 4. Seed ๋ฐ์ดํ„ฐ ์ƒ์„ฑ (์ตœ์ดˆ 1ํšŒ) +# http/commerce-api/seed-black-friday.http ์ˆœ์„œ๋Œ€๋กœ ์‹คํ–‰ +# โ†’ ์ดํ›„ ์‘๋‹ต์—์„œ productId ํ™•์ธ ํ›„ SQL๋กœ ์žฌ๊ณ  ๋“ฑ๋ก + +# 5. Grafana ์ ‘์† ํ›„ k6 ๋Œ€์‹œ๋ณด๋“œ import +open http://localhost:3000 # admin / admin +# Dashboards โ†’ Import โ†’ ID: 2587 ์ž…๋ ฅ + +# 6. Phase 1: ํ•œ๊ณ„์  ํƒ์ƒ‰ +k6 run -e PRODUCT_ID={productId_A} --out influxdb=http://localhost:8086/k6 k6/breakpoint.js + +# 7. Phase 2: ๋ธ”๋ž™ํ”„๋ผ์ด๋ฐ์ด ์žฌํ˜„ (์žฌ๊ณ  ๋ฆฌ์…‹ ํ›„) +# SQL: UPDATE stock SET quantity = 50 WHERE product_id = {productId_B}; +k6 run -e PRODUCT_ID={productId_B} --out influxdb=http://localhost:8086/k6 k6/black-friday.js +``` + +--- + +## Phase 3: ๋Œ€๊ธฐ์—ด ๊ตฌํ˜„ ํ›„ ๊ฒ€์ฆ (๋‹ค์Œ ์ฃผ) + +๋Œ€๊ธฐ์—ด ์‹œ์Šคํ…œ ๊ตฌํ˜„ ํ›„ **๋™์ผํ•œ ์Šคํฌ๋ฆฝํŠธ**๋ฅผ ์žฌ์‹คํ–‰ํ•ด์„œ ๋น„๊ตํ•œ๋‹ค. + +| ์ง€ํ‘œ | Phase 1/2 (ํ˜„์žฌ) | Phase 3 (๋Œ€๊ธฐ์—ด ํ›„) ๊ธฐ๋Œ€๊ฐ’ | +|---|---|---| +| ํ•œ๊ณ„์  VU ์ˆ˜ | ~100 VU | ๋” ๋†’์•„์ง | +| ๋ธ”ํ”„ ์„ฑ๊ณต ์ฃผ๋ฌธ ์ˆ˜ | 50๊ฐœ ๋ฏธ๋งŒ | ์ •ํ™•ํžˆ 50๊ฐœ | +| ๋น„์ •์ƒ ์—๋Ÿฌ์œจ | 30%+ | โ‰ˆ 0% | +| P99 ์‘๋‹ต์‹œ๊ฐ„ | ํƒ€์ž„์•„์›ƒ | ๋น ๋ฅธ ์‘๋‹ต (๋Œ€๊ธฐ ๋ฒˆํ˜ธ ๋ฐ˜ํ™˜) | +| DB ์ปค๋„ฅ์…˜ pending | ๊ณ ๊ฐˆ | ๋‚ฎ์Œ (์ˆœ์ฐจ ์ฒ˜๋ฆฌ) | + +--- + +## ์ž‘์—… ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +### ์ธํ”„๋ผ ์„ค์ • +- [ ] `monitoring-compose.yml`์— InfluxDB 1.8 ์ถ”๊ฐ€ (ํฌํŠธ 8086, DB๋ช…: k6) +- [ ] `datasource.yml`์— InfluxDB ๋ฐ์ดํ„ฐ์†Œ์Šค ์ถ”๊ฐ€ +- [ ] `load-test-compose.yml`์— `--out influxdb` ์˜ต์…˜ ์ถ”๊ฐ€ +- [ ] Grafana์—์„œ k6 ๋Œ€์‹œ๋ณด๋“œ import (ID: 2587) ํ™•์ธ + +### Seed ๋ฐ์ดํ„ฐ +- [ ] ๋ธŒ๋žœ๋“œ ์ƒ์„ฑ ํ›„ brandId ๊ธฐ๋ก +- [ ] ํ•œ๊ณ„์  ํ…Œ์ŠคํŠธ ์ƒํ’ˆ ์ƒ์„ฑ ํ›„ productId_A ๊ธฐ๋ก +- [ ] ๋ธ”ํ”„ ์ƒํ’ˆ ์ƒ์„ฑ ํ›„ productId_B ๊ธฐ๋ก +- [ ] SQL๋กœ ์žฌ๊ณ  ๋“ฑ๋ก ํ™•์ธ (A: 10,000 / B: 50) + +### breakpoint.js ์‹คํ–‰ +- [ ] PRODUCT_ID=productId_A ๋กœ ์‹คํ–‰ +- [ ] Grafana์—์„œ P99 ์ถ”์ด ํ™•์ธ +- [ ] ํ•œ๊ณ„์  VU ์ˆ˜ ๊ธฐ๋ก + +### black-friday.js ์‹คํ–‰ +- [ ] ์žฌ๊ณ  50๊ฐœ ๋ฆฌ์…‹ ํ™•์ธ +- [ ] PRODUCT_ID=productId_B ๋กœ ์‹คํ–‰ +- [ ] ์„ฑ๊ณต ์ฃผ๋ฌธ ์ˆ˜ vs 50๊ฐœ ๋น„๊ต +- [ ] ๋น„์ •์ƒ ์—๋Ÿฌ์œจ ๊ธฐ๋ก + +### Phase 3 (๋Œ€๊ธฐ์—ด ๊ตฌํ˜„ ํ›„) +- [ ] ์žฌ๊ณ  ๋ฆฌ์…‹ +- [ ] ๋‘ ์Šคํฌ๋ฆฝํŠธ ์žฌ์‹คํ–‰ +- [ ] Before/After ์ˆ˜์น˜ ๋น„๊ตํ‘œ ์ž‘์„ฑ + +--- + +## ํ•˜์ง€ ์•Š์„ ๊ฒƒ + +- ์ฟ ํฐ ์ ์šฉ, ์—ฌ๋Ÿฌ ์ƒํ’ˆ โ†’ ๋ณ‘๋ชฉ ํฌ์ธํŠธ ๋‹จ์ˆœํ™” (์žฌ๊ณ  ๋ฝ ํ•˜๋‚˜์— ์ง‘์ค‘) +- ๊ฒฐ์ œ ํ”Œ๋กœ์šฐ โ†’ ์ฃผ๋ฌธ ์ƒ์„ฑ ๋‹จ๊ณ„๊ฐ€ ํ•ต์‹ฌ, ๊ฒฐ์ œ๋Š” ๋ณ„๊ฐœ ๋ถ„์„ +- Spring Boot simulator ๋ชจ๋“ˆ โ†’ k6๋กœ ์ถฉ๋ถ„, ๋‹ค์Œ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ์ฃผ์ œ์—์„œ ๋ณ„๋„ ๊ตฌ์„ฑ \ No newline at end of file diff --git a/docs/concepts/01-sorted-set-basic.mermaid b/docs/concepts/01-sorted-set-basic.mermaid new file mode 100644 index 000000000..1bd64bfa9 --- /dev/null +++ b/docs/concepts/01-sorted-set-basic.mermaid @@ -0,0 +1,26 @@ +--- +title: Redis Sorted Set โ€” ์ˆœ์„œ ๋ณด์žฅ ์›๋ฆฌ +--- +flowchart LR + subgraph ์ž…๋ ฅ["ZADD queue {score} {member}"] + A["userA\nscore: 1000"] + B["userC\nscore: 1500"] + C["userB\nscore: 2000"] + end + + subgraph ์ €์žฅ["Sorted Set (ํ•ญ์ƒ score ์˜ค๋ฆ„์ฐจ์ˆœ ์œ ์ง€)"] + direction TB + R1["1์œ„ userA โ€” score: 1000"] + R2["2์œ„ userC โ€” score: 1500"] + R3["3์œ„ userB โ€” score: 2000"] + R1 --> R2 --> R3 + end + + ์ž…๋ ฅ --> ์ €์žฅ + + subgraph ์กฐํšŒ + Q1["ZRANK queue userC โ†’ 1 (0-indexed)"] + Q2["ZCARD queue โ†’ 3 (์ „์ฒด ์ธ์›)"] + end + + ์ €์žฅ --> ์กฐํšŒ diff --git a/docs/concepts/README.md b/docs/concepts/README.md new file mode 100644 index 000000000..8bbc3bb99 --- /dev/null +++ b/docs/concepts/README.md @@ -0,0 +1,20 @@ +# ๊ฐœ๋… ์ •๋ฆฌ ๋‹ค์ด์–ด๊ทธ๋žจ + +> ๊ฐœ๋… ํ•™์Šต ํ›„ Claude๊ฐ€ Mermaid๋กœ ์ €์žฅํ•˜๋Š” ๊ณต๊ฐ„์ž…๋‹ˆ๋‹ค. +> ๊ฐ ํŒŒ์ผ์€ ํ•˜๋‚˜์˜ ๊ฐœ๋… ๋‹จ์œ„๋กœ ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค. + +## ํŒŒ์ผ ๋ชฉ๋ก + +| ํŒŒ์ผ | ๊ฐœ๋… | ํ€ด์ฆˆ ํ†ต๊ณผ | +|------|------|----------| +| `01-sorted-set-basic.mermaid` | Redis Sorted Set ๊ธฐ๋ณธ ๋™์ž‘ | โฌœ | +| `02-sorted-set-zadd-nx.mermaid` | ZADD NX ์ค‘๋ณต ๋ฐฉ์ง€ ์›๋ฆฌ | โฌœ | +| `03-score-strategy.mermaid` | score ์„ค๊ณ„ ์ „๋žต | โฌœ | +| `04-toctou.mermaid` | TOCTOU race condition | โฌœ | +| `05-queue-vs-ratelimit.mermaid` | ๋Œ€๊ธฐ์—ด vs Rate Limiting | โฌœ | +| `06-thundering-herd.mermaid` | Thundering Herd | โฌœ | +| `07-batch-size.mermaid` | ๋ฐฐ์น˜ ํฌ๊ธฐ N ์‚ฐ์ • | โฌœ | +| `08-token-ttl.mermaid` | ์ž…์žฅ ํ† ํฐ & TTL ์„ค๊ณ„ | โฌœ | +| `09-polling-vs-sse.mermaid` | Polling vs SSE | โฌœ | +| `10-hikaricp.mermaid` | HikariCP ์ปค๋„ฅ์…˜ ํ’€ ๊ณ ๊ฐˆ | โฌœ | +| `11-redis-failure.mermaid` | Redis ์žฅ์•  ์‹œ๋‚˜๋ฆฌ์˜ค | โฌœ | diff --git a/http/commerce-api/seed-black-friday.http b/http/commerce-api/seed-black-friday.http new file mode 100644 index 000000000..aa31ff9ab --- /dev/null +++ b/http/commerce-api/seed-black-friday.http @@ -0,0 +1,64 @@ +### ๋ธ”๋ž™ํ”„๋ผ์ด๋ฐ์ด ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ Seed ๋ฐ์ดํ„ฐ +### ์‹คํ–‰ ์ˆœ์„œ: 1 โ†’ 2 โ†’ 3 ์ˆœ์„œ๋Œ€๋กœ ์‹คํ–‰ +### ์ „์ œ: commerce-api ์‹คํ–‰ ์ค‘, Admin LDAP ํ—ค๋” ํ•„์š” + +@baseUrl = http://localhost:8080 +@adminLdap = loopers.admin + +### 1. ํ…Œ์ŠคํŠธ ๋ธŒ๋žœ๋“œ ์ƒ์„ฑ +POST {{baseUrl}}/api-admin/v1/brands +Content-Type: application/json +X-Loopers-Ldap: {{adminLdap}} + +{ + "name": "๋ธ”ํ”„ํ…Œ์ŠคํŠธ๋ธŒ๋žœ๋“œ", + "description": "๋ธ”๋ž™ํ”„๋ผ์ด๋ฐ์ด ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ ์ „์šฉ ๋ธŒ๋žœ๋“œ", + "logoImageUrl": "https://example.com/logo.png" +} + +### + +### 2-A. ํ•œ๊ณ„์  ํƒ์ƒ‰ ์ „์šฉ ์ƒํ’ˆ ์ƒ์„ฑ (์žฌ๊ณ  10,000 โ†’ ์žฌ๊ณ  ๊ณ ๊ฐˆ ๋…ธ์ด์ฆˆ ์ œ๊ฑฐ) +### โ†’ ์‘๋‹ต์—์„œ productId ํ™•์ธ ํ›„ ์•„๋ž˜ SQL๋กœ ์žฌ๊ณ  ๋“ฑ๋ก +POST {{baseUrl}}/api-admin/v1/products +Content-Type: application/json +X-Loopers-Ldap: {{adminLdap}} + +{ + "brandId": 1, + "name": "ํ•œ๊ณ„์ ํ…Œ์ŠคํŠธ์ƒํ’ˆ", + "price": 10000, + "description": "breakpoint.js ์ „์šฉ - ์žฌ๊ณ  10000", + "thumbnailImageUrl": "https://example.com/bp.png" +} + +### + +### 2-B. ๋ธ”๋ž™ํ”„๋ผ์ด๋ฐ์ด ์ „์šฉ ์ƒํ’ˆ ์ƒ์„ฑ (์žฌ๊ณ  50 โ†’ ํ•œ์ • ์ˆ˜๋Ÿ‰ ์žฌํ˜„) +### โ†’ ์‘๋‹ต์—์„œ productId ํ™•์ธ ํ›„ ์•„๋ž˜ SQL๋กœ ์žฌ๊ณ  ๋“ฑ๋ก +POST {{baseUrl}}/api-admin/v1/products +Content-Type: application/json +X-Loopers-Ldap: {{adminLdap}} + +{ + "brandId": 1, + "name": "ํ•œ์ •ํŒ ๋ธ”ํ”„ ์ƒํ’ˆ", + "price": 99000, + "description": "black-friday.js ์ „์šฉ - ์žฌ๊ณ  50", + "thumbnailImageUrl": "https://example.com/bf.png" +} + +### + +### 3. ์žฌ๊ณ  ๋“ฑ๋ก (Admin API์— ์žฌ๊ณ  ์ˆ˜์ • ์—”๋“œํฌ์ธํŠธ ์—†์œผ๋ฏ€๋กœ ์ง์ ‘ SQL ์‚ฌ์šฉ) +### MySQL ์ ‘์†: localhost:3307 / root / root +### +### ํ•œ๊ณ„์  ํ…Œ์ŠคํŠธ ์ƒํ’ˆ ์žฌ๊ณ  (productId๋Š” 2-A ์‘๋‹ต์—์„œ ํ™•์ธ): +### INSERT INTO stock (product_id, quantity) VALUES ({productId_A}, 10000); +### +### ๋ธ”๋ž™ํ”„๋ผ์ด๋ฐ์ด ์ƒํ’ˆ ์žฌ๊ณ  (productId๋Š” 2-B ์‘๋‹ต์—์„œ ํ™•์ธ): +### INSERT INTO stock (product_id, quantity) VALUES ({productId_B}, 50); +### +### ์žฌ๊ณ  ๋ฆฌ์…‹ (ํ…Œ์ŠคํŠธ ์žฌ์‹คํ–‰ ์‹œ): +### UPDATE stock SET quantity = 50 WHERE product_id = {productId_B}; +### UPDATE stock SET quantity = 10000 WHERE product_id = {productId_A}; \ No newline at end of file diff --git a/k6/breakpoint.js b/k6/breakpoint.js new file mode 100644 index 000000000..d502d7ef9 --- /dev/null +++ b/k6/breakpoint.js @@ -0,0 +1,142 @@ +/** + * ์ฃผ๋ฌธ ์‹œ์Šคํ…œ ํ•œ๊ณ„์  ํƒ์ƒ‰ (Breakpoint Test) + * + * ๋ชฉ์ : ํ˜„์žฌ ์ฝ”๋“œ(๋น„๊ด€์  ๋ฝ + @Transactional)์—์„œ ๋ช‡ ๋ช…๊นŒ์ง€ P99 < 2000ms ๋ฅผ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ์ฐพ๊ธฐ + * + * ์‹คํ–‰ ๋ฐฉ๋ฒ•: + * k6 run -e PRODUCT_ID=1 k6/breakpoint.js + * + * Grafana ์—ฐ๋™ ์‹คํ–‰: + * k6 run -e PRODUCT_ID=1 --out influxdb=http://localhost:8086/k6 k6/breakpoint.js + * + * ์ฃผ์˜์‚ฌํ•ญ: + * - PRODUCT_ID๋Š” ์žฌ๊ณ  10,000์งœ๋ฆฌ ํ•œ๊ณ„์  ํ…Œ์ŠคํŠธ ์ „์šฉ ์ƒํ’ˆ ID๋ฅผ ์‚ฌ์šฉํ•  ๊ฒƒ + * - seed-black-friday.http ๋จผ์ € ์‹คํ–‰ ํ›„ ์ด ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰ + */ + +import http from 'k6/http'; +import { check } from 'k6'; +import { Trend, Rate, Counter } from 'k6/metrics'; + +const BASE_URL = __ENV.BASE_URL || 'http://localhost:8080'; +const PRODUCT_ID = __ENV.PRODUCT_ID || '1'; +const PASSWORD = 'Bptest123!'; +const USER_PREFIX = 'bp_user_'; +const TOTAL_USERS = 300; + +// ์ปค์Šคํ…€ ๋ฉ”ํŠธ๋ฆญ +const orderDuration = new Trend('order_duration', true); +const orderSuccess = new Counter('order_success'); +const stockExhausted = new Counter('stock_exhausted'); +const sysError = new Counter('sys_error'); +const abnormalRate = new Rate('abnormal_error_rate'); + +export const options = { + // ๋‹จ๊ณ„์ ์œผ๋กœ VU ์ฆ๊ฐ€ โ†’ ์–ด๋А ๊ตฌ๊ฐ„์—์„œ P99๊ฐ€ ๊บพ์ด๋Š”์ง€ ๊ด€์ฐฐ + stages: [ + { duration: '5s', target: 10 }, // 1๋‹จ๊ณ„ ์ง„์ž… + { duration: '30s', target: 10 }, // 1๋‹จ๊ณ„ ์œ ์ง€ (๋ฒ ์ด์Šค๋ผ์ธ) + { duration: '5s', target: 30 }, // 2๋‹จ๊ณ„ ์ง„์ž… + { duration: '30s', target: 30 }, // 2๋‹จ๊ณ„ ์œ ์ง€ + { duration: '5s', target: 50 }, // 3๋‹จ๊ณ„ ์ง„์ž… + { duration: '30s', target: 50 }, // 3๋‹จ๊ณ„ ์œ ์ง€ + { duration: '5s', target: 100 }, // 4๋‹จ๊ณ„ ์ง„์ž… (ํ•œ๊ณ„์  ์˜ˆ์ƒ) + { duration: '30s', target: 100 }, // 4๋‹จ๊ณ„ ์œ ์ง€ + { duration: '5s', target: 150 }, // 5๋‹จ๊ณ„ ์ง„์ž… + { duration: '30s', target: 150 }, // 5๋‹จ๊ณ„ ์œ ์ง€ + { duration: '5s', target: 200 }, // 6๋‹จ๊ณ„ ์ง„์ž… (๋ธ”๋ž™ํ”„๋ผ์ด๋ฐ์ด ์ˆ˜์ค€) + { duration: '30s', target: 200 }, // 6๋‹จ๊ณ„ ์œ ์ง€ + { duration: '5s', target: 300 }, // 7๋‹จ๊ณ„ ์ง„์ž… (๊ณผ๋ถ€ํ•˜) + { duration: '30s', target: 300 }, // 7๋‹จ๊ณ„ ์œ ์ง€ + { duration: '5s', target: 0 }, // ์ข…๋ฃŒ + ], + thresholds: { + // ์ด ์ž„๊ณ„๊ฐ’์„ ์–ด๋А ๊ตฌ๊ฐ„์—์„œ ์œ„๋ฐ˜ํ•˜๋Š”์ง€๊ฐ€ ํ•ต์‹ฌ ๊ด€์ฐฐ ํฌ์ธํŠธ + 'order_duration': ['p(99)<2000'], + 'abnormal_error_rate': ['rate<0.05'], + }, + summaryTrendStats: ['avg', 'med', 'p(90)', 'p(95)', 'p(99)', 'max'], +}; + +export function setup() { + console.log(`[setup] ํ…Œ์ŠคํŠธ ์œ ์ € ${TOTAL_USERS}๋ช… ๋“ฑ๋ก ์‹œ์ž‘ (์ด๋ฏธ ์กด์žฌํ•˜๋ฉด ๋ฌด์‹œ)`); + + const headers = { 'Content-Type': 'application/json' }; + + for (let i = 1; i <= TOTAL_USERS; i++) { + const loginId = `${USER_PREFIX}${String(i).padStart(3, '0')}`; + http.post( + `${BASE_URL}/api/v1/users`, + JSON.stringify({ + loginId, + password: PASSWORD, + name: `๋ถ€ํ•˜ํ…Œ์ŠคํŠธ${i}`, + birthDate: '19900101', + email: `${loginId}@loadtest.com`, + }), + { headers } + ); + } + + console.log('[setup] ์œ ์ € ๋“ฑ๋ก ์™„๋ฃŒ. ํ…Œ์ŠคํŠธ ์‹œ์ž‘.'); + return { productId: parseInt(PRODUCT_ID) }; +} + +export default function (data) { + const vuIndex = ((__VU - 1) % TOTAL_USERS) + 1; + const loginId = `${USER_PREFIX}${String(vuIndex).padStart(3, '0')}`; + + const headers = { + 'Content-Type': 'application/json', + 'X-Loopers-LoginId': loginId, + 'X-Loopers-LoginPw': PASSWORD, + }; + + const res = http.post( + `${BASE_URL}/api/v1/orders`, + JSON.stringify({ + items: [{ productId: data.productId, quantity: 1 }], + }), + { headers, timeout: '10s' } + ); + + orderDuration.add(res.timings.duration); + + if (res.status === 201) { + orderSuccess.add(1); + abnormalRate.add(false); + check(res, { '์ฃผ๋ฌธ ์„ฑ๊ณต (201)': (r) => r.status === 201 }); + } else if (res.status === 400) { + const isStockError = res.body && res.body.includes('์žฌ๊ณ ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค'); + if (isStockError) { + stockExhausted.add(1); + abnormalRate.add(false); + } else { + sysError.add(1); + abnormalRate.add(true); + check(res, { '๋น„์ •์ƒ 400': () => false }); + } + } else { + // 500, ํƒ€์ž„์•„์›ƒ, ์„œํ‚ท๋ธŒ๋ ˆ์ด์ปค ๋“ฑ ๋น„์ •์ƒ ์—๋Ÿฌ + sysError.add(1); + abnormalRate.add(true); + check(res, { [`๋น„์ •์ƒ ์—๋Ÿฌ (${res.status})`]: () => false }); + } + // sleep ์—†์Œ โ†’ ์ตœ๋Œ€ ์ฒ˜๋ฆฌ๋Ÿ‰ ์ƒ์„ฑ, ํ•œ๊ณ„์  ๋น ๋ฅด๊ฒŒ ํƒ์ƒ‰ +} + +export function teardown() { + console.log(''); + console.log('=== ํ•œ๊ณ„์  ํƒ์ƒ‰ ๊ฒฐ๊ณผ ==='); + console.log('Grafana์—์„œ order_duration p(99) ์ถ”์ด๋ฅผ ํ™•์ธํ•˜์„ธ์š”.'); + console.log('P99๊ฐ€ 2000ms๋ฅผ ์ดˆ๊ณผํ•˜๋Š” ๊ตฌ๊ฐ„์˜ VU ์ˆ˜ = ํ˜„์žฌ ์‹œ์Šคํ…œ ํ•œ๊ณ„์ '); + console.log(''); + console.log('๋‹จ๊ณ„๋ณ„ VU ๊ธฐ์ค€:'); + console.log(' 1๋‹จ๊ณ„: 10 VU (๋ฒ ์ด์Šค๋ผ์ธ)'); + console.log(' 2๋‹จ๊ณ„: 30 VU'); + console.log(' 3๋‹จ๊ณ„: 50 VU'); + console.log(' 4๋‹จ๊ณ„: 100 VU โ† ํ•œ๊ณ„์  ์˜ˆ์ƒ ๊ตฌ๊ฐ„'); + console.log(' 5๋‹จ๊ณ„: 150 VU'); + console.log(' 6๋‹จ๊ณ„: 200 VU (๋ธ”๋ž™ํ”„๋ผ์ด๋ฐ์ด ์ˆ˜์ค€)'); + console.log(' 7๋‹จ๊ณ„: 300 VU (๊ณผ๋ถ€ํ•˜)'); +}