diff --git a/README.md b/README.md index 852fd83..b3e49e1 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # ๐Ÿ›’ Allday Project Commerce > **Allday Project** ์•„ํ‹ฐ์ŠคํŠธ์˜ ๊ณต์‹ ๊ตฟ์ฆˆยท์•จ๋ฒ”ยท์ด๋ฒคํŠธ ํ‹ฐ์ผ“์„ ํŒ๋งคํ•˜๋Š” ์ปค๋จธ์Šค ํ”Œ๋žซํผ -> ํšŒ์›๊ฐ€์ž…ยท๋กœ๊ทธ์ธ๋ถ€ํ„ฐ ์ƒํ’ˆ ์กฐํšŒ, ์žฅ๋ฐ”๊ตฌ๋‹ˆ, ์ฃผ๋ฌธ, ๊ฒฐ์ œ ํ™•์ •, ํ™˜๋ถˆ, ์‹ค์‹œ๊ฐ„ ์ฑ„ํŒ… ์ƒ๋‹ด๊นŒ์ง€ +> ํšŒ์›๊ฐ€์ž…ยท๋กœ๊ทธ์ธ๋ถ€ํ„ฐ ์ƒํ’ˆ ์กฐํšŒ, ์žฅ๋ฐ”๊ตฌ๋‹ˆ, ์ฃผ๋ฌธ, ๊ฒฐ์ œ ํ™•์ •, ์‹ค์‹œ๊ฐ„ ์ฑ„ํŒ… ์ƒ๋‹ด๊นŒ์ง€ > ์ปค๋จธ์Šค์˜ ์ „์ฒด ํ๋ฆ„์„ ์ง์ ‘ ์„ค๊ณ„ํ•˜๊ณ  ๊ตฌํ˜„ํ•œ ํ”„๋กœ์ ํŠธ์ž…๋‹ˆ๋‹ค. **ํ”„๋กœ์ ํŠธ ๊ธฐ๊ฐ„:** 2026.04.08 ~ 2026.04.28 @@ -14,11 +14,11 @@ 1. [ํŒ€์›๋ณ„ ์—ญํ• ](#-ํŒ€์›๋ณ„-์—ญํ• ) 2. [๊ธฐ์ˆ  ์Šคํƒ](#-๊ธฐ์ˆ -์Šคํƒ) -3. [ํ•ต์‹ฌ ์„ค๊ณ„ ๊ฒฐ์ •์‚ฌํ•ญ](#-ํ•ต์‹ฌ-์„ค๊ณ„-๊ฒฐ์ •์‚ฌํ•ญ) +3. [ํ•ต์‹ฌ ์„ค๊ณ„ ๊ฒฐ์ • ์‚ฌํ•ญ](#-ํ•ต์‹ฌ-์„ค๊ณ„-๊ฒฐ์ •์‚ฌํ•ญ) 4. [ERD ์„ค๊ณ„](#-erd-์„ค๊ณ„) 5. [ํŒจํ‚ค์ง€ ๊ตฌ์กฐ](#-ํŒจํ‚ค์ง€-๊ตฌ์กฐ) 6. [API ๋ช…์„ธ](#-api-๋ช…์„ธ) -7. [ํ•„์ˆ˜ ๊ตฌํ˜„ โ€” ๋™์‹œ์„ฑ ์ œ์–ด](#-ํ•„์ˆ˜-๊ตฌํ˜„--๋™์‹œ์„ฑ-์ œ์–ด) +7. [๋„์ „ ๊ตฌํ˜„ โ€” ๋™์‹œ์„ฑ ์ œ์–ด](#-ํ•„์ˆ˜-๊ตฌํ˜„--๋™์‹œ์„ฑ-์ œ์–ด) 8. [ํ•„์ˆ˜ ๊ตฌํ˜„ โ€” ์บ์‹ฑ ๋ฐ ์ธ๊ธฐ ๊ฒ€์ƒ‰์–ด](#-ํ•„์ˆ˜-๊ตฌํ˜„--์บ์‹ฑ-๋ฐ-์ธ๊ธฐ-๊ฒ€์ƒ‰์–ด) 9. [๋„์ „ ๊ตฌํ˜„ โ€” ์‹ค์‹œ๊ฐ„ ์ฑ„ํŒ…](#-๋„์ „-๊ตฌํ˜„--์‹ค์‹œ๊ฐ„-์ฑ„ํŒ…) 10. [๋„์ „ ๊ตฌํ˜„ โ€” ์ธ๋ฑ์Šค ์ตœ์ ํ™”](#-๋„์ „-๊ตฌํ˜„--์ธ๋ฑ์Šค-์ตœ์ ํ™”) @@ -27,15 +27,14 @@ 13. [๋กœ์ปฌ ์‹คํ–‰ ๋ฐฉ๋ฒ•](#-๋กœ์ปฌ-์‹คํ–‰-๋ฐฉ๋ฒ•) --- - ## ๐Ÿ‘ฅ ํŒ€์›๋ณ„ ์—ญํ•  -| ์—ญํ•  | ์ด๋ฆ„ | ๋‹ด๋‹น ์—…๋ฌด | -|------|------|-----------| -| ๐Ÿ‘‘ ๋ฆฌ๋”ยท๊ฐœ๋ฐœ | ์ด์žฌ๋ฏผ | ๋งˆ์ผ์Šคํ†ค, ์ธ์ฆ/์ธ๊ฐ€(JWT), ๊ณตํ†ต ์ฝ”๋“œ, ์ฃผ๋ฌธ ๋„๋ฉ”์ธ, ํ”„๋ก ํŠธ์—”๋“œ | -| ๐Ÿ’ณ ๊ฐœ๋ฐœยท๊ธฐ๋ก | ๋ฌธํ˜œ๋ฆฐ | ์‚ฌ์šฉ์ž ๋„๋ฉ”์ธ, ํšŒ์˜๋กยทSA ๋ฌธ์„œ, API ๋ช…์„ธ์„œ, ERD | -| ๐Ÿ“ฆ ๊ฐœ๋ฐœ | ๋ฐ•๊ฒฝํ™” | ์ƒํ’ˆ ๋„๋ฉ”์ธ, ํ”„๋ก ํŠธ์—”๋“œ | -| ๐Ÿ’ฐ ๊ฐœ๋ฐœ | ๋ฐ•์†Œ์˜ | ๊ฒฐ์ œยทํ™˜๋ถˆ ๋„๋ฉ”์ธ, API ๋ช…์„ธ์„œ, ERD | +| ์—ญํ•  | ์ด๋ฆ„ | ๋‹ด๋‹น ์—…๋ฌด | +|------|------|--------------------------------------------------------| +| ๐Ÿ‘‘ ๋ฆฌ๋”ยท๊ฐœ๋ฐœ | ์ด์žฌ๋ฏผ | ๋งˆ์ผ์Šคํ†ค, ์ธ์ฆ/์ธ๊ฐ€(JWT), ๊ณตํ†ต ์ฝ”๋“œ, ์ฃผ๋ฌธ ๋„๋ฉ”์ธ, ํ”„๋ก ํŠธ์—”๋“œ, ์‹ค์‹œ๊ฐ„ ์ฑ„ํŒ…, ์ธ๊ธฐ ๊ฒ€์ƒ‰์–ด | +| ๐Ÿ’ณ ๊ฐœ๋ฐœยท๊ธฐ๋ก | ๋ฌธํ˜œ๋ฆฐ | ์‚ฌ์šฉ์ž ๋„๋ฉ”์ธ, ํšŒ์˜๋กยทSA ๋ฌธ์„œ, API ๋ช…์„ธ์„œ, ERD | +| ๐Ÿ“ฆ ๊ฐœ๋ฐœ | ๋ฐ•๊ฒฝํ™” | ์ƒํ’ˆ ๋„๋ฉ”์ธ, ํ”„๋ก ํŠธ์—”๋“œ | +| ๐Ÿ’ฐ ๊ฐœ๋ฐœ | ๋ฐ•์†Œ์˜ | ๊ฒฐ์ œ ๋„๋ฉ”์ธ, ์ฃผ๋ฌธ ๋ฐ ์žฌ๊ณ ์ฐจ๊ฐ ์ƒํƒœ ์ „์ด, API ๋ช…์„ธ์„œ, ERD, Docker-Compose , ๋™์‹œ์„ฑ --- @@ -59,96 +58,26 @@ ## ๐Ÿ”‘ ํ•ต์‹ฌ ์„ค๊ณ„ ๊ฒฐ์ •์‚ฌํ•ญ -| ํ•ญ๋ชฉ | ๊ฒฐ์ • ๋‚ด์šฉ | -|------|-----------| -| ์ธ์ฆ ๋ฐฉ์‹ | JWT Access Token(30๋ถ„) + Refresh Token(7์ผ), HttpOnly ์ฟ ํ‚ค ์ €์žฅ | +| ํ•ญ๋ชฉ | ๊ฒฐ์ • ๋‚ด์šฉ | +|------|-------------------------------------------------------------------------| +| ์ธ์ฆ ๋ฐฉ์‹ | JWT Access Token(30๋ถ„) + Refresh Token(7์ผ), HttpOnly ์ฟ ํ‚ค ์ €์žฅ | | ์ฃผ๋ฌธ ์‹๋ณ„์ž | NanoId ๊ธฐ๋ฐ˜ ๋‚ ์งœ ํฌํ•จ UID โ€” `ORD-YYYYMMDD-XXXXXXXX` / `PAY-YYYYMMDD-XXXXXXXX` | -| ์žฌ๊ณ  ์ฐจ๊ฐ ์‹œ์  | ๊ฒฐ์ œ ํ™•์ •(์„œ๋ฒ„ ๊ฒ€์ฆ ์™„๋ฃŒ) ํ›„ ์ฐจ๊ฐ. ๋ณ€๊ฒฝ ์ด๋ ฅ์€ `product_stock_logs`์— ๊ธฐ๋ก | -| ๋™์‹œ์„ฑ ์ฒ˜๋ฆฌ | Lettuce ๊ธฐ๋ฐ˜ Redis ๋ถ„์‚ฐ ๋ฝ (FAIL_FAST / RETRY / BLOCKING) + Redisson (Watchdog) + AOP | -| ์ฃผ๋ฌธ ์Šค๋ƒ…์ƒท | ๊ฒฐ์ œ ์™„๋ฃŒ ์‹œ `order_products`(์ƒํ’ˆ๋ช…ยท๊ฐ€๊ฒฉ) + `order_users`(์ฃผ๋ฌธ์ž ์ •๋ณด) ๋ณ„๋„ ์ €์žฅ | -| ํŽ˜์ด์ง• ๋ฐฉ์‹ | ์ฃผ๋ฌธยท์žฅ๋ฐ”๊ตฌ๋‹ˆ: ์ปค์„œ ๊ธฐ๋ฐ˜ ๋ฌดํ•œ ์Šคํฌ๋กค / ์ƒํ’ˆ ๋ชฉ๋ก: Offset ํŽ˜์ด์ง• (QueryDSL) | -| ์ธ๊ธฐ ๊ฒ€์ƒ‰์–ด | Redis ZSet ์‹ค์‹œ๊ฐ„ ์ง‘๊ณ„ โ†’ 1์‹œ๊ฐ„๋งˆ๋‹ค DB Write-back โ†’ ์ž์ • Top5 ์Šค๋ƒ…์ƒท. Caffeine L1 ์บ์‹œ | -| ์‹ค์‹œ๊ฐ„ ์ฑ„ํŒ… | WebSocket + STOMP + Redis Pub/Sub (๋‹ค์ค‘ ์„œ๋ฒ„ ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ ์ง€์›) | -| ์บ์‹œ ๊ตฌ์กฐ | L1(Caffeine ๋กœ์ปฌ) + L2(Redis) CompositeCacheManager ์ด์ค‘ ์ ์šฉ | -| ํ™˜๋ถˆ ๋ฒ”์œ„ | ์ „์•ก ํ™˜๋ถˆ๋งŒ ๊ตฌํ˜„ (๋ถ€๋ถ„ ํ™˜๋ถˆ ๋ฏธ์ง€์›) | -| ๋ฐฐ์†ก๋น„ | 3,000์› ๊ณ ์ • (50,000์› ์ด์ƒ ๊ตฌ๋งค ์‹œ ๋ฌด๋ฃŒ) | -| ์ฑ„ํŒ…๋ฐฉ ์ œํ•œ | ์œ ์ €๋‹น ํ™œ์„ฑ ์ฑ„ํŒ…๋ฐฉ 1๊ฐœ. `UNIQUE(user_id, active_flag)` NULL ํŠธ๋ฆญ | +| ์žฌ๊ณ  ์ฐจ๊ฐ ์‹œ์  | ๊ฒฐ์ œ ํ™•์ •(์„œ๋ฒ„ ๊ฒ€์ฆ ์™„๋ฃŒ) ํ›„ ์ฐจ๊ฐ. ๋ณ€๊ฒฝ ์ด๋ ฅ์€ `product_stock_logs`์— ๊ธฐ๋ก | +| ๋™์‹œ์„ฑ ์ฒ˜๋ฆฌ | Redisson ๊ธฐ๋ฐ˜ Redis ๋ถ„์‚ฐ๋ฝ - BLOCKING Watchdog + AOP ,Pessimistic Lock | +| ์ฃผ๋ฌธ ์Šค๋ƒ…์ƒท | ๊ฒฐ์ œ ์™„๋ฃŒ ์‹œ `order_products`(์ƒํ’ˆ๋ช…ยท๊ฐ€๊ฒฉ) + `order_users`(์ฃผ๋ฌธ์ž ์ •๋ณด) ๋ณ„๋„ ์ €์žฅ | +| ํŽ˜์ด์ง• ๋ฐฉ์‹ | ์ฃผ๋ฌธยท์žฅ๋ฐ”๊ตฌ๋‹ˆ: ์ปค์„œ ๊ธฐ๋ฐ˜ ๋ฌดํ•œ ์Šคํฌ๋กค / ์ƒํ’ˆ ๋ชฉ๋ก: Offset ํŽ˜์ด์ง• (QueryDSL) | +| ์ธ๊ธฐ ๊ฒ€์ƒ‰์–ด | Redis ZSet ์‹ค์‹œ๊ฐ„ ์ง‘๊ณ„ โ†’ 1์‹œ๊ฐ„๋งˆ๋‹ค DB Write-back โ†’ ์ž์ • Top5 ์Šค๋ƒ…์ƒท. Caffeine L1 ์บ์‹œ | +| ์‹ค์‹œ๊ฐ„ ์ฑ„ํŒ… | WebSocket + STOMP + Redis Pub/Sub (๋‹ค์ค‘ ์„œ๋ฒ„ ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ ์ง€์›) | +| ์บ์‹œ ๊ตฌ์กฐ | L1(Caffeine ๋กœ์ปฌ) + L2(Redis) CompositeCacheManager ์ด์ค‘ ์ ์šฉ | +| ๋ฐฐ์†ก๋น„ | 3,000์› ๊ณ ์ • (50,000์› ์ด์ƒ ๊ตฌ๋งค ์‹œ ๋ฌด๋ฃŒ) | +| ์ฑ„ํŒ…๋ฐฉ ์ œํ•œ | ์œ ์ €๋‹น ํ™œ์„ฑ ์ฑ„ํŒ…๋ฐฉ 1๊ฐœ. `UNIQUE(user_id, active_flag)` NULL ํŠธ๋ฆญ | --- ## ๐Ÿ—‚๏ธ ERD ์„ค๊ณ„ <> - -### ์ฃผ์š” ํ…Œ์ด๋ธ” ์„ค๋ช… - -
-users โ€” ์‚ฌ์šฉ์ž - -| ์ปฌ๋Ÿผ | ํƒ€์ž… | ์„ค๋ช… | -|------|------|------| -| id | BIGINT PK | AUTO_INCREMENT | -| name | VARCHAR(20) | NULL ํ—ˆ์šฉ | -| email | VARCHAR(100) | UNIQUE NOT NULL | -| password | VARCHAR(255) | BCrypt ์•”ํ˜ธํ™” | -| phone | VARCHAR(100) | NULL ํ—ˆ์šฉ | -| address | VARCHAR(250) | NULL ํ—ˆ์šฉ | -| role | ENUM(ADMIN, USER) | NOT NULL DEFAULT USER | - -
- -
-products โ€” ์ƒํ’ˆ - -| ์ปฌ๋Ÿผ | ํƒ€์ž… | ์„ค๋ช… | -|------|------|------| -| id | BIGINT PK | AUTO_INCREMENT | -| name | VARCHAR(100) | NOT NULL | -| price | BIGINT | NOT NULL (โ‰ฅ0) | -| stock | INT | NOT NULL (โ‰ฅ0) | -| status | ENUM | ON_SALE / SOLD_OUT / DISCONTINUED | -| category | ENUM | ALBUM / MERCH / EVENT | - -> **INDEX:** `price` / `name` / `(status, id)` / `(category, status)` - -
- -
-orders โ€” ์ฃผ๋ฌธ / order_products โ€” ์ฃผ๋ฌธ ์Šค๋ƒ…์ƒท / order_users โ€” ์ฃผ๋ฌธ์ž ์Šค๋ƒ…์ƒท - -- `orders`: `order_uid`(NanoId ๊ธฐ๋ฐ˜ ๋น„์ฆˆ๋‹ˆ์Šค ์‹๋ณ„์ž) ์‚ฌ์šฉ, ๋‚ด๋ถ€ PK ์™ธ๋ถ€ ๋…ธ์ถœ ๋ฐฉ์ง€ -- `order_products`: ๊ฒฐ์ œ ์™„๋ฃŒ ์‹œ ์ƒํ’ˆ๋ช…ยท๊ฐ€๊ฒฉ ์Šค๋ƒ…์ƒท ์ €์žฅ (์ƒํ’ˆ ์ˆ˜์ • ํ›„์—๋„ ์ฃผ๋ฌธ ๋‹น์‹œ ์ •๋ณด ๋ณด์กด) -- `order_users`: ๊ฒฐ์ œ ์™„๋ฃŒ ์‹œ ์ฃผ๋ฌธ์ž ์ •๋ณด ์Šค๋ƒ…์ƒท ์ €์žฅ - -> **INDEX:** `(user_id, id DESC)` โ€” ์ปค์„œ ๊ธฐ๋ฐ˜ ํŽ˜์ด์ง• ์ตœ์ ํ™” - -
- -
-payments โ€” ๊ฒฐ์ œ / refunds โ€” ํ™˜๋ถˆ - -- `payments`: `payment_uid`(PAY-YYYYMMDD-XXXXXXXX), `expires_at`(์ƒ์„ฑ ํ›„ 5๋ถ„) ๊ด€๋ฆฌ -- `refunds`: ํ™˜๋ถˆ ์š”์ฒญ(REQUESTED) โ†’ ์™„๋ฃŒ(SUCCESS) / ์‹คํŒจ(FAILED) ์ƒํƒœ ๊ด€๋ฆฌ - -
- -
-search_keywords / popular_keywords โ€” ์ธ๊ธฐ ๊ฒ€์ƒ‰์–ด - -- `search_keywords`: `UNIQUE(keyword, search_date)` โ€” ๋‚ ์งœ๋ณ„ ๊ฒ€์ƒ‰ ํšŸ์ˆ˜ ์ง‘๊ณ„ -- `popular_keywords`: ์ž์ • Top5 ์Šค๋ƒ…์ƒท. `is_fallback=true`๋Š” ๋Œ€์ฒด ํ‚ค์›Œ๋“œ - -
- -
-chat_rooms / chat_messages โ€” ์‹ค์‹œ๊ฐ„ ์ฑ„ํŒ… - -- `chat_rooms`: `UNIQUE(user_id, active_flag)` + NULL ํŠธ๋ฆญ์œผ๋กœ ํ™œ์„ฑ ๋ฐฉ 1๊ฐœ ์ œํ•œ -- `chat_messages`: `INDEX(room_id, id)` โ€” ์ปค์„œ ๊ธฐ๋ฐ˜ ํŽ˜์ด์ง• ์ตœ์ ํ™” - -
- ---- +![img.png](images/ERD.png) ## ๐Ÿ“ ํŒจํ‚ค์ง€ ๊ตฌ์กฐ @@ -167,7 +96,7 @@ src/main/java/jpa/basic/alldayprojectcommerce/ โ”‚ โ”œโ”€โ”€ exception/ # GlobalExceptionHandler, ErrorCode, CustomException โ”‚ โ”œโ”€โ”€ lock/ โ”‚ โ”‚ โ”œโ”€โ”€ annotation/ # @RedisLock, @RedissonLock -โ”‚ โ”‚ โ”œโ”€โ”€ aspect/ # RedisLockAspect, RedissonLockAspect (SpEL ํ‚ค ํŒŒ์‹ฑ) +โ”‚ โ”‚ โ”œโ”€โ”€ aspect/ # RedisLockAspect, RedissonLockAspect โ”‚ โ”‚ โ”œโ”€โ”€ enums/ # RedisLockStrategy (FAIL_FAST, RETRY, BLOCKING) โ”‚ โ”‚ โ”œโ”€โ”€ repository/ # RedisLockRepository (SET NX + Lua Script ํ•ด์ œ) โ”‚ โ”‚ โ””โ”€โ”€ service/ # RedisLockService, RedissonLockService @@ -184,7 +113,6 @@ src/main/java/jpa/basic/alldayprojectcommerce/ โ”œโ”€โ”€ order/ # ์ฃผ๋ฌธ (์ฃผ๋ฌธ์„œ ์ƒ์„ฑยท์กฐํšŒยท์ƒ์„ธยท์ด๋ฒคํŠธ ์ฃผ๋ฌธ) โ”‚ โ””โ”€โ”€ service/event/ # EventOrderService (์ด๋ฒคํŠธ ์ „์šฉ ์ฃผ๋ฌธ ๋กœ์ง) โ”œโ”€โ”€ payment/ # ๊ฒฐ์ œ (์ƒ์„ฑยทํ™•์ •ยท๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ) - โ”œโ”€โ”€ refund/ # ํ™˜๋ถˆ (์š”์ฒญยท์ƒํƒœ ๊ด€๋ฆฌ) โ”œโ”€โ”€ keyword/ # ์ธ๊ธฐ ๊ฒ€์ƒ‰์–ด (Redis ZSet + Write-back + Caffeine) โ”‚ โ””โ”€โ”€ scheduler/ # KeywordScheduler (Write-backยท์ž์ • ์ดˆ๊ธฐํ™”) โ”œโ”€โ”€ chat/ # ์‹ค์‹œ๊ฐ„ ์ฑ„ํŒ… @@ -238,14 +166,14 @@ src/main/java/jpa/basic/alldayprojectcommerce/ ### ์ฃผ๋ฌธ / ๊ฒฐ์ œ -| Method | ๊ฒฝ๋กœ | ์„ค๋ช… | ์ธ์ฆ | -|--------|------|------|------| -| POST | `/api/orders` | ์ฃผ๋ฌธ์„œ ์ƒ์„ฑ | โœ… | -| GET | `/api/orders` | ์ฃผ๋ฌธ ๋ชฉ๋ก (์ปค์„œ ํŽ˜์ด์ง•) | โœ… | -| GET | `/api/orders/{orderUid}` | ์ฃผ๋ฌธ์„œ ์กฐํšŒ (๊ฒฐ์ œ ์ „) | โœ… | +| Method | ๊ฒฝ๋กœ | ์„ค๋ช… | ์ธ์ฆ | +|--------|------|-----------------|------| +| POST | `/api/orders` | ์ฃผ๋ฌธ์„œ ์ƒ์„ฑ | โœ… | +| GET | `/api/orders` | ์ฃผ๋ฌธ ๋ชฉ๋ก ์กฐํšŒ | โœ… | +| GET | `/api/orders/{orderUid}` | ์ฃผ๋ฌธ์„œ ์กฐํšŒ (๊ฒฐ์ œ ์ „) | โœ… | | GET | `/api/orders/{orderUid}/details` | ์ฃผ๋ฌธ ์ƒ์„ธ ์กฐํšŒ (๊ฒฐ์ œ ํ›„) | โœ… | -| POST | `/api/orders/{orderUid}/payments` | ๊ฒฐ์ œ ์ƒ์„ฑ | โœ… | -| POST | `/api/orders/{orderUid}/payments/{paymentUid}/confirm` | ๊ฒฐ์ œ ํ™•์ • | โœ… | +| POST | `/api/orders/{orderUid}/payments` | ๊ฒฐ์ œ ์ƒ์„ฑ | โœ… | +| POST | `/api/orders/{orderUid}/payments/{paymentUid}/confirm` | ๊ฒฐ์ œ ํ™•์ • | โœ… | ### ์ด๋ฒคํŠธ / ๊ฒ€์ƒ‰์–ด / ์ฑ„ํŒ… @@ -279,11 +207,15 @@ src/main/java/jpa/basic/alldayprojectcommerce/ ```java // 100๊ฐœ ์Šค๋ ˆ๋“œ๊ฐ€ ๋™์‹œ์— ์ถœ๋ฐœ CyclicBarrier startBarrier = new CyclicBarrier(100); -// ๋ฝ ์—†๋Š” ๋ฒ„์ „์€ ์ •ํ•ฉ์„ฑ์ด ๊นจ์ง€๋Š” ๊ฒƒ์„ ํ…Œ์ŠคํŠธ๋กœ ์ฆ๋ช… -assertThat(isExactlyCorrect).isFalse(); // ๋ฝ ์—†์Œ โ†’ ํ…Œ์ŠคํŠธ ์˜๋„์  ์‹คํŒจ -``` +// ๋ฝ ์—†๋Š” ๋ฒ„์ „์€ ์ •ํ•ฉ์„ฑ์ด ๊นจ์ง€๋Š” ๊ฒƒ์ด ๋ชฉ์ , ์ฆ‰ ์ •์ƒ ๊ธฐ๋Œ€๊ฐ’(10/90/10/0)๊ณผ ๋‹ฌ๋ผ์•ผ ํ•œ๋‹ค. +boolean isExactlyCorrect = + result.successCount() == 10 && + result.failCount() == 90 && + orderCount == 10 && + product.getStock() == 0; -<<๋™์‹œ์„ฑ ํ…Œ์ŠคํŠธ ์‹คํŒจ ๊ฒฐ๊ณผ ์‚ฌ์ง„ (๋ฝ ์—†์Œ - ์žฌ๊ณ  ์ •ํ•ฉ์„ฑ ๊นจ์ง)>> +assertThat(isExactlyCorrect).isFalse(); +``` ### Redis ๋ถ„์‚ฐ ๋ฝ ๊ตฌํ˜„ @@ -312,6 +244,24 @@ String script = """ | **RETRY** | ์ตœ๋Œ€ 15ํšŒ, 100ms ๊ฐ„๊ฒฉ ์žฌ์‹œ๋„ | ์žฌ์‹œ๋„๊ฐ€ ์˜๋ฏธ ์žˆ๋Š” ๊ฒฝ์šฐ | | **BLOCKING** | ์ตœ๋Œ€ 5์ดˆ, 50ms ๊ฐ„๊ฒฉ ๋Œ€๊ธฐ | ์ฒ˜๋ฆฌ๋Ÿ‰๋ณด๋‹ค ์ •ํ•ฉ์„ฑ ์šฐ์„  | + +### Redisson ๊ธฐ๋ฐ˜ ๋ถ„์‚ฐ ๋ฝ ๊ตฌํ˜„ +```java +if (leaseTimeMillis < 0) { + locked = lock.tryLock(waitTimeMillis, TimeUnit.MILLISECONDS); +} else { + locked = lock.tryLock(waitTimeMillis, leaseTimeMillis, TimeUnit.MILLISECONDS); +} +``` + +```java +if (locked && lock.isHeldByCurrentThread()) { + lock.unlock(); + log.info("[RedissonLock] ๋ฝ ํ•ด์ œ ์„ฑ๊ณต key={}", key); +} +``` + + #### AOP ๊ธฐ๋ฐ˜ ๋ฝ ์ ์šฉ (`@RedisLock`, `@RedissonLock`) ๋น„์ฆˆ๋‹ˆ์Šค ์ฝ”๋“œ์—์„œ ๋ฝ ์ฒ˜๋ฆฌ ๋กœ์ง์„ ์™„์ „ํžˆ ๋ถ„๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค. @@ -325,7 +275,9 @@ String script = """ public EventOrderResponse createEventOrderWithRedissonLockAopBlockingWatchdog( Long productId, Long userId) { return eventOrderService.createEventOrder(productId, userId); -} +}``` + + ``` SpEL ํ‘œํ˜„์‹์œผ๋กœ ๋™์  ํ‚ค ์ƒ์„ฑ: @@ -350,27 +302,24 @@ Optional findByIdForUpdate(@Param("productId") Long productId); ### ๋ฝ ๋ฒ„์ „๋ณ„ ๋น„๊ต ํ…Œ์ŠคํŠธ (v1 ~ v8) -| ๋ฒ„์ „ | ์ „๋žต | ์„ฑ๊ณต ์ˆ˜ | ์‹คํŒจ ์ˆ˜ | ์ •ํ•ฉ์„ฑ | -|------|------|---------|---------|--------| -| v1 | ๋ฝ ์—†์Œ | - | - | โŒ ๊นจ์ง | -| v2 | DB ๋น„๊ด€์  ๋ฝ๋งŒ | 10 | 90 | โœ… | -| v3 | Redisson Retry + TTL | 10 | 90 | โœ… | -| v4 | Redisson Retry + Watchdog | 10 | 90 | โœ… | -| v5 | Redisson FailFast | 0~1 | 99~100 | โœ… | -| v6 | Redisson Blocking + TTL | 10 | 90 | โœ… | -| **v7** | **Redisson Blocking + Watchdog** | **10** | **90** | **โœ… ์ตœ์ข… ์„ ํƒ** | -| v8 | Redisson Blocking + Watchdog + ๋น„๊ด€๋ฝ | 10 | 90 | โœ… | +| ๋ฐฉ์‹ | ์„ฑ๊ณต ์ˆ˜ | ์ตœ์ข… ์žฌ๊ณ  | ์ดˆ๊ณผ ํŒ๋งค | p95 | TPS | timeout | lock_fail_count | waitTime | leaseTime | ๋ณ‘๋ชฉ ์œ„์น˜ | ํ‰๊ฐ€ | +|------|--------|----------|----------|------|------|---------|-----------------|----------|-----------|------------|------| +| v1 ๋ฝ ์—†์Œ | 998 | โŒ ์ดˆ๊ณผ ํŒ๋งค | โŒ ๋ฐœ์ƒ | 8.31s | 116 req/s | 0 | 0 | - | - | DB ์ •ํ•ฉ์„ฑ | โŒ ํƒˆ๋ฝ | +| v2 DB ๋น„๊ด€๋ฝ | 100 | 0 | 0 | 2.21s | 413 req/s | 0 | 0 | - | - | DB row lock | โœ… ๋งค์šฐ ์šฐ์ˆ˜ | +| v3 Redis Retry (TTL) | 100 | 0 | 0 | 7.97s | 119 req/s | 0 | 4 | 5s | 3s | Redis ๋Œ€๊ธฐ | โš ๏ธ ๋А๋ฆผ | +| v4 Redis Blocking (TTL) | 100 | 0 | 0 | 5.50s | 169 req/s | 0 | 0 | 10s | 5s | Redis ๋Œ€๊ธฐ | โ–ณ ๊ฒฝ๊ณ„ | +| v5 Redis FailFast | 6 | โŒ ์žฌ๊ณ  ๋‚จ์Œ | 0 | 1.14s | 567 req/s | 0 | 994 | 0 | 3s | lock fail | โŒ ํƒˆ๋ฝ | +| v6 Redis Retry (Watchdog) | 100 | 0 | 0 | 5.06s | 191 req/s | 0 | 0 | 5s | -1 | Redis ๋Œ€๊ธฐ | โ–ณ ๊ฒฝ๊ณ„ | +| v7 Redis Blocking (Watchdog) | 100 | 0 | 0 | 3.91s | 226 req/s | 0 | 0 | 10s | -1 | Redis ์ง๋ ฌํ™” | ๐Ÿ”ฅ ์ตœ๊ณ  | +| v8 Redis + DB ๋น„๊ด€๋ฝ | 100 | 0 | 0 | 5.83s | 164 req/s | 0 | 0 | 10s | -1 | Redis + DB | โœ… ์•ˆ์ • but ๋А๋ฆผ | #### ์ตœ์ข… ์„ ํƒ: **v7 โ€” Redisson Blocking + Watchdog** **์„ ํƒ ์ด์œ :** -- Blocking ์ „๋žต์œผ๋กœ ์žฌ๊ณ  100๊ฐœ(ํ‹ฐ์ผ“ 10๊ฐœ ๊ธฐ์ค€)๋ฅผ ์ตœ๋Œ€ํ•œ ์†Œ์ง„ +- Blocking ์ „๋žต์œผ๋กœ ์žฌ๊ณ  100๊ฐœ(ํ‹ฐ์ผ“ 10๊ฐœ ๊ธฐ์ค€)๋ฅผ ๋ชจ๋‘ ์†Œ์ง„ - Watchdog์œผ๋กœ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด ๊ธธ์–ด์ ธ๋„ TTL ์ž๋™ ์—ฐ์žฅ โ†’ ์•ˆ์ „ - ๋น„๊ด€๋ฝ ์ค‘์ฒฉ(v8) ๋Œ€๋น„ ๋ถˆํ•„์š”ํ•œ DB ๋ฝ ์˜ค๋ฒ„ํ—ค๋“œ ์—†์Œ - -<<๋™์‹œ์„ฑ ํ…Œ์ŠคํŠธ ์„ฑ๊ณต ๊ฒฐ๊ณผ ์‚ฌ์ง„ (๋ฝ ์ ์šฉ ํ›„ โ€” ์žฌ๊ณ  10๊ฐœ ์ •ํ™•ํžˆ ์†Œ์ง„)>> - -<> +- ๋น„๊ด€๋ฝ ๋Œ€๋น„ DB ๋ถ€ํ•˜ ๋ถ„์‚ฐ ๊ฐ€๋Šฅ --- @@ -758,7 +707,6 @@ Long nextCursor = hasNext ? getId.apply(content.getLast()) : null; ### ์‚ฌ์ „ ์š”๊ตฌ์‚ฌํ•ญ - Docker & Docker Compose -- Java 21 ### ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์„ค์ • @@ -786,10 +734,7 @@ docker compose up -d ```bash # ์ด๋ฒคํŠธ ์„ ์ฐฉ์ˆœ ์ฃผ๋ฌธ ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ -docker compose --profile test run k6 run /scripts/event-order-load-test.js - -# ๋ฒ„์ „๋ณ„ ๋น„๊ต ํ…Œ์ŠคํŠธ (v1~v8) -bash run-event-order-compare.sh +PRODUCT_ID=4 VUS=1000 BASE_URL=http://app:8090 DB_PASSWORD=12345678 ./run-event-order-compare.sh ``` ์„œ๋ฒ„ ์‹คํ–‰ ํ›„ `http://localhost:8090` ์ ‘์† diff --git a/images/ERD.png b/images/ERD.png new file mode 100644 index 0000000..9f92909 Binary files /dev/null and b/images/ERD.png differ