Skip to content

ZeroColaa/HotDealAPI

ย 
ย 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

207 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

HotDeal - ์‹ค์‹œ๊ฐ„ ํ•ซ๋”œ ์ด๋ฒคํŠธ ์‹œ์Šคํ…œ


๋ชฉ์ฐจ


ํ”„๋กœ์ ํŠธ ๊ฐœ์š”

HotDeal์€ ์‹ค์‹œ๊ฐ„ ํ• ์ธ ์ด๋ฒคํŠธ ๊ด€๋ฆฌ ๋ฐ ์ฃผ๋ฌธ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ์‹œ์Šคํ…œ์ž…๋‹ˆ๋‹ค.

  • ๋™์‹œ์„ฑ ์ œ์–ด๋ฅผ ์œ„ํ•œ Redisson ๊ธฐ๋ฐ˜ ๋ถ„์‚ฐ ๋ฝ
  • WebSocket์„ ํ™œ์šฉํ•œ ์‹ค์‹œ๊ฐ„ ์ด๋ฒคํŠธ ์•Œ๋ฆผ
  • ๋„๋ฉ”์ธ ๊ฐ„ ๋ถ„๋ฆฌ์™€ ํ–ฅํ›„ ํ™•์žฅ์„ฑ์„ ๊ณ ๋ คํ•ด ๋‚ด๋ถ€ API ํ†ต์‹  ๋ฐฉ์‹ ์ ์šฉ

์•„ํ‚คํ…์ฒ˜ ๋ฐ ์„ค๊ณ„

๋„๋ฉ”์ธ ์ฃผ๋„ ์„ค๊ณ„ (DDD)

์ด ํ”„๋กœ์ ํŠธ๋Š” ๋ณต์žกํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ๋ช…ํ™•ํ•˜๊ฒŒ ๊ตฌ์กฐํ™”ํ•˜๊ธฐ ์œ„ํ•ด **๋„๋ฉ”์ธ ์ฃผ๋„ ์„ค๊ณ„(DDD)**๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ „์ฒด ์•„ํ‚คํ…์ฒ˜๋ฅผ ๊ตฌ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.

ํŠนํžˆ ๊ฐ€์žฅ ๋งŽ์€ ์‹œ๊ฐ„๊ณผ ๊ณ ๋ฏผ์„ ํˆฌ์žํ•œ ๋ถ€๋ถ„์€ ๋„๋ฉ”์ธ ๊ฐ„์˜ ์ฑ…์ž„์„ ๊ตฌ๋ถ„ํ•˜๊ณ  ๊ทธ ๊ฒฝ๊ณ„๋ฅผ ๋ช…ํ™•ํžˆ ์ •์˜ํ•˜๋Š” ๊ฒƒ์ด์—ˆ์Šต๋‹ˆ๋‹ค.


๋„๋ฉ”์ธ ์ฃผ๋„ ์„ค๊ณ„๋ฅผ ์„ ํƒํ•œ ์ด์œ 

์ด๋ฒคํŠธ, ์ƒํ’ˆ, ์ฃผ๋ฌธ๊ณผ ๊ฐ™์€ ๋„๋ฉ”์ธ์€ ๊ฐ๊ฐ ๋…๋ฆฝ์ ์ธ ๋น„์ฆˆ๋‹ˆ์Šค ๊ฐœ๋…์„ ๊ฐ€์ง€๊ณ  ์žˆ์œผ๋ฉฐ ์ด๋“ค์ด ๊ต์ฐจํ•˜๋Š” ์ง€์ ์—์„œ๋Š” ๋ฐ์ดํ„ฐ ์˜์กด์„ฑ๊ณผ ๋ณ€๊ฒฝ ์ „ํŒŒ์˜ ์œ„ํ—˜์ด ํ•ญ์ƒ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.

์ด์— ๋”ฐ๋ผ ์šฐ๋ฆฌ๋Š” ๋„๋ฉ”์ธ ์ฃผ๋„ ์„ค๊ณ„(DDD) ๋ฅผ ์„ ํƒํ•˜์˜€๊ณ  ์ด๋ฅผ ํ†ตํ•ด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํšจ๊ณผ๋ฅผ ์–ป๊ณ ์ž ํ–ˆ์Šต๋‹ˆ๋‹ค.

  1. ๋ถ„๋ฆฌ๋œ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ๊ตฌ์ถ•
  2. ๋„๋ฉ”์ธ ๊ฐ„ ์ฑ…์ž„๊ณผ ์—ญํ•  ๋ช…ํ™•ํ™”
  3. ๋ถˆํ•„์š”ํ•œ ๋ณ€๊ฒฝ ์ „ํŒŒ ์ฐจ๋‹จ
  4. ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™๊ณผ ํ๋ฆ„์ด ๋ฐ˜์˜๋œ ๋ชจ๋ธ๋ง
  5. ํ˜„์‹ค ์—…๋ฌด์™€ ์œ ์‚ฌํ•œ ๊ตฌ์กฐ๋กœ ํ˜‘์—… ํšจ์œจ ํ–ฅ์ƒ

HTTP ํ†ต์‹  ๋ฐฉ์‹

์ด ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ์„œ๋ฒ„ ๊ฐ„ ํ†ต์‹ ์„ ์œ„ํ•ด Spring์˜ RestTemplate์„ ์„ ํƒํ–ˆ์Šต๋‹ˆ๋‹ค.

๋น„๋ก RestTemplate์€ ๋ ˆ๊ฑฐ์‹œ๋กœ ๋ถ„๋ฅ˜๋˜๋ฉฐ WebClient๊ฐ€ ๊ณต์‹์ ์œผ๋กœ ๊ถŒ์žฅ๋˜๋Š” ์ถ”์„ธ์ด์ง€๋งŒ ์ด๋ฒˆ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ด์œ ๋กœ RestTemplate์„ ์šฐ์„  ๋„์ž…์„ ๊ฒฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

RestTemplate์€ ๋™๊ธฐ/๋ธ”๋กœํ‚น ๊ธฐ๋ฐ˜์œผ๋กœ ๋™์ž‘ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ „์ฒด ํ๋ฆ„์„ ์ง๊ด€์ ์œผ๋กœ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ์–ด ๋””๋ฒ„๊น… ๋ฐ ๋ฌธ์ œ ์ถ”์ ์ด ์ƒ๋Œ€์ ์œผ๋กœ ์ˆ˜์›”ํ•ฉ๋‹ˆ๋‹ค.

WebClient๋Š” ๋น„๋™๊ธฐ/๋…ผ๋ธ”๋กœํ‚น ๊ธฐ๋ฐ˜์œผ๋กœ ๊ณ ์„ฑ๋Šฅ ์ฒ˜๋ฆฌ์— ์ ํ•ฉํ•˜์ง€๋งŒ ์ดˆ๊ธฐ ํ•™์Šต ๋น„์šฉ์ด ํฌ๊ณ  ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋‚˜ ์žฅ์•  ์ถ”์ ์— ๋Œ€ํ•œ ์ง„์ž… ์žฅ๋ฒฝ์ด ๋†’์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ”„๋กœ์ ํŠธ ์ดˆ๋ฐ˜์—๋Š” ํ•ต์‹ฌ ๋„๋ฉ”์ธ ์„ค๊ณ„ ๋ฐ ํ๋ฆ„ ํŒŒ์•…์— ์ง‘์ค‘์„ ์œ„ํ•ด ํ†ต์‹  ๋ฐฉ์‹์— ๋Œ€ํ•œ ๋ณต์žก๋„๋Š” ์ตœ์†Œํ™”ํ•˜๊ณ ์ž ํ–ˆ์Šต๋‹ˆ๋‹ค. ํ–ฅํ›„ ํ™•์žฅ ๋˜๋Š” ์„ฑ๋Šฅ ์ตœ์ ํ™”๊ฐ€ ํ•„์š”ํ•œ ๊ตฌ๊ฐ„์—์„œ๋Š” WebClient๋กœ์˜ ์ „ํ™˜์„ ๊ณ ๋ คํ•  ์ˆ˜ ์žˆ๋„๋ก ์œ ์—ฐํ•œ ๊ตฌ์กฐ๋กœ ์„ค๊ณ„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.


์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ ๊ตฌ์กฐ

์ด ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ๋Œ€๋Ÿ‰ ์‚ฌ์šฉ์ž์—๊ฒŒ ์•ˆ์ •์ ์œผ๋กœ ์•Œ๋ฆผ์„ ์ „์†กํ•  ์ˆ˜ ์žˆ๋Š” ๊ตฌ์กฐ๋ฅผ ๋ชฉํ‘œ๋กœ ์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ ์‹œ์Šคํ…œ์„ ์„ค๊ณ„ํ–ˆ์Šต๋‹ˆ๋‹ค.

์ดˆ๊ธฐ์—๋Š” ์„œ๋ฒ„๊ฐ€ ์ œํ’ˆ์„ ๊ตฌ๋… ์ค‘์ธ ๋ชจ๋“  ์‚ฌ์šฉ์ž์—๊ฒŒ ์›น์†Œ์ผ“์„ ํ†ตํ•ด ์ง์ ‘ ์•Œ๋ฆผ์„ ํ‘ธ์‹œํ•˜๋Š” ๋ฐฉ์‹์ด์—ˆ๊ณ  ์ด๋Š” ์†Œ๊ทœ๋ชจ ํŠธ๋ž˜ํ”ฝ ํ™˜๊ฒฝ์—์„œ๋Š” ์ž˜ ๋™์ž‘ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ๊ตฌ๋…์ž๊ฐ€ ๋งŽ์•„์ง€๋Š” ์ƒํ™ฉ์—์„œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฌธ์ œ์ ์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Œ์„ ๊ณ ๋ คํ–ˆ์Šต๋‹ˆ๋‹ค.

  • ์—ฐ๊ฒฐ ํ’€(pool) ํ•œ๊ณ„๋กœ ์ธํ•ด ๋™์‹œ ์—ฐ๊ฒฐ ์ˆ˜ ์ œํ•œ
  • ๋™๊ธฐ ์ฒ˜๋ฆฌ ์ง€์—ฐ์œผ๋กœ ์ธํ•œ ์‘๋‹ต ์†๋„ ์ €ํ•˜
  • ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์ฆ๊ฐ€ ๋ฐ GC ๋ถ€ํ•˜
  • ๊ตฌ๋…์ž ๋ชฉ๋ก ์กฐํšŒ ์‹œ์ ์˜ ์„ฑ๋Šฅ ์ €ํ•˜

์„ค๊ณ„ ๋ฐฉํ–ฅ๊ณผ ๊ตฌ์กฐ์  ๋Œ€์‘

์ด๋Ÿฌํ•œ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ „๋žต์„ ์„ค๊ณ„ํ•˜๊ณ  ๋ฐ˜์˜ํ–ˆ์Šต๋‹ˆ๋‹ค.

DB ์ธก๋ฉด์—์„œ๋Š” ์ธ๋ฑ์‹ฑ๊ณผ ์บ์‹ฑ์„ ํ†ตํ•ด ์กฐํšŒ ์„ฑ๋Šฅ์„ ํ™•๋ณดํ–ˆ๊ณ  ์›น์†Œ์ผ“ ์ฒ˜๋ฆฌ ๋กœ์ง์€ ๋ฐฐ์น˜ ์ „์†ก ๋ฐ ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ๋กœ ๋ถ€๋‹ด์„ ๋ถ„์‚ฐํ–ˆ์Šต๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ์›น์†Œ์ผ“ ์—ฐ๊ฒฐ ์ˆ˜ ์ œํ•œ์€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ฐจ์›์—์„œ ๊ทผ๋ณธ์ ์œผ๋กœ ํ•ด๊ฒฐํ•  ์ˆ˜ ์—†๋Š” ๊ตฌ์กฐ์  ์ œ์•ฝ์ด์—ˆ์Šต๋‹ˆ๋‹ค. ์ด์— ๋”ฐ๋ผ ์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ ๊ตฌ์กฐ๋ฅผ ํด๋ผ์ด์–ธํŠธ ์ฃผ๋„ ๋ฐฉ์‹์œผ๋กœ ์ „ํ™˜ํ•˜๋Š” ๋ฐฉํ–ฅ์œผ๋กœ ์žฌ์„ค๊ณ„ํ–ˆ์Šต๋‹ˆ๋‹ค.


์ตœ์ข… ์„ค๊ณ„ ๋ฐฉํ–ฅ

์ตœ์ข…์ ์œผ๋กœ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฐฉ์‹์œผ๋กœ ์•Œ๋ฆผ ์‹œ์Šคํ…œ์„ ๊ตฌ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.

  • ์„œ๋ฒ„๋Š” โ€œ์ œํ’ˆ์— ์ƒˆ๋กœ์šด ์ด๋ฒคํŠธ๊ฐ€ ๋“ฑ๋ก๋˜์—ˆ์Œ์„ ์•Œ๋ฆฌ๋Š” ์‹ ํ˜ธโ€๋งŒ ์ „์†ก
  • ํด๋ผ์ด์–ธํŠธ๋Š” ํ•ด๋‹น ์‹ ํ˜ธ๋ฅผ ์ˆ˜์‹ ํ•œ ๋’ค ํ•„์š”ํ•œ ์•Œ๋ฆผ ๋ฐ์ดํ„ฐ๋ฅผ ์ง์ ‘ ์กฐํšŒ

์ด๋Ÿฌํ•œ ๊ตฌ์กฐ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์žฅ์ ์„ ๊ฐ€์ง‘๋‹ˆ๋‹ค.

  • ์„œ๋ฒ„๊ฐ€ ๋ชจ๋“  ๊ตฌ๋…์ž์—๊ฒŒ ์ง์ ‘ ๋ฐ์ดํ„ฐ๋ฅผ ์ „์†กํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์›น์†Œ์ผ“ ์—ฐ๊ฒฐ ์ˆ˜ ์ œํ•œ ๋ฌธ์ œ๋ฅผ ํšจ๊ณผ์ ์œผ๋กœ ํšŒํ”ผ
  • ํด๋ผ์ด์–ธํŠธ๊ฐ€ ํ•„์š”ํ•œ ์‹œ์ ์— ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋งŒ ์š”์ฒญํ•˜์—ฌ ๋ฆฌ์†Œ์Šค ํšจ์œจ์„ฑ ์ƒ์Šน
  • ์ „์ฒด ์‹œ์Šคํ…œ์˜ ํ™•์žฅ์„ฑ๊ณผ ์•ˆ์ •์„ฑ ํ–ฅ์ƒ

๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ ์ „๋žต

์•„ํ‚คํ…์ฒ˜ ๊ตฌ์กฐ ๋ฐ ๋ฐ์ดํ„ฐ ํ๋ฆ„

์ด ํ”„๋กœ์ ํŠธ๋Š” ์ธ์ฆ ์ •๋ณด(Auth)์™€ ์‚ฌ์šฉ์ž ํ”„๋กœํ•„(User)์„ ์„œ๋กœ ๋‹ค๋ฅธ ๋ชฉ์ ๊ณผ ์ฑ…์ž„์„ ๊ฐ€์ง„ ํ…Œ์ด๋ธ”๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. Auth๋Š” ์‚ฌ์šฉ์ž ์ธ์ฆ์— ํ•„์š”ํ•œ ์ •๋ณด๋ฅผ ๊ด€๋ฆฌํ•˜๊ณ  User๋Š” ์‚ฌ์šฉ์ž ์กฐํšŒ ์ „์šฉ ๋ฐ์ดํ„ฐ๋ฅผ ์ œ๊ณตํ•˜๋Š” Read Model ์—ญํ• ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

๋‘ ํ…Œ์ด๋ธ”์€ ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜์œผ๋กœ ์—ฐ๋™๋˜๋ฉฐ ์˜ˆ๋ฅผ ๋“ค์–ด ํšŒ์›๊ฐ€์ž… ์‹œ Auth ์ €์žฅ ์ดํ›„ UserRegisteredEvent๊ฐ€ ๋ฐœํ–‰์„ ํ†ตํ•ด ์ด๋ฅผ ๊ตฌ๋…ํ•˜๋Š” ๋ฆฌ์Šค๋„ˆ๊ฐ€ User ๋ฐ์ดํ„ฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.


์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ + ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜ ์„ค๊ณ„

์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ์•„ํ‚คํ…์ฒ˜๋Š” ๋น„๋™๊ธฐ์ ์ด๋ฉฐ ์œ ์—ฐํ•˜์ง€๋งŒ ๋น„์ •์ƒ์ ์ธ ํ๋ฆ„(์˜ˆ: ๋ฆฌ์Šค๋„ˆ ์‹คํŒจ)์œผ๋กœ ์ธํ•ด ์ผ๋ถ€ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ˆ„๋ฝ๋˜๊ฑฐ๋‚˜ ์ผ๊ด€์„ฑ์ด ์–ด๊ธ‹๋‚  ๊ฐ€๋Šฅ์„ฑ๋„ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.

์ด๋ฅผ ๋ณด์™„ํ•˜๊ธฐ ์œ„ํ•ด ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜ ํŒจํ„ด์„ ์•„ํ‚คํ…์ฒ˜์— ํฌํ•จ์‹œ์ผฐ์Šต๋‹ˆ๋‹ค.

User ์ €์žฅ ๋ฆฌ์Šค๋„ˆ์—์„œ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•  ๊ฒฝ์šฐ UserSaveFailedEvent๋ฅผ ๋ฐœํ–‰ํ•ด Auth ๋ฐ์ดํ„ฐ๋ฅผ ๋˜๋Œ๋ฆฌ๋Š” ๋ณด์ƒ ๋กœ์ง์„ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.


๋ฐ์ดํ„ฐ ํ๋ฆ„๋„

image

ERD

image

์ฃผ์š” ๊ธฐ๋Šฅ

1. ์ด๋ฒคํŠธ ๊ด€๋ฆฌ

  • ํ• ์ธ ์ด๋ฒคํŠธ ์ƒ์„ฑ ๋ฐ ๊ด€๋ฆฌ
  • ์ƒํ’ˆ๋ณ„ ์ตœ์  ํ• ์ธ์œจ ์ž๋™ ๊ณ„์‚ฐ
  • ๋งŒ๋ฃŒ๋œ ์ด๋ฒคํŠธ ์ž๋™ ์‚ญ์ œ (์Šค์ผ€์ค„๋Ÿฌ)

2. ์ฃผ๋ฌธ ์ฒ˜๋ฆฌ

  • ๋‹ค์ค‘ ์ƒํ’ˆ ์ฃผ๋ฌธ ์ง€์›
  • ์ด๋ฒคํŠธ ํ• ์ธ๊ฐ€ ์ž๋™ ์ ์šฉ
  • ์ฃผ๋ฌธ ์ƒํƒœ ๊ด€๋ฆฌ (ORDER_BEFORE, ORDER_PENDING, ORDER_SUCCESS, ORDER_FAILURE)

3. ์žฌ๊ณ  ๊ด€๋ฆฌ

  • Redisson์„ ํ™œ์šฉํ•œ ๋ถ„์‚ฐ ๋ฝ ๊ตฌํ˜„
  • ๋™์‹œ์„ฑ ์ œ์–ด๋ฅผ ํ†ตํ•œ ์žฌ๊ณ  ์ฐจ๊ฐ
  • ์žฌ๊ณ  ๋ถ€์กฑ ์‹œ ์ž๋™ ์ฃผ๋ฌธ ์‹คํŒจ ์ฒ˜๋ฆฌ

4. ์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ

  • WebSocket์„ ํ†ตํ•œ ์‹ค์‹œ๊ฐ„ ์ด๋ฒคํŠธ ์•Œ๋ฆผ
  • Spring Event๋ฅผ ํ™œ์šฉํ•œ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ

๊ธฐ์ˆ  ์Šคํƒ

Backend

  • Java 17
  • Spring Boot 3.5.3
  • Spring Data JPA
  • Spring Security
  • Spring WebSocket

Database

  • MySQL
  • Redis

๊ธฐํƒ€

  • Redisson
  • JWT
  • Lombok
  • TestContainers

ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ

ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ
com.example.hotdeal
โ”œโ”€โ”€ domain
โ”‚   โ”œโ”€โ”€ common
โ”‚   โ”‚   โ”œโ”€โ”€ client           # ๋‚ด๋ถ€ API ํด๋ผ์ด์–ธํŠธ
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ event
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ product
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ stock
โ”‚   โ”‚   โ””โ”€โ”€ springEvent      # Spring Event ์ •์˜
โ”‚   โ”œโ”€โ”€ event               # ์ด๋ฒคํŠธ ๋„๋ฉ”์ธ
โ”‚   โ”‚   โ”œโ”€โ”€ api
โ”‚   โ”‚   โ”œโ”€โ”€ application
โ”‚   โ”‚   โ”œโ”€โ”€ domain
โ”‚   โ”‚   โ””โ”€โ”€ infra
โ”‚   โ”œโ”€โ”€ order               # ์ฃผ๋ฌธ ๋„๋ฉ”์ธ
โ”‚   โ”‚   โ”œโ”€โ”€ api
โ”‚   โ”‚   โ”œโ”€โ”€ application
โ”‚   โ”‚   โ”œโ”€โ”€ domain
โ”‚   โ”‚   โ””โ”€โ”€ infra
โ”‚   โ”œโ”€โ”€ stock               # ์žฌ๊ณ  ๋„๋ฉ”์ธ
โ”‚   โ”‚   โ”œโ”€โ”€ api
โ”‚   โ”‚   โ”œโ”€โ”€ application
โ”‚   โ”‚   โ”œโ”€โ”€ domain
โ”‚   โ”‚   โ””โ”€โ”€ infra
โ”‚   โ”œโ”€โ”€ notification        # ์•Œ๋ฆผ ๋„๋ฉ”์ธ
โ”‚   โ”‚   โ”œโ”€โ”€ application
โ”‚   โ”‚   โ”œโ”€โ”€ domain
โ”‚   โ”‚   โ””โ”€โ”€ infra
โ”‚   โ””โ”€โ”€ user               # ์‚ฌ์šฉ์ž ๋„๋ฉ”์ธ
โ”‚       โ””โ”€โ”€ auth
โ””โ”€โ”€ global
    โ”œโ”€โ”€ config              # ์ „์—ญ ์„ค์ •
    โ”œโ”€โ”€ enums              # ๊ณตํ†ต ์—ด๊ฑฐํ˜•
    โ”œโ”€โ”€ exception          # ์˜ˆ์™ธ ์ฒ˜๋ฆฌ
    โ”œโ”€โ”€ lock               # ๋ถ„์‚ฐ ๋ฝ ๊ตฌํ˜„
    โ””โ”€โ”€ model              # ๊ณตํ†ต ๋ชจ๋ธ

API ๋ช…์„ธ

์ „์ฒด API ๊ฐœ์š”

์ธ์ฆ (Auth) API

API Method Endpoint ๊ถŒํ•œ ์„ค๋ช…
ํšŒ์›๊ฐ€์ž… POST /api/auth/signup PUBLIC ์ƒˆ๋กœ์šด ์‚ฌ์šฉ์ž ํšŒ์›๊ฐ€์ž…
๋กœ๊ทธ์ธ POST /api/auth/login PUBLIC ์‚ฌ์šฉ์ž ๋กœ๊ทธ์ธ ๋ฐ ํ† ํฐ ๋ฐœ๊ธ‰
ํ† ํฐ ์žฌ๋ฐœ๊ธ‰ POST /api/auth/reissue PUBLIC Access Token ์žฌ๋ฐœ๊ธ‰
๋กœ๊ทธ์•„์›ƒ POST /api/auth/logout USER ์‚ฌ์šฉ์ž ๋กœ๊ทธ์•„์›ƒ
ํšŒ์›ํƒˆํ‡ด POST /api/auth/withdraw USER ์‚ฌ์šฉ์ž ๊ณ„์ • ๋น„ํ™œ์„ฑํ™”
๊ณ„์ • ๋ณต๊ตฌ POST /api/auth/{authId}/restore ADMIN ํƒˆํ‡ดํ•œ ๊ณ„์ • ๋ณต๊ตฌ

์‚ฌ์šฉ์ž (User) API

API Method Endpoint ๊ถŒํ•œ ์„ค๋ช…
๋‚ด ์ •๋ณด ์กฐํšŒ GET /api/users/me USER ํ˜„์žฌ ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ

์ƒํ’ˆ (Product) API

API Method Endpoint ๊ถŒํ•œ ์„ค๋ช…
์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ POST /api/products/search-product USER ์—ฌ๋Ÿฌ ์ƒํ’ˆ ์ •๋ณด๋ฅผ ํ•œ ๋ฒˆ์— ์กฐํšŒ
๋‹จ์ผ ์ƒํ’ˆ ์กฐํšŒ GET /api/products/{productId} USER ํŠน์ • ์ƒํ’ˆ์˜ ์ƒ์„ธ ์ •๋ณด ์กฐํšŒ
์ƒํ’ˆ ์ƒ์„ฑ POST /api/products ADMIN ์ƒˆ๋กœ์šด ์ƒํ’ˆ ๋“ฑ๋ก
์ƒํ’ˆ ์ˆ˜์ • PUT /api/products/{productId} ADMIN ๊ธฐ์กด ์ƒํ’ˆ ์ •๋ณด ์ˆ˜์ •
์ƒํ’ˆ ์‚ญ์ œ DELETE /api/products/{productId} ADMIN ์ƒํ’ˆ ์†Œํ”„ํŠธ ์‚ญ์ œ

์žฌ๊ณ  (Stock) API

API Method Endpoint ๊ถŒํ•œ ์„ค๋ช…
์žฌ๊ณ  ๋ชฉ๋ก ์กฐํšŒ POST /api/stocks/search USER ์—ฌ๋Ÿฌ ์ƒํ’ˆ์˜ ์žฌ๊ณ  ์กฐํšŒ
๋‹จ์ผ ์žฌ๊ณ  ์กฐํšŒ GET /api/stocks/product/{productId} USER ํŠน์ • ์ƒํ’ˆ์˜ ์žฌ๊ณ  ์กฐํšŒ
์žฌ๊ณ  ์ฆ๊ฐ€ POST /api/stocks/product/{productId}/increase ADMIN ์ƒํ’ˆ ์žฌ๊ณ  ์ฆ๊ฐ€
์žฌ๊ณ  ์ดˆ๊ธฐํ™” POST /api/stocks/product/{productId}/reset ADMIN ์ƒํ’ˆ ์žฌ๊ณ  ์ดˆ๊ธฐํ™”

์ด๋ฒคํŠธ (Event) API

API Method Endpoint ๊ถŒํ•œ ์„ค๋ช…
์ด๋ฒคํŠธ ์ƒ์„ฑ POST /api/event/create ADMIN ์ƒˆ๋กœ์šด ํ•ซ๋”œ ์ด๋ฒคํŠธ ์ƒ์„ฑ
์ด๋ฒคํŠธ ์กฐํšŒ POST /api/event/search-event USER ์ƒํ’ˆ๋ณ„ ์ด๋ฒคํŠธ ์ •๋ณด ์กฐํšŒ

์ฃผ๋ฌธ (Order) API

API Method Endpoint ๊ถŒํ•œ ์„ค๋ช…
๋‹ค์ค‘ ์ƒํ’ˆ ์ฃผ๋ฌธ POST /api/orders/products USER ๋‹ค์ค‘ ์ƒํ’ˆ ์ฃผ๋ฌธ (RestTemplate ๋ฐฉ์‹)
์ฃผ๋ฌธ ์ทจ์†Œ PUT /api/orders/{orderId} USER ๊ธฐ์กด ์ฃผ๋ฌธ ์ทจ์†Œ
์ฃผ๋ฌธ ์กฐํšŒ GET /api/orders/{orderId} USER ์ฃผ๋ฌธ ์ƒ์„ธ ์ •๋ณด ์กฐํšŒ

๊ตฌ๋… (Subscribe) API

API Method Endpoint ๊ถŒํ•œ ์„ค๋ช…
์ƒํ’ˆ ๊ตฌ๋… POST /api/subscribe/sub-product USER ์ƒํ’ˆ ์•Œ๋ฆผ ๊ตฌ๋… ๋“ฑ๋ก
๊ตฌ๋…์ž ์กฐํšŒ GET /api/subscribe/search-sub-user USER ํŠน์ • ์ƒํ’ˆ ๊ตฌ๋…์ž ๋ชฉ๋ก ์กฐํšŒ
๊ตฌ๋… ์ทจ์†Œ DELETE /api/subscribe/cancel-sub USER ์ƒํ’ˆ ๊ตฌ๋… ์ทจ์†Œ
API ์ƒ์„ธ ์ •๋ณด

์ธ์ฆ (Auth) API

1. ํšŒ์›๊ฐ€์ž…

POST /api/auth/signup
Request Body
{
  "email": "user@example.com",
  "name": "ํ™๊ธธ๋™",
  "password": "password123"
}

2. ๋กœ๊ทธ์ธ

POST /api/auth/login
Request Body
{
  "email": "user@example.com",
  "password": "password123"
}

3. ํ† ํฐ ์žฌ๋ฐœ๊ธ‰

POST /api/auth/reissue
Request Body
{
  "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

4. ๋กœ๊ทธ์•„์›ƒ

POST /api/auth/logout
Authorization: Bearer {token}
Request Body
{
  "password": "password123"
}

5. ํšŒ์›ํƒˆํ‡ด

POST /api/auth/withdraw
Authorization: Bearer {token}
Request Body
{
  "password": "password123"
}

6. ๊ณ„์ • ๋ณต๊ตฌ

POST /api/auth/{authId}/restore
Authorization: Bearer {token} (ADMIN ๊ถŒํ•œ ํ•„์š”)
Query Parameter
  • authId: ๋ณต๊ตฌํ•  ์‚ฌ์šฉ์ž ID

์‚ฌ์šฉ์ž (User) API

1. ๋‚ด ์ •๋ณด ์กฐํšŒ

GET /api/users/me
Authorization: Bearer {token}

์ƒํ’ˆ (Product) API

1. ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ

POST /api/products/search-product
Authorization: Bearer {token}
Request Body
{
  "productIds": [1, 2, 3, 4, 5]
}

2. ๋‹จ์ผ ์ƒํ’ˆ ์กฐํšŒ

GET /api/products/{productId}
Authorization: Bearer {token}

3. ์ƒํ’ˆ ์ƒ์„ฑ

POST /api/products
Authorization: Bearer {token} (ADMIN ๊ถŒํ•œ ํ•„์š”)
Request Body
{
  "productName": "Galaxy S24 Ultra",
  "productDescription": "์‚ผ์„ฑ ์ตœ์‹  ํ”Œ๋ž˜๊ทธ์‹ญ ์Šค๋งˆํŠธํฐ",
  "productPrice": 1600000,
  "productImageUrl": "https://example.com/galaxy-s24.jpg",
  "productCategory": "ELECTRONICS"
}

4. ์ƒํ’ˆ ์ˆ˜์ •

PUT /api/products/{productId}
Authorization: Bearer {token} (ADMIN ๊ถŒํ•œ ํ•„์š”)
Request Body
{
  "productName": "iPhone 15 Pro Max ์—…๋ฐ์ดํŠธ",
  "productDescription": "์—…๋ฐ์ดํŠธ๋œ ์ƒํ’ˆ ์„ค๋ช…",
  "productPrice": 1700000,
  "productImageUrl": "https://example.com/updated-iphone15.jpg",
  "productCategory": "ELECTRONICS"
}

5. ์ƒํ’ˆ ์‚ญ์ œ

DELETE /api/products/{productId}
Authorization: Bearer {token} (ADMIN ๊ถŒํ•œ ํ•„์š”)

์žฌ๊ณ  (Stock) API

1. ์žฌ๊ณ  ๋ชฉ๋ก ์กฐํšŒ

POST /api/stocks/search
Authorization: Bearer {token}
Request Body
{
  "productIds": [1, 2, 3, 4, 5]
}

2. ๋‹จ์ผ ์žฌ๊ณ  ์กฐํšŒ

GET /api/stocks/product/{productId}
Authorization: Bearer {token}

3. ์žฌ๊ณ  ์ฆ๊ฐ€

POST /api/stocks/product/{productId}/increase?quantity={์ˆ˜๋Ÿ‰}
Authorization: Bearer {token} (ADMIN ๊ถŒํ•œ ํ•„์š”)

4. ์žฌ๊ณ  ์ดˆ๊ธฐํ™”

POST /api/stocks/product/{productId}/reset?quantity={์ˆ˜๋Ÿ‰}
Authorization: Bearer {token} (ADMIN ๊ถŒํ•œ ํ•„์š”)

์ด๋ฒคํŠธ (Event) API

1. ์ด๋ฒคํŠธ ์ƒ์„ฑ

POST /api/event/create
Authorization: Bearer {token} (ADMIN ๊ถŒํ•œ ํ•„์š”)
Request Body
{
  "eventType": "HOT_DEAL",
  "eventDiscount": 20,
  "eventDuration": 7,
  "startEventTime": "2025-07-15T00:00:00",
  "productIds": [1, 2, 3, 4, 5]
}

2. ์ด๋ฒคํŠธ ์กฐํšŒ

POST /api/event/search-event
Authorization: Bearer {token}
Request Body
{
  "productIds": [1, 2, 3, 4, 5]
}

์ฃผ๋ฌธ (Order) API

1. ๋‹จ์ผ ์ƒํ’ˆ ์ฃผ๋ฌธ

POST /api/orders/v1
Authorization: Bearer {token}
Request Body
{
  "productId": 1,
  "quantity": 2
}

2. ๋‹ค์ค‘ ์ƒํ’ˆ ์ฃผ๋ฌธ

POST /api/orders/v2
Authorization: Bearer {token}
Request Body
{
  "orderItems": [
    {
      "productId": 1,
      "quantity": 2
    },
    {
      "productId": 2,
      "quantity": 1
    }
  ]
}

3. ์ฃผ๋ฌธ ์ทจ์†Œ

PUT /api/orders/{orderId}
Authorization: Bearer {token}

4. ์ฃผ๋ฌธ ์กฐํšŒ

GET /api/orders/{orderId}
Authorization: Bearer {token}

๊ตฌ๋… (Subscribe) API

1. ์ƒํ’ˆ ๊ตฌ๋…

POST /api/subscribe/sub-product
Authorization: Bearer {token}
Request Body
{
  "productIds": [1, 2, 3, 4, 5]
}

2. ๊ตฌ๋…์ž ์กฐํšŒ

GET /api/subscribe/search-sub-user?productId={์ƒํ’ˆID}
Authorization: Bearer {token}

3. ๊ตฌ๋… ์ทจ์†Œ

DELETE /api/subscribe/cancel-sub?userId={์‚ฌ์šฉ์žID}&productId={์ƒํ’ˆID}
Authorization: Bearer {token}
์ถ”๊ฐ€ ์ •๋ณด

์ƒํ’ˆ ์นดํ…Œ๊ณ ๋ฆฌ

์ฝ”๋“œ ํ•œ๊ธ€๋ช… ์ฝ”๋“œ ํ•œ๊ธ€๋ช…
ELECTRONICS ์ „์ž์ œํ’ˆ HEALTH ๊ฑด๊ฐ•/์˜๋ฃŒ
FASHION ํŒจ์…˜/์˜๋ฅ˜ BABY ์œก์•„/์ถœ์‚ฐ
BEAUTY ๋ทฐํ‹ฐ/ํ™”์žฅํ’ˆ PET ๋ฐ˜๋ ค๋™๋ฌผ
HOME_LIVING ํ™ˆ/๋ฆฌ๋น™ CAR ์ž๋™์ฐจ/์šฉํ’ˆ
FOOD ์‹ํ’ˆ HOBBY ์ทจ๋ฏธ/์ˆ˜์ง‘
SPORTS ์Šคํฌ์ธ /๋ ˆ์ € OFFICE ์‚ฌ๋ฌด/๋ฌธ๊ตฌ
BOOKS ๋„์„œ OTHER ๊ธฐํƒ€

์‹คํ–‰ ๋ฐฉ๋ฒ•

์‹คํ–‰ ๋ฐฉ๋ฒ•

1. ํ•„์ˆ˜ ์š”๊ตฌ์‚ฌํ•ญ

  • JDK 17 ์ด์ƒ
  • MySQL 8.0
  • Redis 6.0 ์ด์ƒ
  • Gradle 7.x ์ด์ƒ

2. ํ™˜๊ฒฝ ์ค€๋น„

๋ฐฉ๋ฒ• 1: ๋กœ์ปฌ ์„ค์น˜

# MySQL๊ณผ Redis๋ฅผ ๋กœ์ปฌ์— ์ง์ ‘ ์„ค์น˜ํ•˜์—ฌ ์‚ฌ์šฉ
# MySQL: 3306 ํฌํŠธ
# Redis: 6379 ํฌํŠธ

๋ฐฉ๋ฒ• 2: Docker ์‚ฌ์šฉ (๊ถŒ์žฅ)

# 1. ํ”„๋กœ์ ํŠธ ํด๋ก 
git clone https://github.com/your-repo/hotdeal.git
cd hotdeal

# 2. .env ํŒŒ์ผ ์ƒ์„ฑ
cp .env.example .env
# .env ํŒŒ์ผ์„ ์—ด์–ด ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๊ฐ’ ์„ค์ •

# 3. Docker Compose๋กœ ์ธํ”„๋ผ ์‹คํ–‰
docker-compose up -d

3. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹คํ–‰

# 1. ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ • (ํ„ฐ๋ฏธ๋„์—์„œ ์‹คํ–‰ํ•˜๋Š” ๊ฒฝ์šฐ)
export DB_SCHEME=hotdeal
export DB_USERNAME=root
export DB_PASSWORD=your_password
export SECRET_KEY=your_secret_key_at_least_256_bits_long

# 2. Gradle ๋นŒ๋“œ
./gradlew clean build

# 3. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹คํ–‰
./gradlew bootRun

# ๋˜๋Š” JAR ํŒŒ์ผ๋กœ ์‹คํ–‰
java -jar build/libs/hotdeal-0.0.1-SNAPSHOT.jar

# IDE(IntelliJ IDEA ๋“ฑ)์—์„œ ์‹คํ–‰ํ•˜๋Š” ๊ฒฝ์šฐ
# Run Configuration์—์„œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ • ํ›„ ์‹คํ–‰

4. ์‹คํ–‰ ์ˆœ์„œ

  1. ์ธํ”„๋ผ ํ™˜๊ฒฝ ์ค€๋น„

    • MySQL ์„œ๋ฒ„ ์‹œ์ž‘ (3306 ํฌํŠธ)
    • Redis ์„œ๋ฒ„ ์‹œ์ž‘ (6379 ํฌํŠธ)
  2. ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ •

    • .env ํŒŒ์ผ ์ƒ์„ฑ ๋ฐ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ •
    • ๋˜๋Š” IDE/ํ„ฐ๋ฏธ๋„์—์„œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ export
  3. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹คํ–‰

    ./gradlew bootRun
  4. ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ ์„ค์ •

    • ์ƒํ’ˆ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” (ProductDataInsertTest ํ™œ์šฉ ๊ฐ€๋Šฅ)
    • ์žฌ๊ณ  ๋ฐ์ดํ„ฐ ์„ค์ • (๊ฐ ์ƒํ’ˆ๋ณ„ ์žฌ๊ณ  API ํ˜ธ์ถœ)
  5. ์„œ๋น„์Šค ์ด์šฉ

    • ์ด๋ฒคํŠธ ์ƒ์„ฑ (๊ด€๋ฆฌ์ž)
    • WebSocket ์—ฐ๊ฒฐ (์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ ์ˆ˜์‹ ์šฉ)
    • ์ฃผ๋ฌธ ์ฒ˜๋ฆฌ

ํ™˜๊ฒฝ ์„ค์ •

ํ™˜๊ฒฝ ์„ค์ •

.env ํŒŒ์ผ (ํ™˜๊ฒฝ ๋ณ€์ˆ˜)

.env ํŒŒ์ผ ์˜ˆ์‹œ ๋ณด๊ธฐ
# Database
DB_SCHEME=hotdeal
DB_USERNAME=root
DB_PASSWORD=your_password

# JWT
SECRET_KEY=your_secret_key_at_least_256_bits_long

# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=

application.yml

application.yml ์ „์ฒด ์„ค์ • ๋ณด๊ธฐ
spring:
  application:
    name: HotDeal

  datasource:
    url: jdbc:mysql://localhost:3306/${DB_SCHEME}
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
      maximum-pool-size: 500
      minimum-idle: 50
      connection-timeout: 60000
      idle-timeout: 600000
      max-lifetime: 1800000
      leak-detection-threshold: 60000

  jpa:
    hibernate:
      ddl-auto: create-drop  # ์šด์˜ํ™˜๊ฒฝ์—์„œ๋Š” validate ๋˜๋Š” none ์‚ฌ์šฉ
    properties:
      hibernate:
        show_sql: true
        format_sql: true
        use_sql_comments: true
        dialect: org.hibernate.dialect.MySQLDialect
    open-in-view: false

  data:
    redis:
      host: ${REDIS_HOST:localhost}
      port: ${REDIS_PORT:6379}

jwt:
  secret:
    key: ${SECRET_KEY}

redis:
  host: ${REDIS_HOST:localhost}
  port: ${REDIS_PORT:6379}
  password: ${REDIS_PASSWORD:}

redisson:
  address: redis://${REDIS_HOST:localhost}:${REDIS_PORT:6379}
  password: ${REDIS_PASSWORD:}

server:
  port: 8080

application-test.yml

application-test.yml ์ „์ฒด ์„ค์ • ๋ณด๊ธฐ
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/test_db
    username: root
    password: 3030
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
      maximum-pool-size: 30
      minimum-idle: 10

  data:
    redis:
      host: localhost
      port: 6379

  jpa:
    hibernate:
      ddl-auto: create-drop
    open-in-view: false

jwt:
  secret:
    key: dGVzdFNlY3JldEtleUZvclRlc3RpbmdQdXJwb3NlT25seTEyMzQ1Njc4OTAxMjM0NTY3ODkw

docker-compose.yml (์„ ํƒ์‚ฌํ•ญ)

Docker๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ธํ”„๋ผ๋ฅผ ๊ตฌ์„ฑํ•˜๋Š” ๊ฒฝ์šฐ
version: '3.8'
services:
  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
      MYSQL_DATABASE: ${DB_SCHEME}
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
      
  redis:
    image: redis:6.2-alpine
    ports:
      - "6379:6379"
    command: redis-server --requirepass ${REDIS_PASSWORD}
    volumes:
      - redis_data:/data

volumes:
  mysql_data:
  redis_data:

๊ถŒํ•œ ๋ ˆ๋ฒจ

  • PUBLIC: ์ธ์ฆ ์—†์ด ์ ‘๊ทผ ๊ฐ€๋Šฅ
  • USER: ์ผ๋ฐ˜ ์‚ฌ์šฉ์ž ๊ถŒํ•œ ํ•„์š”
  • ADMIN: ๊ด€๋ฆฌ์ž ๊ถŒํ•œ ํ•„์š”

ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ

  • ํ…Œ์ŠคํŠธ ์‹คํ–‰ ์‹œ TestContainers๊ฐ€ ์ž๋™์œผ๋กœ Redis ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์ƒ์„ฑ
  • ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ์—์„œ๋Š” ์‹ค์ œ MySQL๊ณผ TestContainers Redis๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉ
  • ํ…Œ์ŠคํŠธ ํ”„๋กœํŒŒ์ผ(@ActiveProfiles("test"))๋กœ ๋ณ„๋„ ์„ค์ • ์ ์šฉ

์ฃผ์š” ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง

1. ๋™์‹œ์„ฑ ์ œ์–ด (์žฌ๊ณ  ๊ด€๋ฆฌ)

  • Redisson ๋ถ„์‚ฐ ๋ฝ์„ ์‚ฌ์šฉํ•œ ์•ˆ์ „ํ•œ ์žฌ๊ณ  ์ฐจ๊ฐ
    • ๋ฝ ํš๋“ ์ตœ๋Œ€ ๋Œ€๊ธฐ์‹œ๊ฐ„: 4์ดˆ
    • ๋ฝ ์œ ์ง€ ์‹œ๊ฐ„: 100ms
    • ๋™์‹œ ์š”์ฒญ ์‹œ์—๋„ ์ •ํ™•ํ•œ ์žฌ๊ณ  ๊ด€๋ฆฌ ๋ณด์žฅ
  • Redis ๊ธฐ๋ฐ˜ ๋ถ„์‚ฐ ํ™˜๊ฒฝ ์ง€์›

2. ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ํ๋ฆ„

  1. ์ด๋ฒคํŠธ ์ƒ์„ฑ ์‹œ WebSocket์œผ๋กœ ์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ ๋ฐœ์†ก
  2. Spring Event๋ฅผ ํ†ตํ•œ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ
  3. ์Šค์ผ€์ค„๋Ÿฌ๋ฅผ ํ†ตํ•œ ๋งŒ๋ฃŒ ์ด๋ฒคํŠธ ์ž๋™ ์‚ญ์ œ (๋งค์ผ ์ž์ •)
  4. ์ƒํ’ˆ๋ณ„ ์ตœ์  ํ• ์ธ๊ฐ€ ์ž๋™ ๊ณ„์‚ฐ (๋™์ผ ์ƒํ’ˆ ๋‹ค์ค‘ ์ด๋ฒคํŠธ ์‹œ)

3. ์ฃผ๋ฌธ ์ฒ˜๋ฆฌ ํ๋ฆ„

  1. ์ƒํ’ˆ ์ •๋ณด ์กฐํšŒ (ProductApiClient)
  2. ์ด๋ฒคํŠธ ํ• ์ธ๊ฐ€ ์กฐํšŒ (HotDealApiClient)
  3. ์ฃผ๋ฌธ ์ƒ์„ฑ ๋ฐ ์ €์žฅ
  4. OrderCreatedEvent ๋ฐœํ–‰
  5. ๋น„๋™๊ธฐ๋กœ ์žฌ๊ณ  ์ฐจ๊ฐ ์ฒ˜๋ฆฌ (StockEventListener)
  6. ์žฌ๊ณ  ๋ถ€์กฑ ์‹œ ์ž๋™ ๋กค๋ฐฑ

4. ์ธ์ฆ ๋ฐ ๋ณด์•ˆ

  • JWT ํ† ํฐ ๊ธฐ๋ฐ˜ ์ธ์ฆ
  • Redis๋ฅผ ํ™œ์šฉํ•œ ํ† ํฐ ๋ธ”๋ž™๋ฆฌ์ŠคํŠธ ๊ด€๋ฆฌ
  • Spring Security ์ ์šฉ

5. ์—๋Ÿฌ ์ฒ˜๋ฆฌ

  • CustomException์„ ํ†ตํ•œ ์ผ๊ด€๋œ ์—๋Ÿฌ ์ฒ˜๋ฆฌ
  • ์žฌ๊ณ  ๋ถ€์กฑ, ์ƒํ’ˆ ์—†์Œ ๋“ฑ ๋น„์ฆˆ๋‹ˆ์Šค ์˜ˆ์™ธ ์ฒ˜๋ฆฌ
  • ํŠธ๋žœ์žญ์…˜ ๋กค๋ฐฑ ๋ฐ ๋ณด์ƒ ์ฒ˜๋ฆฌ
  • Spring Event๋ฅผ ํ™œ์šฉํ•œ ์‹คํŒจ ์‹œ ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜

ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…

๋ชฉ์ฐจ

  1. ๋„๋ฉ”์ธ ๊ฐ„ ๋ฐ์ดํ„ฐ ์ฐธ์กฐ ๋ฐฉ์‹ ์„ค๊ณ„
  2. ๋‚ด๋ถ€ API ํ˜ธ์ถœ ์‹œ ์ธ์ฆ ํ† ํฐ ์ „๋‹ฌ ๋ฌธ์ œ
  3. HTTP ํด๋ผ์ด์–ธํŠธ ์„ ํƒ
  4. Auth์™€ User ๊ฐ„์˜ ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ ๋ฌธ์ œ
  5. ์›น์†Œ์ผ“ ์•Œ๋ฆผ ์‹œ์Šคํ…œ ์ตœ์ ํ™”
  6. ํ•ซ๋”œ ์ด๋ฒคํŠธ ๋„๋ฉ”์ธ ์„ฑ๋Šฅ ์ตœ์ ํ™”

1. ๋„๋ฉ”์ธ ๊ฐ„ ๋ฐ์ดํ„ฐ ์ฐธ์กฐ ๋ฐฉ์‹ ์„ค๊ณ„

๋ฌธ์ œ ์ƒํ™ฉ

๋„๋ฉ”์ธ ๊ฐ„์˜ ๊ฒฝ๊ณ„๋ฅผ ๋ช…ํ™•ํžˆ ํ•˜๋Š” ๊ณผ์ •์—์„œ ๋ฐœ์ƒํ•œ ํ•ต์‹ฌ ๋”œ๋ ˆ๋งˆ
"์ฃผ๋ฌธ ์ •๋ณด๋ฅผ ์–ด๋–ป๊ฒŒ ๊ตฌ์„ฑํ•  ๊ฒƒ์ธ๊ฐ€?"
๊ณ ๋ คํ•œ ๋ฐฉ์•ˆ๋“ค
๋ฐฉ์‹ ์„ค๋ช… ์žฅ์  ๋‹จ์ 
๋ฐฉ์‹ 1 ์ด๋ฒคํŠธ ์•„์ดํ…œ ํ…Œ์ด๋ธ”์—์„œ ๋ชจ๋“  ๋ฐ์ดํ„ฐ ์กฐํšŒ ๋‹จ์ˆœํ•œ ์กฐํšŒ ๋ฐ์ดํ„ฐ ์ค‘๋ณต, ์ผ๊ด€์„ฑ ๋ฌธ์ œ
๋ฐฉ์‹ 2 ๋„๋ฉ”์ธ๋ณ„ ์ฑ…์ž„ ๋ถ„๋ฆฌ (์ƒํ’ˆโ†”์ด๋ฒคํŠธ) ์ผ๊ด€์„ฑ ๋ณด์žฅ, ๋„๋ฉ”์ธ ๋…๋ฆฝ์„ฑ ๋ณต์žกํ•œ ๊ตฌํ˜„

์„ ํƒํ•œ ํ•ด๊ฒฐ ๋ฐฉ์•ˆ

๋ฐฉ์‹ 2: ๋„๋ฉ”์ธ๋ณ„ ์ฑ…์ž„ ๋ถ„๋ฆฌ

์ฃผ๋ฌธ ๋„๋ฉ”์ธ โ†’ ์ƒํ’ˆ ๋„๋ฉ”์ธ (๊ธฐ๋ณธ ์ •๋ณด)
           โ†’ ์ด๋ฒคํŠธ ๋„๋ฉ”์ธ (ํ• ์ธ ์ •๋ณด)

์„ ํƒ ์ด์œ 

  • ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ ๋ณด์žฅ: ์ƒํ’ˆ ์ •๋ณด ๋ณ€๊ฒฝ ์‹œ ๋ฐ์ดํ„ฐ ๋ถˆ์ผ์น˜ ๋ฐฉ์ง€
  • ๋„๋ฉ”์ธ ๋…๋ฆฝ์„ฑ: ๊ฐ ๋„๋ฉ”์ธ์˜ ์ฑ…์ž„ ๋ฒ”์œ„ ๋ช…ํ™•ํ™” ๋ฐ ์˜์กด์„ฑ ์ตœ์†Œํ™”

2. ๋‚ด๋ถ€ API ํ˜ธ์ถœ ์‹œ ์ธ์ฆ ํ† ํฐ ์ „๋‹ฌ ๋ฌธ์ œ

๋ฌธ์ œ ์ƒํ™ฉ

๊ด€๋ฆฌ์ž ์žฌ๊ณ  ์ฆ๊ฐ€ ์š”์ฒญ โ†’ ๋‚ด๋ถ€ ์ƒํ’ˆ ๊ฒ€์ฆ API ํ˜ธ์ถœ โ†’ 401 ์ธ์ฆ ์˜ค๋ฅ˜ ๋ฐœ์ƒ

์›์ธ ๋ถ„์„

์™ธ๋ถ€ ์š”์ฒญ: ์ธ์ฆ๋จ โœ“
๋‚ด๋ถ€ API ํ˜ธ์ถœ: ํ† ํฐ ์ •๋ณด ์—†์Œ โœ—

ํ•ด๊ฒฐ ๋ฐฉ์•ˆ: RestTemplate ์ธํ„ฐ์…‰ํ„ฐ

๊ตฌํ˜„ ์ฝ”๋“œ
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
    return builder
            .additionalInterceptors((request, body, execution) -> {
                // ํ˜„์žฌ ์š”์ฒญ์˜ Authorization ํ—ค๋”๋ฅผ ๋ณต์‚ฌ
                RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
                if (attrs instanceof ServletRequestAttributes) {
                    HttpServletRequest httpRequest = ((ServletRequestAttributes) attrs).getRequest();
                    String authHeader = httpRequest.getHeader("Authorization");
                    if (authHeader != null) {
                        request.getHeaders().set("Authorization", authHeader);
                        log.debug("JWT ํ† ํฐ ์ „๋‹ฌ - URL: {}", request.getURI());
                    }
                }
                return execution.execute(request, body);
            })
            .build();
}

๊ฐœ์„  ํšจ๊ณผ

  • ๋‚ด๋ถ€ API ํ˜ธ์ถœ ์‹œ ์ž๋™ ํ† ํฐ ์ „๋‹ฌ
  • ์ธ์ฆ ๊ด€๋ จ ์˜ค๋ฅ˜ ์™„์ „ ํ•ด๊ฒฐ
  • ์ถ”๊ฐ€ ์„ค์ • ์—†์ด ๋ชจ๋“  RestTemplate ์š”์ฒญ์— ์ ์šฉ

3. HTTP ํด๋ผ์ด์–ธํŠธ ์„ ํƒ (RestTemplate vs WebClient)

๊ธฐ์ˆ  ์„ ํƒ ๊ณ ๋ฏผ

๊ธฐ์ˆ  ์žฅ์  ๋‹จ์ 
WebClient ๋น„๋™๊ธฐ/๋…ผ๋ธ”๋กœํ‚น, ๊ณ ์„ฑ๋Šฅ ๋†’์€ ํ•™์Šต ๊ณก์„ , ๋ณต์žกํ•œ ๋””๋ฒ„๊น…
RestTemplate ๊ฐ„๋‹จํ•œ ์‚ฌ์šฉ๋ฒ•, ์‰ฌ์šด ๋””๋ฒ„๊น… ๋™๊ธฐ ๋ฐฉ์‹, ์ƒ๋Œ€์  ์ €์„ฑ๋Šฅ

์„ ํƒ: RestTemplate

์„ ํƒ ์ด์œ 

DDD ๋„๋ฉ”์ธ ๊ฐ„ ํ†ต์‹  ๊ตฌ์กฐ๋ฅผ ์žก์•„๊ฐ€๋Š” ์ƒํ™ฉ์—์„œ๋Š”
๋น ๋ฅธ ๋ฌธ์ œ ํŒŒ์•…๊ณผ ๋””๋ฒ„๊น…์ด ์„ฑ๋Šฅ๋ณด๋‹ค ์šฐ์„ ์ˆœ์œ„๊ฐ€ ๋†’๋‹ค๊ณ  ํŒ๋‹จ

4. Auth์™€ User ๊ฐ„์˜ ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ ๋ฌธ์ œ

๋ฌธ์ œ ์ƒํ™ฉ

ํšŒ์›๊ฐ€์ž… ํ”„๋กœ์„ธ์Šค:
1. Auth ํ…Œ์ด๋ธ”์— ๋ฐ์ดํ„ฐ ์ €์žฅ โœ“
2. ์ด๋ฒคํŠธ ๋ฐœํ–‰ โœ“
3. User ํ…Œ์ด๋ธ”์— ๋ฐ์ดํ„ฐ ์ €์žฅ โœ— (์‹คํŒจ ๊ฐ€๋Šฅ)

๊ฒฐ๊ณผ: ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ ๊นจ์ง

ํ•ด๊ฒฐ ๋ฐฉ์•ˆ: ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜ (Saga Pattern)

๊ตฌํ˜„ ๊ตฌ์กฐ
@EventListener
public void handlerUserRegisteredEvent(UserRegisteredEvent event) {
    try {
        User user = User.fromUserEvent(
            event.getUserId(), 
            event.getEmail(), 
            event.getName(), 
            event.getCreatedAt()
        );
        userRepository.save(user);
    } catch (Exception e) {
        // User ์ €์žฅ ์‹คํŒจ ์‹œ ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜ ์ด๋ฒคํŠธ ๋ฐœํ–‰
        eventPublisher.publishEvent(
            UserCreationFailedEvent.of(event.getUserId())
        );
    }
}

๋ณด์ƒ ํŠธ๋žœ์žญ์…˜ ํ๋ฆ„:

User ์ €์žฅ ์‹คํŒจ โ†’ UserCreationFailedEvent ๋ฐœํ–‰ โ†’ Auth ๋ฐ์ดํ„ฐ ๋กค๋ฐฑ

๊ฐœ์„  ํšจ๊ณผ

  • ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ ๋ณด์žฅ
  • ๋ถ„์‚ฐ ํŠธ๋žœ์žญ์…˜ ํ™˜๊ฒฝ์—์„œ์˜ ์•ˆ์ •์„ฑ ํ™•๋ณด
  • ์‹คํŒจ ์ƒํ™ฉ์— ๋Œ€ํ•œ ์ž๋™ ๋ณต๊ตฌ ๋ฉ”์ปค๋‹ˆ์ฆ˜

5. ์›น์†Œ์ผ“ ์•Œ๋ฆผ ์‹œ์Šคํ…œ ์ตœ์ ํ™”

๊ธฐ์กด ๋ฐฉ์‹์˜ ๋ฌธ์ œ์ 

๊ฐœ๋ณ„ ์ „์†ก ๋ฐฉ์‹

์„œ๋ฒ„ โ†’ ๊ตฌ๋…์ž 1
      โ†’ ๊ตฌ๋…์ž 2  
      โ†’ ๊ตฌ๋…์ž 3
      โ†’ ...
      โ†’ ๊ตฌ๋…์ž N

๋ฐœ์ƒํ•œ ๋ฌธ์ œ๋“ค

๋ฌธ์ œ ์„ค๋ช… ์˜ํ–ฅ
์—ฐ๊ฒฐ ํ’€๋ง ์ œํ•œ ๋™์‹œ ์—ฐ๊ฒฐ ์ˆ˜ ํ•œ๊ณ„ ๋Œ€์šฉ๋Ÿ‰ ์‚ฌ์šฉ์ž ์‹œ ์—ฐ๊ฒฐ ๋Š๊น€
๋™๊ธฐ ์ฒ˜๋ฆฌ ์ง€์—ฐ ์ˆœ์ฐจ์  ๋ฉ”์‹œ์ง€ ์ „์†ก ๋งˆ์ง€๋ง‰ ์‚ฌ์šฉ์ž ์•Œ๋ฆผ ์ง€์—ฐ

๊ณ ๋ คํ•œ ํ•ด๊ฒฐ ๋ฐฉ์•ˆ๋“ค

  • ์บ์‹ฑ ์ ์šฉ
  • ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ
  • ํ•œ๊ณ„: ์›น์†Œ์ผ“ ์—ฐ๊ฒฐ ํ’€๋ง ์ œํ•œ์€ ๊ทผ๋ณธ์  ํ•ด๊ฒฐ ๋ถˆ๊ฐ€

์ตœ์ข… ํ•ด๊ฒฐ ๋ฐฉ์•ˆ: ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ ๋ฐฉ์‹

๋ณ€๊ฒฝ๋œ ๊ตฌ์กฐ

์„œ๋ฒ„: ๊ณตํ†ต ์•Œ๋ฆผ ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ
ํด๋ผ์ด์–ธํŠธ: ๊ตฌ๋… ์ƒํ’ˆ ํ•„ํ„ฐ๋ง ํ›„ ์ฒ˜๋ฆฌ
๊ตฌํ˜„ ์˜ˆ์‹œ

์„œ๋ฒ„ ์ธก (๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ)

@Service
public class NotificationService {
    public void notifyProductEvent(WSEventProduct event) {
        // ๋ชจ๋“  ์—ฐ๊ฒฐ๋œ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ
        messagingTemplate.convertAndSend(
            "/topic/notification", 
            event.toNotificationMessage()
        );
    }
}

ํด๋ผ์ด์–ธํŠธ ์ธก (ํ•„ํ„ฐ๋ง)

stompClient.subscribe('/topic/notification', function(message) {
    const eventData = JSON.parse(message.body);
    
    // ์‚ฌ์šฉ์ž๊ฐ€ ๊ตฌ๋…ํ•œ ์ƒํ’ˆ์ธ์ง€ ํ™•์ธ
    if (userSubscribedProducts.includes(eventData.productId)) {
        displayNotification(eventData);
    }
});

๊ฐœ์„  ํšจ๊ณผ

๊ฐœ์„  ํ•ญ๋ชฉ Before After
ํ™•์žฅ์„ฑ ์‚ฌ์šฉ์ž ์ˆ˜์— ๋น„๋ก€ํ•œ ์„ฑ๋Šฅ ์ €ํ•˜ ์‚ฌ์šฉ์ž ์ˆ˜ ๋ฌด๊ด€ํ•œ ์ผ์ • ์„ฑ๋Šฅ
์ฒ˜๋ฆฌ ๋ฐฉ์‹ ๋™๊ธฐ ์ˆœ์ฐจ ์ฒ˜๋ฆฌ ๋‹จ์ผ ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ
์—ฐ๊ฒฐ ๊ด€๋ฆฌ ๊ฐœ๋ณ„ ์—ฐ๊ฒฐ ๊ด€๋ฆฌ ํ•„์š” ๋‹จ์ˆœํ•œ ์—ฐ๊ฒฐ ๊ด€๋ฆฌ
์•Œ๋ฆผ ์ง€์—ฐ ๋งˆ์ง€๋ง‰ ์‚ฌ์šฉ์ž ์ง€์—ฐ ๋ฐœ์ƒ ๋ชจ๋“  ์‚ฌ์šฉ์ž ๋™์‹œ ์ˆ˜์‹ 

๋ฐœ์ƒํ•œ ๋ฌธ์ œ๋“ค

๋ฌธ์ œ ์„ค๋ช… ์˜ํ–ฅ
์—ฐ๊ฒฐ ํ’€๋ง ์ œํ•œ ๋™์‹œ ์—ฐ๊ฒฐ ์ˆ˜ ํ•œ๊ณ„ ๋Œ€์šฉ๋Ÿ‰ ์‚ฌ์šฉ์ž ์‹œ ์—ฐ๊ฒฐ ๋Š๊น€
๋™๊ธฐ ์ฒ˜๋ฆฌ ์ง€์—ฐ ์ˆœ์ฐจ์  ๋ฉ”์‹œ์ง€ ์ „์†ก ๋งˆ์ง€๋ง‰ ์‚ฌ์šฉ์ž ์•Œ๋ฆผ ์ง€์—ฐ

๊ณ ๋ คํ•œ ํ•ด๊ฒฐ ๋ฐฉ์•ˆ๋“ค

  • ์บ์‹ฑ ์ ์šฉ
  • ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ
  • ํ•œ๊ณ„: ์›น์†Œ์ผ“ ์—ฐ๊ฒฐ ํ’€๋ง ์ œํ•œ์€ ๊ทผ๋ณธ์  ํ•ด๊ฒฐ ๋ถˆ๊ฐ€

์ตœ์ข… ํ•ด๊ฒฐ ๋ฐฉ์•ˆ: ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ ๋ฐฉ์‹

๋ณ€๊ฒฝ๋œ ๊ตฌ์กฐ

์„œ๋ฒ„: ๊ณตํ†ต ์•Œ๋ฆผ ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ
ํด๋ผ์ด์–ธํŠธ: ๊ตฌ๋… ์ƒํ’ˆ ํ•„ํ„ฐ๋ง ํ›„ ์ฒ˜๋ฆฌ
๊ตฌํ˜„ ์˜ˆ์‹œ

์„œ๋ฒ„ ์ธก (๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ)

@Service
public class NotificationService {
    public void notifyProductEvent(WSEventProduct event) {
        // ๋ชจ๋“  ์—ฐ๊ฒฐ๋œ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ
        messagingTemplate.convertAndSend(
            "/topic/notification", 
            event.toNotificationMessage()
        );
    }
}

ํด๋ผ์ด์–ธํŠธ ์ธก (ํ•„ํ„ฐ๋ง)

stompClient.subscribe('/topic/notification', function(message) {
    const eventData = JSON.parse(message.body);
    
    // ์‚ฌ์šฉ์ž๊ฐ€ ๊ตฌ๋…ํ•œ ์ƒํ’ˆ์ธ์ง€ ํ™•์ธ
    if (userSubscribedProducts.includes(eventData.productId)) {
        displayNotification(eventData);
    }
});

๊ฐœ์„  ํšจ๊ณผ

๊ฐœ์„  ํ•ญ๋ชฉ Before After
ํ™•์žฅ์„ฑ ์‚ฌ์šฉ์ž ์ˆ˜์— ๋น„๋ก€ํ•œ ์„ฑ๋Šฅ ์ €ํ•˜ ์‚ฌ์šฉ์ž ์ˆ˜ ๋ฌด๊ด€ํ•œ ์ผ์ • ์„ฑ๋Šฅ
์ฒ˜๋ฆฌ ๋ฐฉ์‹ ๋™๊ธฐ ์ˆœ์ฐจ ์ฒ˜๋ฆฌ ๋‹จ์ผ ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ
์—ฐ๊ฒฐ ๊ด€๋ฆฌ ๊ฐœ๋ณ„ ์—ฐ๊ฒฐ ๊ด€๋ฆฌ ํ•„์š” ๋‹จ์ˆœํ•œ ์—ฐ๊ฒฐ ๊ด€๋ฆฌ
์•Œ๋ฆผ ์ง€์—ฐ ๋งˆ์ง€๋ง‰ ์‚ฌ์šฉ์ž ์ง€์—ฐ ๋ฐœ์ƒ ๋ชจ๋“  ์‚ฌ์šฉ์ž ๋™์‹œ ์ˆ˜์‹ 

6. ํ•ซ๋”œ ์ด๋ฒคํŠธ ๋Œ€์šฉ๋Ÿ‰ ๋“ฑ๋ก ์‹œ ์„ฑ๋Šฅ ๋ฌธ์ œ

์„ฑ๋Šฅ ์ธก์ • ๊ฒฐ๊ณผ (๊ฐœ์„  ์ „)

1๋‹จ๊ณ„ - Event ์ €์žฅ ์™„๋ฃŒ: 22ms
2๋‹จ๊ณ„ - ProductApiClient ํ˜ธ์ถœ ์™„๋ฃŒ: 236ms (์กฐํšŒ๋œ ์ƒํ’ˆ ์ˆ˜: 10000)
3๋‹จ๊ณ„ - EventItem ๊ฐ์ฒด ์ƒ์„ฑ ์™„๋ฃŒ: 4ms
4๋‹จ๊ณ„ - EventItem insert ์™„๋ฃŒ: 1052ms
5๋‹จ๊ณ„ - Event์— EventItem ๋ฆฌ์ŠคํŠธ ์„ค์ • ์™„๋ฃŒ: 0ms
6๋‹จ๊ณ„ - WSEventProduct ๊ฐ์ฒด ์ƒ์„ฑ ์™„๋ฃŒ: 1ms
7๋‹จ๊ณ„ - ์ด๋ฒคํŠธ ๋ฐœํ–‰ ์™„๋ฃŒ: 799ms
=== createEvent ์ด ์‹คํ–‰์‹œ๊ฐ„: 2115ms ===
8๋‹จ๊ณ„ - ์ปจํŠธ๋กค๋Ÿฌ ์‹คํ–‰ ์™„๋ฃŒ: 4126ms

๋ฌธ์ œ์ :

  • ์ปจํŠธ๋กค๋Ÿฌ ์ „์ฒด ์‹คํ–‰ ์‹œ๊ฐ„์ด 4126ms๋กœ ์‹ฌ๊ฐํ•œ ์ง€์—ฐ
  • EventItem ๋ฒŒํฌ insert๊ฐ€ 1052ms๋กœ ํฐ ๋ณ‘๋ชฉ
  • ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๊ฐ€ ๋™๊ธฐ์ ์œผ๋กœ ์ฒ˜๋ฆฌ๋˜์–ด ์ง€์—ฐ ๋ฐœ์ƒ

ํ•ด๊ฒฐ ๋ฐฉ์•ˆ

1. Notification ๋ฒŒํฌ ์ธ์„œํŠธ ์ ์šฉ

๊ตฌํ˜„ ์˜ˆ์‹œ

๊ฐœ์„  ์ „: ๊ฐœ๋ณ„ insert

// ๊ธฐ์กด ๋ฐฉ์‹ - ๊ฐœ๋ณ„ insert
notificationRepository.save(notification);

๊ฐœ์„  ํ›„: ๋ฒŒํฌ insert

@Service
public class NotificationService {
    private final List<Notification> buffer = new ArrayList<>();

    public void addNotification(Notification notification) {
        synchronized (lock) {
            buffer.add(notification);
            if(buffer.size() >= 1000) {
                // 1000๊ฐœ์”ฉ ๋ฒŒํฌ insert
                insertBatch();
            }
        }
    }

    @Async
    public void insertBatch() {
        List<Notification> notifications;
        synchronized (lock) {
            notifications = new ArrayList<>(buffer);
            buffer.clear();
        }
        notificationRepository.insertNotifications(notifications);
    }
}

๊ฐœ์„  ํšจ๊ณผ: ๊ฐœ๋ณ„ insert โ†’ ๋ฒŒํฌ insert๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ DB ํ˜ธ์ถœ ํšŸ์ˆ˜ ๋Œ€ํญ ๊ฐ์†Œ

2. EventItem ๋ฒŒํฌ ์ธ์„œํŠธ ์ ์šฉ

๊ตฌํ˜„ ์˜ˆ์‹œ

๊ฐœ์„  ์ „: JPA saveAll ์‚ฌ์šฉ

// ๊ธฐ์กด ๋ฐฉ์‹
eventItemRepository.saveAll(eventItems);

๊ฐœ์„  ํ›„: JdbcTemplate batchUpdate ์‚ฌ์šฉ

@Repository
public class EventItemInsertRepository {

    public void insertEventItem(List<EventItem> eventItems, Long eventId) {
        String sql = "INSERT INTO event_items (event_id, product_id, product_name, original_price, discount_price, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)";

        jdbcTemplate.batchUpdate(sql, eventItems, 1000, (ps, eventItem) -> {
            ps.setLong(1, eventId);
            ps.setLong(2, eventItem.getProductId());
            ps.setString(3, eventItem.getProductName());
            ps.setBigDecimal(4, eventItem.getOriginalPrice());
            ps.setBigDecimal(5, eventItem.getDiscountPrice());
            ps.setObject(6, LocalDateTime.now());
            ps.setObject(7, LocalDateTime.now());
        });
    }
}

๊ฐœ์„  ํšจ๊ณผ:

  • JPA saveAll โ†’ JdbcTemplate batchUpdate๋กœ ๋ณ€๊ฒฝ
  • ๋ฐฐ์น˜ ํฌ๊ธฐ 1000์œผ๋กœ ์„ค์ •ํ•˜์—ฌ ๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ์„ฑ ํ–ฅ์ƒ

3. ์ด๋ฒคํŠธ ๋ฐœํ–‰ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ + ํŠธ๋žœ์žญ์…˜ ์ผ๊ด€์„ฑ ๋ณด์žฅ

๊ตฌํ˜„ ์˜ˆ์‹œ

๋ฌธ์ œ ์ƒํ™ฉ

@Transactional
public EventResponse createEvent(EventCrateRequest request) {
    // ... ๋ฐ์ดํ„ฐ ์ €์žฅ ...

    // ์ด๋ฒคํŠธ ๋ฐœํ–‰ (๋™๊ธฐ์ )
    wsEventProducts.forEach(wsEvent -> {
        eventPublisher.publishEvent(wsEvent);
    });

    return new EventResponse(event);
}

๋ฌธ์ œ์ : ์ด๋ฒคํŠธ ๋ฐœํ–‰์ด ๋™๊ธฐ์ ์œผ๋กœ ์ฒ˜๋ฆฌ๋˜์–ด ์ง€์—ฐ ๋ฐœ์ƒ

ํ•ด๊ฒฐ ๋ฐฉ์•ˆ: @TransactionalEventListener ์ ์šฉ

@Component
public class NotificationListener {

    @Async
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void addProductDiscountEvent(WSEventProduct event) {
        try {
            log.info("addProductDiscountEvent ์‹œ์ž‘ - ๋‹จ์ผ ์ด๋ฒคํŠธ: {}", event.product_id());

            ListenProductEvent listenProductEvent = new ListenProductEvent(event);
            notificationService.notifyProductEventMessage(listenProductEvent);

            log.info("addProductDiscountEvent ์ข…๋ฃŒ");
        } catch (Exception e) {
            log.error("addProductDiscountEvent ์ฒ˜๋ฆฌ ์‹คํŒจ message : {}", e.getMessage());
        }
    }
}

๊ฐœ์„  ํšจ๊ณผ:

  • ํŠธ๋žœ์žญ์…˜ ์ปค๋ฐ‹ ํ›„ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์‹คํ–‰์œผ๋กœ ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ ๋ณด์žฅ
  • ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ๋กœ ์ปจํŠธ๋กค๋Ÿฌ ์‘๋‹ต ์‹œ๊ฐ„ ๋‹จ์ถ•
  • ํŠธ๋žœ์žญ์…˜ ๋กค๋ฐฑ ์‹œ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ๋ฏธ์‹คํ–‰์œผ๋กœ ์•ˆ์ •์„ฑ ํ™•๋ณด

4. JPA ์—ฐ๊ด€๊ด€๊ณ„ ๋งคํ•‘ ์ œ๊ฑฐ

๊ตฌํ˜„ ์˜ˆ์‹œ

๊ฐœ์„  ์ „: ์ผ๋Œ€๋‹ค ์—ฐ๊ด€๊ด€๊ณ„

@Entity
public class Event {
    @OneToMany(mappedBy = "event", cascade = CascadeType.ALL)
    private List<EventItem> products;
}

@Entity
public class EventItem {
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "event_id")
    private Event event;
}

๋ฌธ์ œ์ :

  • ์กฐ์ธ ํ…Œ์ด๋ธ”(events_products)์— ๋Œ€๋Ÿ‰ insert ์ฟผ๋ฆฌ ๋ฐœ์ƒ
  • ํŠธ๋žœ์žญ์…˜ ์ข…๋ฃŒ ์‹œ ์ง€์—ฐ ๋ฐœ์ƒ
  • ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์ฆ๊ฐ€

๊ฐœ์„  ํ›„: ์—ฐ๊ด€๊ด€๊ณ„ ์ œ๊ฑฐ

@Entity
public class Event {
    @Transient  // ์กฐํšŒ์šฉ์œผ๋กœ๋งŒ ์‚ฌ์šฉ
    private List<EventItem> products;
}

@Entity
public class EventItem {
    private Long eventId;  // ๋‹จ์ˆœ ์™ธ๋ž˜ํ‚ค๋งŒ ์ €์žฅ
}

๊ฐœ์„  ํšจ๊ณผ:

  • ์กฐ์ธ ํ…Œ์ด๋ธ” insert ์ฟผ๋ฆฌ ์ œ๊ฑฐ
  • ํŠธ๋žœ์žญ์…˜ ์ข…๋ฃŒ ์‹œ ์˜ค๋ฒ„ํ—ค๋“œ ๋Œ€ํญ ๊ฐ์†Œ
  • ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์ตœ์ ํ™”

์„ฑ๋Šฅ ์ธก์ • ๊ฒฐ๊ณผ (๊ฐœ์„  ํ›„)

1๋‹จ๊ณ„ - Event ์ €์žฅ ์™„๋ฃŒ: 29ms
2๋‹จ๊ณ„ - ProductApiClient ํ˜ธ์ถœ ์™„๋ฃŒ: 260ms (์กฐํšŒ๋œ ์ƒํ’ˆ ์ˆ˜: 10000)
3๋‹จ๊ณ„ - EventItem ๊ฐ์ฒด ์ƒ์„ฑ ์™„๋ฃŒ: 3ms
4๋‹จ๊ณ„ - EventItem ๋ฒŒํฌ insert ์™„๋ฃŒ: 520ms
5๋‹จ๊ณ„ - Event์— EventItem ๋ฆฌ์ŠคํŠธ ์„ค์ • ์™„๋ฃŒ: 0ms
6๋‹จ๊ณ„ - WSEventProduct ๊ฐ์ฒด ์ƒ์„ฑ ์™„๋ฃŒ: 1ms
7๋‹จ๊ณ„ - ์ด๋ฒคํŠธ ๋ฐœํ–‰ ์™„๋ฃŒ: 10ms
=== createEvent ์ด ์‹คํ–‰์‹œ๊ฐ„: 823ms ===
8๋‹จ๊ณ„ - ์ปจํŠธ๋กค๋Ÿฌ ์‹คํ–‰: 844ms

๊ด€๋ จ ๋ธ”๋กœ๊ทธ

์ฐจ์ค€ํ˜ธ

  • https://juno0112.tistory.com/116 : ๋„๋ฉ”์ธ์ฃผ๋„ ์„ค๊ณ„ ๋ฐ ๋ฐ์ดํ„ฐ ํ๋ฆ„ ์‹œ๊ฐํ™”ํ•˜๊ธฐ
  • https://juno0112.tistory.com/117 : ๋„๋ฉ”์ธ ๊ฐ„ ์˜์กด์„ฑ ๋‚ฎ์ถ”๊ธฐ : API ํ˜ธ์ถœ ๋ฐ ์Šคํ”„๋ง ์ด๋ฒคํŠธ ๊ตฌ๋…์„ ์œ„ํ•œ Common ํŒจํ‚ค์ง€ ์„ค๊ณ„
  • https://juno0112.tistory.com/118 : Spring Boot์—์„œ RestTemplate ๋‚ด๋ถ€ API ํ˜ธ์ถœ ์‹œ JWT ํ† ํฐ ์ „๋‹ฌํ•˜๊ธฐ

๊น€์‹ ์˜

์ด์ค€์˜

์ตœ์˜์žฌ

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Languages

  • Java 95.0%
  • HTML 4.0%
  • Python 1.0%