Skip to content

GuDaeWoong/Mania-Place

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

766 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

image

ยทโ€ขยท ๋งˆ๋‹ˆ์•„ ์ธต์„ ์œ„ํ•œ ์ค‘๊ณ ๊ฑฐ๋ž˜ ํ”Œ๋žซํผ ยทโ€ขยท

ํŒ€์› ์†Œ๊ฐœ

์ด๋ฆ„ ์—ญํ•  GitHub ์ฃผ์†Œ
๊ตฌ๋Œ€์›… ๋ฆฌ๋” https://github.com/GuDaeWoong
์ตœ๊ฒฝ์ง„ ๋ถ€๋ฆฌ๋” https://github.com/Che0807
์šฐ์ƒˆ๋น› ๋ฉค๋ฒ„ https://github.com/saevit
์œคํฌ์ค€ ๋ฉค๋ฒ„ https://github.com/planbsoho
์ดํ˜ธ์ค€ ๋ฉค๋ฒ„ https://github.com/Gaeso
์ดํšจ์„  ๋ฉค๋ฒ„ https://github.com/hohohosn

๐Ÿ—‚๏ธ ๋ชฉ์ฐจ

| ๐Ÿ‘€ ์„œ๋น„์Šค ๊ฐœ์š” | ๐Ÿ—๏ธ ํ•ต์‹ฌ ๊ธฐ๋Šฅ | ๐Ÿ–‡๏ธ ์‹œ์Šคํ…œ ์•„ํ‚คํ…์ฒ˜ |

| ๐Ÿ“ ์„ค๊ณ„ ๋ฌธ์„œ | ๐Ÿ› ๏ธ ๊ธฐ์ˆ  ์Šคํƒ | ๐ŸŒŠ ์„œ๋น„์Šค ํ”Œ๋กœ์šฐ |

| ๐Ÿ’ก ์˜์‚ฌ๊ฒฐ์ • ๋ฐ ๊ธฐ๋Šฅ ๊ตฌํ˜„ | โšก๏ธ ์„ฑ๋Šฅ ๊ฐœ์„  | ๐Ÿšจ ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ… |

| ๐Ÿ“… ์ผ์ • | ๐Ÿงญย ํ–ฅํ›„ ๊ฐœ์„  ๊ณ„ํš |

๐Ÿ‘€ ์„œ๋น„์Šค ๊ฐœ์š”

Mania Place

์š”์ฆ˜ ์• ๋‹ˆ๋ฉ”์ด์…˜์€ '๋ฌธํ™”'์ž…๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์‚ฌ๋ž‘ํ•˜๋Š” ๋•ํ›„๋“ค์€ ์—ฌ์ „ํžˆ ์ •๋ณด ์ˆ˜์ง‘, ์†Œํ†ต, ๊ตฟ์ฆˆ ๊ฑฐ๋ž˜, ์ปค๋ฎค๋‹ˆํ‹ฐ

๋“ฑ ์ผ์ƒ ์† ๋ถˆํŽธํ•จ์„ ๊ฒช๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.


๐ŸŽฌ ๋งค์ผ๋งค์ผ "์ƒˆ๋กœ ๋‚˜์˜จ ์žฌ๋ฐŒ๋Š” ์• ๋‹ˆ ๊ตฟ์ฆˆ ์ƒํ’ˆ์€ ์—†์„๊นŒ?" ๊ถ๊ธˆํ•˜์ง€๋งŒ,

์—ฌ๋Ÿฌ ์‚ฌ์ดํŠธ๋ฅผ ๋Œ์•„๋‹ค๋‹ˆ๋ฉฐ ์ฐพ๊ธฐ์—” ๋„ˆ๋ฌด ๋ฒˆ๊ฑฐ๋กญ๊ณ ...


โ™ป๏ธ ํ•œ ๋ฒˆ ๋ณด๊ณ  ๋งˆ๋Š” ์•„ํฌ๋ฆด ์Šคํƒ ๋“œ, ์•ˆ ๋งž๋Š” ํ”ผ๊ทœ์–ด, ์ค‘๋ณต ๊ตฟ์ฆˆ๋“ค...

์ค‘๊ณ  ๊ฑฐ๋ž˜๋Š” ์‚ฌ๊ธฐ ๊ฑฑ์ •๋„ ๋˜๊ณ  ๋„ˆ๋ฌด ๊ท€์ฐฎ๊ณ ...


๐Ÿ›๏ธ ํ•œ์ •ํŒ ๊ตฟ์ฆˆ ๋†“์น˜๊ณ  ์‹ถ์ง€ ์•Š์€๋ฐ

์–ด๋””์„œ ์–ธ์ œ ๋‚˜์˜ค๋Š”์ง€ ์ •๋ณด๊ฐ€ ๋‹ค ํฉ์–ด์ ธ ์žˆ๊ณ ...


๐Ÿ”ฅ ์ง€๊ธˆ ์‚ฌ๋žŒ๋“ค์ด ๊ฐ€์žฅ ๋งŽ์ด ์ฐพ๋Š” ๊ฑด ๋ญ˜๊นŒ?

์‹ค์‹œ๊ฐ„์œผ๋กœ ํ™•์ธํ•  ๋ฐฉ๋ฒ•์ด ์—†์œผ๋‹ˆ, ํŠธ๋ Œ๋“œ๋ฅผ ๋†“์น˜๊ธฐ ์ผ์‘ค...


๐Ÿค” "์ด ๊ตฟ์ฆˆ ์‚ด๊นŒ? ๋ง๊นŒ?" ํ˜ผ์ž ๊ณ ๋ฏผ๋งŒ ํ•˜๋‹ค๊ฐ€

๊ฒฐ๊ตญ ์ถฉ๋™๊ตฌ๋งค๋กœ ํ›„ํšŒํ•˜๊ฑฐ๋‚˜, ๋ง์„ค์ด๋‹ค ๋†“์น˜๊ฑฐ๋‚˜...


์ด๋Ÿฐ ๋ถˆํŽธํ•จ์„ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด,
ํ•˜๋ฃจ ํ•œ ๋ฒˆ, ์• ๋‹ˆ๋ฉ”์ด์…˜๊ณผ ๋” ๊ฐ€๊นŒ์›Œ์ง€๋Š” ํ”Œ๋žซํผ์„ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค.


๐Ÿ—๏ธ ํ•ต์‹ฌ ๊ธฐ๋Šฅ

๊ฑฐ๋ž˜์ž ๊ฐ„ ์ฑ„ํŒ… ์‹œ์Šคํ…œ

์‹ค์‹œ๊ฐ„ ์†Œํ†ต ๊ธฐ๋Šฅ

  • ๊ตฟ์ฆˆ ๊ฑฐ๋ž˜ ์ „ ์ƒํ’ˆ ์ƒํƒœ ๋ฐ ๊ฐ€๊ฒฉ ํ˜‘์ƒ
  • ๊ฑฐ๋ž˜ ์กฐ๊ฑด ๋ฐ ๋ฐฐ์†ก ๋ฐฉ๋ฒ• ๋…ผ์˜

์•ˆ์ „ํ•œ ๊ฑฐ๋ž˜ ํ™˜๊ฒฝ

  • ๊ฒ€์ฆ๋œ websocket๊ณผ stompํ†ต์‹ ์„ ํ†ตํ•ด ์•ˆ์ „ํ•œ ๊ฑฐ๋ž˜ ํ™˜๊ฒฝ ์ œ๊ณต
  • ์ˆ˜ํ‰ ํ™•์žฅ์„ ๊ณ ๋ คํ•œ ๋ฉ”์„ธ์ง€ ๋ธŒ๋กœ์ปค ์„ค๊ณ„๋กœ ์•ˆ์ •์„ฑ ์ œ๊ณต

๋ถ„์‚ฐ๋ฝ์„ ํ†ตํ•œ ์ค‘๋ณต ๊ฑฐ๋ž˜ ๋ฐฉ์ง€

๋™์‹œ ๊ฑฐ๋ž˜ ์ œ์–ด

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

์‹œ์Šคํ…œ ์•ˆ์ •์„ฑ

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

๊ด€์‹ฌ์‚ฌ ํƒœ๊ทธ ์‹œ์Šคํ…œ

๋งž์ถคํ˜• ์ฝ˜ํ…์ธ  ํ๋ ˆ์ด์…˜

  • ๊ฐœ์ธ ๊ด€์‹ฌ์‚ฌ ํƒœ๊ทธ ์„ค์ • ๋ฐ ๊ด€๋ฆฌ
  • ํƒœ๊ทธ ๊ธฐ๋ฐ˜ ๋งž์ถค ์ƒํ’ˆ ์ถ”์ฒœ

ํƒœ๊ทธ ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ๋ง

  • ๊ตฟ์ฆˆ ๊ฒ€์ƒ‰ ์‹œ ๊ด€์‹ฌ ํƒœ๊ทธ ์šฐ์„  ํ‘œ์‹œ

๊ตฌ๋งค ๊ณ ๋ฏผ ์ƒ๋‹ด ํŽ˜์ด์ง€

์ปค๋ฎค๋‹ˆํ‹ฐ ๊ธฐ๋ฐ˜ ์ƒ๋‹ด

  • "์‚ด๊นŒ๋ง๊นŒ" ๊ณ ๋ฏผ ๊ฒŒ์‹œํŒ ์šด์˜
  • ๋‹ค๋ฅธ ๋•ํ›„๋“ค์˜ ์†”์งํ•œ ๊ตฌ๋งค ์กฐ์–ธ
  • ์ƒํ’ˆ๋ณ„ ์žฅ๋‹จ์  ๋ฐ ํ›„๊ธฐ ๊ณต์œ 

ํˆฌํ‘œ ์‹œ์Šคํ…œ ์„ธ๋ถ€ ๊ธฐ๋Šฅ

  • ์ข‹์•„์š” / ์‹ซ์–ด์š” : โ€œ๊ตฌ๋งค ์ถ”์ฒœ ์—ฌ๋ถ€โ€ ์‹ค์‹œ๊ฐ„ ์ง‘๊ณ„
  • ํˆฌํ‘œ ์ด์œ : ์žฌ๋ฏธ ๋˜๋Š” ๊ตฌ๋งค๋ฅผ ๊ฐˆ๋“ฑํ•˜๋Š” ์œ ์ €๋ฅผ ์œ„ํ•œ ์ข‹์•„์š”/์‹ซ์–ด์š”

๋Œ“๊ธ€ ์‹œ์Šคํ…œ

  • ์ข‹์•„์š” ์‹ซ์–ด์š” ์ด์™ธ์—๋„ ์ƒํ’ˆ ๊ตฌ๋งคํ•˜๋Š” ๊ฒƒ์— ๋Œ€ํ•œ ์ง„์‹ฌ ์–ด๋ฆฐ ์กฐ์–ธ

์ตœ์‹  ์†Œ์‹ ๊ณต์œ  ํŽ˜์ด์ง€

์ตœ์‹  ์ •๋ณด ๊ณต์œ 

  • "์ƒˆ์†Œ์‹" ๊ณต์ง€ ๊ฒŒ์‹œํŒ ์šด์˜
  • ์ตœ์‹  ์„œ๋ธŒ์ปฌ์ฒ˜ ์ปจํ…์ธ , ๊ตฟ์ฆˆ, ์ด๋ฒคํŠธ ์†Œ์‹์„ ์ œ๊ณต
  • ๊ด‘๊ณ  ๊ฒŒ์‹œ๋ฅผ ํ†ตํ•ด ์ˆ˜์ต ์ฐฝ์ถœ ๊ฐ€๋Šฅ

์บ์‹œ ๊ธฐ๋ฐ˜ ์„ฑ๋Šฅ ์ตœ์ ํ™”

  • Redis ์บ์‹œ๋ฅผ ํ™œ์šฉํ•ด ๋น ๋ฅธ ์‘๋‹ต ์†๋„์™€ ๋†’์€ ์ฒ˜๋ฆฌ๋Ÿ‰ ํ™•๋ณด
  • TTL๊ณผ ๋ฌดํšจํ™” ๋กœ์ง์„ ํ•จ๊ป˜ ์ ์šฉํ•ด ๋ฐ์ดํ„ฐ ์ตœ์‹ ์„ฑ ํ™•๋ณด

์ธ๊ธฐ ๊ฒ€์ƒ‰์–ด ๋žญํ‚น

์ธ๊ธฐ ๊ฒ€์ƒ‰์–ด ๋žญํ‚น

  • ์ง€๋‚œ 24์‹œ๊ฐ„ ๋™์•ˆ ๊ฐ€์žฅ ๋งŽ์ด ๊ฒ€์ƒ‰๋œ ํ‚ค์›Œ๋“œ๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ง‘๊ณ„
  • ์ตœ์‹  ํŠธ๋ Œ๋“œ๋ฅผ ํ•œ๋ˆˆ์— ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ๋Š” ํƒ์ƒ‰ ๊ฒฝํ—˜ ์ œ๊ณต
  • ๊ด€์‹ฌ์‚ฌ ๊ธฐ๋ฐ˜์œผ๋กœ ์ƒˆ๋กœ์šด ์ƒํ’ˆ์„ ๋ฐœ๊ฒฌํ•˜๊ณ  ๊ตฌ๋งค๋กœ ์—ฐ๊ฒฐ
  • ๊ฒ€์ƒ‰๋Ÿ‰ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ˆ˜์š” ๋†’์€ ์ƒํ’ˆ์„ ์˜ˆ์ธก

์šด์˜ ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง

AOP ๊ธฐ๋ฐ˜ ๋กœ๊น…

  • ์ฃผ์š” ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์˜ ์‹คํ–‰ ํ๋ฆ„๊ณผ ์˜ˆ์™ธ๋ฅผ ์ž๋™์œผ๋กœ ๋กœ๊น…
  • ์—๋Ÿฌ ์ถ”์  ๋ฐ ๋””๋ฒ„๊น… ํšจ์œจ์„ฑ ํ–ฅ์ƒ

Pinpoint ๋ชจ๋‹ˆํ„ฐ๋ง ํ™˜๊ฒฝ ๊ตฌ์ถ•

  • ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง(APM)์„ ํ†ตํ•œ ํŠธ๋žœ์žญ์…˜ ์ถ”์ 
  • ์žฅ์•  ๋ฐœ์ƒ ์‹œ ์›์ธ ๋ถ„์„ ๋ฐ ์„ฑ๋Šฅ ๋ณ‘๋ชฉ ์ง€์  ํŒŒ์•… ๊ฐ€๋Šฅ

๐Ÿ–‡๏ธ ์‹œ์Šคํ…œ ์•„ํ‚คํ…์ฒ˜

Cloud Architecture

image

CI/CD Pipeline

image

๐Ÿ“ ์„ค๊ณ„ ๋ฌธ์„œ

ERD

image

์™€์ด์–ดํ”„๋ ˆ์ž„

image

API ๋ช…์„ธ์„œ

API ๋ฌธ์„œ

๋””๋ ‰ํ† ๋ฆฌ

๐Ÿ“ฆ Mania-Place
โ”œโ”€โ”€ ๐Ÿ“‚ common                  # ๊ณตํ†ต ๋ชจ๋“ˆ, ์ „์ฒด ํ”„๋กœ์ ํŠธ์—์„œ ์žฌ์‚ฌ์šฉ๋˜๋Š” ์ฝ”๋“œ
โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ annotation          # ์ปค์Šคํ…€ ์–ด๋…ธํ…Œ์ด์…˜
โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ aop                 # AOP ๊ด€๋ จ ๋กœ์ง
โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ config              # ๊ณตํ†ต ์„ค์ •
โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ dto                 # ๊ณตํ†ต DTO
โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ entity              # ๊ณตํ†ต ์—”ํ‹ฐํ‹ฐ
โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ exception           # ์˜ˆ์™ธ ์ฒ˜๋ฆฌ
โ”‚   โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ enums           # ์˜ˆ์™ธ ๊ด€๋ จ ์—ด๊ฑฐํ˜•
โ”‚   โ”‚   โ””โ”€โ”€ ๐Ÿ“‚ exceptionclass  # ์ปค์Šคํ…€ ์˜ˆ์™ธ ํด๋ž˜์Šค
โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ filter              # ํ•„ํ„ฐ ์ฒ˜๋ฆฌ
โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ health              # ์ƒํƒœ ์ฒดํฌ
โ”‚   โ””โ”€โ”€ ๐Ÿ“‚ security            # ๋ณด์•ˆ ๊ด€๋ จ
โ”‚       โ””โ”€โ”€ ๐Ÿ“‚ jwt             # JWT ์ธ์ฆ ์ฒ˜๋ฆฌ
โ””โ”€โ”€ ๐Ÿ“‚ domain                  # ๋„๋ฉ”์ธ๋ณ„ ๋ชจ๋“ˆ
    โ”œโ”€โ”€ ๐Ÿ“‚ auth                # ์ธ์ฆ/์ธ๊ฐ€
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ controller      # API ์ปจํŠธ๋กค๋Ÿฌ
    โ”‚   โ”‚   โ””โ”€โ”€ ๐Ÿ“‚ dto         # ์š”์ฒญ/์‘๋‹ต DTO
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ domain          # ๋„๋ฉ”์ธ ๋ ˆ์ด์–ด
    โ”‚   โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ model       # ๋„๋ฉ”์ธ ๋ชจ๋ธ
    โ”‚   โ”‚   โ””โ”€โ”€ ๐Ÿ“‚ repository  # DB ์ ‘๊ทผ
    โ”‚   โ””โ”€โ”€ ๐Ÿ“‚ service         # ์„œ๋น„์Šค ๋กœ์ง
    โ”œโ”€โ”€ ๐Ÿ“‚ chatmessage         # ์ฑ„ํŒ… ๋ฉ”์‹œ์ง€
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ controller
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ dto
    โ”‚   โ”‚   โ””โ”€โ”€ ๐Ÿ“‚ request
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ entity
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ repository
    โ”‚   โ””โ”€โ”€ ๐Ÿ“‚ service
    โ”œโ”€โ”€ ๐Ÿ“‚ chatroom            # ์ฑ„ํŒ…๋ฐฉ
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ controller
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ dto
    โ”‚   โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ request
    โ”‚   โ”‚   โ””โ”€โ”€ ๐Ÿ“‚ response
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ entity
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ repository
    โ”‚   โ””โ”€โ”€ ๐Ÿ“‚ service
    โ”œโ”€โ”€ ๐Ÿ“‚ Image               # ์ด๋ฏธ์ง€
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ dto
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ entity
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ repository
    โ”‚   โ””โ”€โ”€ ๐Ÿ“‚ service
    โ”œโ”€โ”€ ๐Ÿ“‚ item                # ์ƒํ’ˆ
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ controller
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ dto
    โ”‚   โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ request
    โ”‚   โ”‚   โ””โ”€โ”€ ๐Ÿ“‚ response
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ entity
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ repository
    โ”‚   โ””โ”€โ”€ ๐Ÿ“‚ service
    โ”œโ”€โ”€ ๐Ÿ“‚ itemcomment         # ์ƒํ’ˆ ๋Œ“๊ธ€
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ controller
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ dto
    โ”‚   โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ request
    โ”‚   โ”‚   โ””โ”€โ”€ ๐Ÿ“‚ response
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ entity
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ repository
    โ”‚   โ””โ”€โ”€ ๐Ÿ“‚ service
    โ”œโ”€โ”€ ๐Ÿ“‚ itemtag             # ์ƒํ’ˆ ํƒœ๊ทธ
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ entity
    โ”‚   โ””โ”€โ”€ ๐Ÿ“‚ repository
    โ”œโ”€โ”€ ๐Ÿ“‚ keyword             # ํ‚ค์›Œ๋“œ
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ controller
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ domain
    โ”‚   โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ model
    โ”‚   โ”‚   โ””โ”€โ”€ ๐Ÿ“‚ repository
    โ”‚   โ””โ”€โ”€ ๐Ÿ“‚ service
    โ”‚       โ””โ”€โ”€ ๐Ÿ“‚ dto
    โ”œโ”€โ”€ ๐Ÿ“‚ newsfeed            # ๋‰ด์Šคํ”ผ๋“œ
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ controller
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ dto
    โ”‚   โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ request
    โ”‚   โ”‚   โ””โ”€โ”€ ๐Ÿ“‚ response
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ entity
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ repository
    โ”‚   โ””โ”€โ”€ ๐Ÿ“‚ service
    โ”œโ”€โ”€ ๐Ÿ“‚ newsfeedcomment     # ๋‰ด์Šคํ”ผ๋“œ ๋Œ“๊ธ€
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ controller
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ dto
    โ”‚   โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ request
    โ”‚   โ”‚   โ””โ”€โ”€ ๐Ÿ“‚ response
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ entity
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ repository
    โ”‚   โ””โ”€โ”€ ๐Ÿ“‚ service
    โ”œโ”€โ”€ ๐Ÿ“‚ order               # ์ฃผ๋ฌธ
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ controller
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ dto
    โ”‚   โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ request
    โ”‚   โ”‚   โ””โ”€โ”€ ๐Ÿ“‚ response
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ entity
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ repository
    โ”‚   โ””โ”€โ”€ ๐Ÿ“‚ service
    โ”œโ”€โ”€ ๐Ÿ“‚ post                # ๊ฒŒ์‹œ๊ธ€
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ controller
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ dto
    โ”‚   โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ request
    โ”‚   โ”‚   โ””โ”€โ”€ ๐Ÿ“‚ response
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ entity
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ repository
    โ”‚   โ””โ”€โ”€ ๐Ÿ“‚ service
    โ”œโ”€โ”€ ๐Ÿ“‚ postcomment         # ๊ฒŒ์‹œ๊ธ€ ๋Œ“๊ธ€
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ controller
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ dto
    โ”‚   โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ request
    โ”‚   โ”‚   โ””โ”€โ”€ ๐Ÿ“‚ response
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ entity
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ repository
    โ”‚   โ””โ”€โ”€ ๐Ÿ“‚ service
    โ”œโ”€โ”€ ๐Ÿ“‚ tag                 # ํƒœ๊ทธ
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ controller
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ dto
    โ”‚   โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ request
    โ”‚   โ”‚   โ””โ”€โ”€ ๐Ÿ“‚ response
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ entity
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ repository
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ service
    โ”‚   โ””โ”€โ”€ ๐Ÿ“‚ util
    โ”œโ”€โ”€ ๐Ÿ“‚ user                # ์‚ฌ์šฉ์ž
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ controller
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ dto
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ entity
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ repository
    โ”‚   โ””โ”€โ”€ ๐Ÿ“‚ service
    โ”œโ”€โ”€ ๐Ÿ“‚ usertag             # ์‚ฌ์šฉ์ž ํƒœ๊ทธ
    โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ entity
    โ”‚   โ””โ”€โ”€ ๐Ÿ“‚ repository
    โ””โ”€โ”€ ๐Ÿ“‚ vote                # ํˆฌํ‘œ
        โ”œโ”€โ”€ ๐Ÿ“‚ controller
        โ”œโ”€โ”€ ๐Ÿ“‚ dto
        โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ request
        โ”‚   โ””โ”€โ”€ ๐Ÿ“‚ response
        โ”œโ”€โ”€ ๐Ÿ“‚ entity
        โ”œโ”€โ”€ ๐Ÿ“‚ repository
        โ””โ”€โ”€ ๐Ÿ“‚ service

๐ŸŒŠ ์„œ๋น„์Šค ํ”Œ๋กœ์šฐ

image

๐Ÿ› ๏ธ ๊ธฐ์ˆ  ์Šคํƒ

๊ตฌ๋ถ„ ์‚ฌ์šฉ ๊ธฐ์ˆ 
Back-end Java 17, Spring Framework, Spring Boot, Spring Data JPA, Spring Security, JWT, Query DSL, Websocket, STOMP, Jackson
Productivity Tools Lombok, Gradle
Database MySQL, Redis
Infra & CI/CD Docker, RabbitMQ, Amazon EC2, Amazon SES, GitHub Actions
Test Postman, JMeter
Monitoring PinPoint
Tools IntelliJ IDEA
Collaboration GitHub, Notion, Slack, ERD cloud, draw.io

๐Ÿ’ก ์˜์‚ฌ๊ฒฐ์ • ๋ฐ ๊ธฐ๋Šฅ ๊ตฌํ˜„

๐Ÿ’ก JWT&Spring Security

1. ๋ฐฐ๊ฒฝ

Mania Place ์„œ๋น„์Šค์—์„œ๋Š” ๋กœ๊ทธ์ธ๊ณผ ๊ถŒํ•œ ๊ด€๋ฆฌ๊ฐ€ ํ•„์š”ํ–ˆ์Šต๋‹ˆ๋‹ค.

์„ธ์…˜ ๊ธฐ๋ฐ˜ ์ธ์ฆ์„ ๊ณ ๋ คํ–ˆ์œผ๋‚˜, ํŠธ๋ž˜ํ”ฝ์ด ๋ชฐ๋ฆด ๋•Œ ์„œ๋ฒ„ ๋ถ€๋‹ด์ด ํฌ๊ณ  ์—ฌ๋Ÿฌ ์„œ๋ฒ„๋ฅผ ํ™•์žฅํ•  ๋•Œ ์„ธ์…˜ ๊ณต์œ  ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

์ด ๋ฌธ์ œ ๋•Œ๋ฌธ์— JWT์™€ Spring Security๋ฅผ ๋„์ž…ํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.


2. ์š”๊ตฌ์‚ฌํ•ญ

  • ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธ ํ›„ ์•ˆ์ „ํ•˜๊ฒŒ ์ธ์ฆ ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•  ๊ฒƒ
  • ์ผ๋ฐ˜ ์‚ฌ์šฉ์ž / ๊ด€๋ฆฌ์ž ๊ถŒํ•œ์„ ๊ตฌ๋ถ„ํ•ด ์ ‘๊ทผ์„ ์ œ์–ดํ•  ๊ฒƒ
  • ์„œ๋ฒ„๊ฐ€ ์ƒํƒœ๋ฅผ ์ง์ ‘ ๋“ค๊ณ  ์žˆ์ง€ ์•Š์•„๋„ ํ™•์žฅ์ด ๊ฐ€๋Šฅํ•  ๊ฒƒ
  • ๋ณด์•ˆ์„ฑ์„ ํ•ด์น˜์ง€ ์•Š์œผ๋ฉด์„œ๋„ ์‚ฌ์šฉ์ž๊ฐ€ ๋ถˆํŽธํ•˜์ง€ ์•Š๊ฒŒ ๊ตฌํ˜„ํ•  ๊ฒƒ

3. ์˜์‚ฌ๊ฒฐ์ •

  • ์„ธ์…˜ ๊ธฐ๋ฐ˜ ์ธ์ฆ์€ ์„œ๋ฒ„ ๋ถ€ํ•˜์™€ ํ™•์žฅ์„ฑ ๋ฌธ์ œ๋กœ ์ œ์™ธ
  • OAuth2 / OIDC๋Š” ๊ธฐ๋Šฅ์€ ํ’๋ถ€ํ•˜์ง€๋งŒ ํ˜„์žฌ ํ”„๋กœ์ ํŠธ ๋ฒ”์œ„์—๋Š” ์˜ค๋ฒ„์ŠคํŽ™์ด๋ผ ํŒ๋‹จ
  • JWT๋Š” ๋ฌด์ƒํƒœ(stateless)๋กœ ์„œ๋ฒ„ ๋ถ€๋‹ด์„ ์ค„์ด๊ณ , ๊ถŒํ•œ ์ •๋ณด๋„ ํ•จ๊ป˜ ๋‹ด์„ ์ˆ˜ ์žˆ์–ด ์ ํ•ฉํ•˜๋‹ค๊ณ  ํŒ๋‹จ
  • JWT + Spring Security ์กฐํ•ฉ์œผ๋กœ ๊ฒฐ์ •

4. ๊ตฌํ˜„ ์ƒ์„ธ

  • ๋กœ๊ทธ์ธ ์„ฑ๊ณต ์‹œ ์•ก์„ธ์Šค ํ† ํฐ๊ณผ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์„ ๋ฐœ๊ธ‰
    • ์•ก์„ธ์Šค ํ† ํฐ: ์œ ํšจ๊ธฐ๊ฐ„ ์งง๊ฒŒ ์„ค์ • (๋ณด์•ˆ ๊ฐ•ํ™”)
    • ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ: ์ƒˆ ํ† ํฐ์„ ๋ฐœ๊ธ‰๋ฐ›์„ ๋•Œ ์‚ฌ์šฉ
  • Spring Security์˜ ํ•„ํ„ฐ ์ฒด์ธ์„ ์„ค์ •ํ•ด
  • JWT ํ† ํฐ ๊ฒ€์ฆ ๊ณผ์ •์—์„œ ์‚ฌ์šฉ์ž ๊ถŒํ•œ(ROLE_USER, ROLE_ADMIN)์„ ํ™•์ธํ•ด ๊ธฐ๋Šฅ๋ณ„ ์ ‘๊ทผ ์ œ์–ด
  • ๋กœ๊ทธ์•„์›ƒ ์‹œ ํ† ํฐ ๋ธ”๋ž™๋ฆฌ์ŠคํŠธ ์ฒ˜๋ฆฌ๋ฅผ ํ†ตํ•ด ํ† ํฐ ์žฌ ์‚ฌ์šฉ์„ ๋ฐฉ์ง€

5. ํ–ฅํ›„ ๊ณ ๋ ค ์‚ฌํ•ญ

  • ํ† ํฐ ๊ฐฑ์‹  ๊ณผ์ •์—์„œ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ์žฌ์‚ฌ์šฉ ๋ฐฉ์ง€ ๋กœ์ง์„ ๊ฐ•ํ™”ํ•  ํ•„์š” ์žˆ์Œ
  • ์ถ”ํ›„ ์™ธ๋ถ€ ์„œ๋น„์Šค ์—ฐ๋™ ์‹œ OAuth2 / OIDC๋กœ ํ™•์žฅ ๊ฐ€๋Šฅ์„ฑ ๊ณ ๋ ค
๐Ÿ’ก Query DSL์„ ํ†ตํ•œ ์ƒํ’ˆ ๊ฒ€์ƒ‰

1. ๋ฐฐ๊ฒฝ

์ƒํ’ˆ ์ „์ฒด ์กฐํšŒ ์‹œ ์—ฐ๊ด€๋œ ์ด๋ฏธ์ง€์™€ ํƒœ๊ทธ๋ฅผ N+1 ๋ฌธ์ œ ์—†์ด fetch join์œผ๋กœ ๊ฐ€์ ธ์˜ค๋ฉด์„œ, ๋™์‹œ์— Pageable๋กœ ํŽ˜์ด์ง• ์ฒ˜๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ ์ƒํ’ˆ๊ณผ ์ด๋ฏธ์ง€๊ฐ€ @OneToMany ๊ด€๊ณ„๋กœ:

  • ์กฐ์ธ ๊ฒฐ๊ณผ๊ฐ€ ๊ณฑํ•ด์ ธ ์ค‘๋ณต row ๋ฐœ์ƒ โ†’ JSON ์‘๋‹ต์— ๋ถˆํ•„์š”ํ•œ ์ค‘๋ณต ๊ฐ’ ํฌํ•จ
  • DB ๋ ˆ๋ฒจ์—์„œ LIMIT, OFFSET ์ ์šฉ ๋ถˆ๊ฐ€ โ†’ Hibernate๊ฐ€ Java ๋ฉ”๋ชจ๋ฆฌ์—์„œ ์ค‘๋ณต ์ œ๊ฑฐ
  • ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ ์‹œ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์ฆ๊ฐ€, ์„ฑ๋Šฅ ์ €ํ•˜ ์šฐ๋ ค

๋“ฑ์˜ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.


2. ์š”๊ตฌ์‚ฌํ•ญ

  • ์—ฐ๊ด€ ์—”ํ‹ฐํ‹ฐ ์ค‘๋ณต ์—†์ด ์กฐํšŒ
  • DB ๋ ˆ๋ฒจ์—์„œ ํŽ˜์ด์ง• ์ ์šฉ
  • N+1 ๋ฌธ์ œ ๋ฐฉ์ง€
  • ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ์—์„œ๋„ ์„ฑ๋Šฅ ๋ฌธ์ œ ๋ฐฉ์ง€

3. ์˜์‚ฌ๊ฒฐ์ •

๋‹จ๊ณ„๋ณ„๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ์กฐํšŒ

  1. ์กฐ๊ฑด์— ๋งž๋Š” Item์˜ ID๋งŒ ๋จผ์ € ํŽ˜์ด์ง• ์ฒ˜๋ฆฌํ•˜์—ฌ ์กฐํšŒ
  2. ํ•ด๋‹น ID ๋ฆฌ์ŠคํŠธ ๊ธฐ์ค€์œผ๋กœ ์—ฐ๊ด€๋œ Tag, Image๋ฅผ fetch joinํ•˜์—ฌ ์กฐํšŒ
  3. ๋ณ„๋„ count ์ฟผ๋ฆฌ๋ฅผ ์ž‘์„ฑํ•˜์—ฌ ์ „์ฒด ํŽ˜์ด์ง€ ์ˆ˜ ๊ณ„์‚ฐ
  4. ์ตœ์ข…์ ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐ๋ฆฝํ•˜์—ฌ Page<ItemDto> ํ˜•ํƒœ๋กœ ๋ฐ˜ํ™˜

๋‹จ๊ณ„๋ณ„๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ์กฐํšŒํ•˜๋Š” ์ด์œ 

  • ์ค‘๋ณต row ๋ฐฉ์ง€: fetch join์œผ๋กœ ์—ฌ๋Ÿฌ ์ปฌ๋ ‰์…˜์„ ํ•œ ๋ฒˆ์— ์กฐํšŒํ•˜๋ฉด (Item ร— Tag ร— Image)์ฒ˜๋Ÿผ ๊ณฑํ•ด์ ธ ์ค‘๋ณต ๋ฐœ์ƒ
  • DB ๋ ˆ๋ฒจ ํŽ˜์ด์ง• ์ ์šฉ: ID๋งŒ ๋จผ์ € ์กฐํšŒํ•˜๋ฉด LIMIT, OFFSET์ด ์ •ํ™•ํžˆ ๋™์ž‘
  • ์„ฑ๋Šฅ ์ตœ์ ํ™”: ํŽ˜์ด์ง€์— ํ•„์š”ํ•œ Item์— ๋Œ€ํ•ด์„œ๋งŒ ์กฐํšŒํ•˜๋ฏ€๋กœ, ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ์—์„œ๋„ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰๊ณผ ์ฒ˜๋ฆฌ ์†๋„๊ฐ€ ์ดˆ๊ธฐ ๋ฒ„์ „ ๋Œ€๋น„ ํšจ์œจ์ 
  • ID ๊ธฐ๋ฐ˜ ์กฐํšŒ ์žฅ์ : IN์ ˆ ์กฐํšŒ๋Š” ๊ธฐ๋ณธ ์—”ํ‹ฐํ‹ฐ ์ค‘์‹ฌ ์ฟผ๋ฆฌ๋ผ JPA๊ฐ€ ๊ฐ™์€ Item ID๋ฅผ ๊ธฐ์ค€์œผ๋กœ ํ•˜๋‚˜์˜ ๊ฐ์ฒด๋กœ ๋ฌถ์–ด ์—ฐ๊ด€ ์ปฌ๋ ‰์…˜์„ ์ถ”๊ฐ€ โ†’ row ์ˆ˜ ์ฆ๊ฐ€ ์—†์ด ์•ˆ์ „ํ•˜๊ฒŒ ์กฐํšŒ ๊ฐ€๋Šฅ

QueryDSL์„ ๋„์ž…ํ•œ ์ด์œ 

  • ํŽ˜์ด์ง•, ๋™์  where์ ˆ, ์ •๋ ฌ ๋“ฑ ๋ณต์žกํ•œ ์—ฌ๋Ÿฌ ์ฟผ๋ฆฌ๋“ค์„ ํ•˜๋‚˜์˜ ๋ฉ”์„œ๋“œ์— ์‘์ง‘์‹œํ‚ฌ ์ˆ˜ ์žˆ์Œ
  • ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ๊ณ„์ธต์—์„œ ์กฐํšŒ ์ฑ…์ž„์„ ๋ช…ํ™•ํ•˜๊ฒŒ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด, ์œ ์ง€๋ณด์ˆ˜์„ฑ๊ณผ ํ™•์žฅ์„ฑ ์ธก๋ฉด์—์„œ๋„ ์œ ๋ฆฌํ•˜๋‹ค๊ณ  ํŒ๋‹จ

4. ๊ตฌํ˜„ ์ƒ์„ธ

ItemRepositoryCustomImpl

@RequiredArgsConstructor
public class ItemRepositoryCustomImpl implements ItemRepositoryCustom {

	private final JPAQueryFactory queryFactory;

	@Override
	public Page<Item> search(String keyword, List<String> tags, Long userId, Pageable pageable) {

		// ์ƒํ’ˆ ์กฐํšŒ๋ฅผ ์œ„ํ•œ where์ ˆ
		BooleanExpression whereCondition = null;

		// ํ‚ค์›Œ๋“œ๋ฅผ ์ „๋‹ฌ ๋ฐ›์•˜๋‹ค๋ฉด ํ•ด๋‹น ํ‚ค์›Œ๋“œ๊ฐ€ ์ œ๋ชฉor์„ค๋ช…์— ํฌํ•จ๋œ ์ƒํ’ˆ์„ ์กฐํšŒ
		if (keyword != null) {
			BooleanExpression keywordCondition = item.itemName.likeIgnoreCase("%" + keyword + "%")
				.or(item.itemDescription.likeIgnoreCase("%" + keyword + "%"));
			whereCondition = keywordCondition;
		}

		// ํƒœ๊ทธ๋ฅผ ์ „๋‹ฌ ๋ฐ›์•˜๋‹ค๋ฉด ํ•ด๋‹น ํƒœ๊ทธ๊ฐ€ ์กด์žฌํ•˜๋Š” ์ƒํ’ˆ์„ ์กฐํšŒ
		if (tags != null && !tags.isEmpty()) {
			BooleanExpression tagCondition = tag.tagName.in(tags);
			whereCondition = (whereCondition == null) ? tagCondition : whereCondition.or(tagCondition);
		}

		// ์œ ์ € id๋ฅผ ์ „๋‹ฌ ๋ฐ›์•˜๋‹ค๋ฉด ํ•ด๋‹น ํŒ๋งค์ž๊ฐ€ ํ•ด๋‹น id์™€ ์ผ์น˜ํ•˜๋Š” ์ƒํ’ˆ์„ ์กฐํšŒ
		if (userId != null) {
			BooleanExpression userCondition = item.user.id.eq(userId);
			whereCondition = (whereCondition == null) ? userCondition : whereCondition.and(userCondition);
		}

		return buildPagedItem(whereCondition, pageable);
	}

	private Page<Item> buildPagedItem(BooleanExpression whereCondition, Pageable pageable) {

		// ์ •๋ ฌ ์กฐ๊ฑด
		OrderSpecifier<?> orderSpecifier = getOrderSpecifiers(pageable);

		// ํŽ˜์ด์ง€์˜ id ์กฐํšŒ
		List<Long> itemIds = queryFactory
			.select(item.id)
			.from(item)
			.join(item.itemTags, itemTag)
			.join(itemTag.tag, tag)
			.where(
				item.isDeleted.isFalse(),
				whereCondition)
			.orderBy(orderSpecifier)
			.offset(pageable.getOffset())
			.limit(pageable.getPageSize())
			.fetch();

		if (itemIds.isEmpty()) {
			return new PageImpl<>(Collections.emptyList(), pageable, 0);
		}

		// ํŽ˜์ด์ง€์˜ ์ƒํ’ˆ๋“ค ์ •๋ณด ์กฐํšŒ
		List<Item> items = queryFactory
			.selectFrom(item)
			.distinct()
			.join(item.user).fetchJoin()
			.join(item.itemTags, itemTag).fetchJoin()
			.join(itemTag.tag, tag).fetchJoin()
			.join(item.images).fetchJoin()
			.where(item.id.in(itemIds))
			.orderBy(orderSpecifier)
			.fetch();

		// ํŽ˜์ด์ง• ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ์ „์ฒด ์—ด ์นด์šดํŠธ
		long total = Optional.ofNullable(
			queryFactory
				.select(item.countDistinct()) // fetchJoin ์—†์ด count ํ•˜๊ธฐ์œ„ํ•ด distinct
				.from(item)
				.join(item.itemTags, itemTag)
				.join(itemTag.tag, tag)
				.where(
					item.isDeleted.isFalse(),
					whereCondition)
				.fetchOne()
		).orElse(0L);

		return new PageImpl<>(items, pageable, total);
	}

	private OrderSpecifier<?> getOrderSpecifiers(Pageable pageable) {
		// Query DSL ํ•„๋“œ ์ ‘๊ทผ ๊ฒฝ๋กœ(path) ์ง€์ • ๋„๊ตฌ
		PathBuilder<Item> path = new PathBuilder<>(Item.class, "item");

		// ํ•„ํ„ฐ๋งํ•ด์„œ createdAt ์ •๋ ฌ๋งŒ ๋ฐ›๊ณ , ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’(์ตœ์‹ ์ˆœ) ์„ธํŒ…
		Sort.Order firstOrder = pageable.getSort().stream()
			.filter(order -> "createdAt".equals(order.getProperty()))
			.findFirst()
			.orElse(new Sort.Order(Sort.Direction.DESC, "createdAt"));

		// pageable์˜ ์ •๋ ฌ ๊ฐ’์„ Query DSL์— ์ ์šฉ๊ฐ€๋Šฅํ•œ ํ˜•ํƒค ๋ณ€ํ™˜ ("createdAt,desc" -> item.createdAt.desc())
		return new OrderSpecifier<>(
			firstOrder.isAscending() ? Order.ASC : Order.DESC,               // .asc()/.desc() ํŒ๋ณ„
			path.getComparable(firstOrder.getProperty(), Comparable.class)); // "createdAt" -> item.createdAt
	}
}

ItemService

@Slf4j
@Service
@RequiredArgsConstructor
public class ItemService {
  private final ItemRepository itemRepository;

	@Loggable
	@Transactional
	public PageResponseDto<ItemSummaryResponse> searchItems(String keyword, List<String> tags, Long userId,
		Pageable pageable) {

		Page<Item> pagedItems = itemRepository.search(keyword, tags, userId, pageable);

		Page<ItemSummaryResponse> response = pagedItems.map(ItemSummaryResponse::from);

		return new PageResponseDto<>(response);
	}
}

5. ํ–ฅํ›„ ๊ณ ๋ ค ์‚ฌํ•ญ

ํ˜„์žฌ ๊ฐ•์ 

  • ์ •ํ™•ํ•œ ํŽ˜์ด์ง• + ์—ฐ๊ด€ ์ •๋ณด ํ•œ๋ฒˆ์— ์กฐํšŒ
  • fetch join์œผ๋กœ N+1 ๋ฌธ์ œ ํ•ด๊ฒฐ
  • ๋™์  ์กฐ๊ฑด ๋ฐ ์ •๋ ฌ ๋“ฑ ์ฟผ๋ฆฌ ์ตœ์ ํ™” ์œ ๋ฆฌ

์ œ์•ฝ ์‚ฌํ•ญ

  • ์ฟผ๋ฆฌ 3๋ฒˆ ํ•„์š”
  • ์ •๋ ฌ ์œ ์ง€๋‚˜ ์ค‘๋ณต ์ œ๊ฑฐ ๋“ฑ ์ฟผ๋ฆฌ ์ž‘์„ฑ ์‹œ ์ฃผ์˜ ํ•„์š”
  • ์ฑ…์ž„ ๋ถ„๋ฆฌ ์„ค๊ณ„๊ฐ€ ์–ด๋ ค์›€
๐Ÿ’ก ์ƒํ’ˆ ๊ฒ€์ƒ‰๊ณผ ์ด๋ฏธ์ง€ ์กฐํšŒ ๋กœ์ง ๋ถ„๋ฆฌ

1. ๋ฐฐ๊ฒฝ

QueryDSL์„ ์‚ฌ์šฉํ•˜์—ฌ ์ƒํ’ˆ ์กฐํšŒ ์‹œ ์—ฐ๊ด€ ์ •๋ณด๋ฅผ ํ•œ ๋ฒˆ์— ์กฐํšŒํ•˜๋ฉด์„œ ํŽ˜์ด์ง•์ด ์˜๋„๋Œ€๋กœ ์ž‘๋™ํ•˜๋„๋ก ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜, ์ƒํ’ˆ์— ์ข…์†๋œ ํƒœ๊ทธ์™€ ๋‹ฌ๋ฆฌ ์ด๋ฏธ์ง€๋Š” ์ƒํ’ˆ๋ฟ ์•„๋‹ˆ๋ผ ์—ฌ๋Ÿฌ ๋„๋ฉ”์ธ์—์„œ ์žฌ์‚ฌ์šฉ๋  ์ˆ˜ ์žˆ์–ด, ์ด๋ฏธ์ง€๋ฅผ ์ƒํ’ˆ๊ณผ ํ•œ ๊ฐ์ฒด๋กœ ๋ณผ ์ˆ˜ ์žˆ์„์ง€ ๊ณ ๋ฏผ์ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.


2. ์š”๊ตฌ์‚ฌํ•ญ

  • ์—ฌ๋Ÿฌ ๊ฐ์ฒด ๊ฐ„์˜ ๋กœ์ง ๋ถ„๋ฆฌํ•˜์—ฌ ์ฑ…์ž„ ๋ช…ํ™•ํ™”
  • ์—ฐ๊ด€ ์—”ํ‹ฐํ‹ฐ ์ค‘๋ณต ์—†์ด ์กฐํšŒ
  • DB ๋ ˆ๋ฒจ์—์„œ ํŽ˜์ด์ง• ์ ์šฉ
  • N+1 ๋ฌธ์ œ ๋ฐฉ์ง€
  • ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ์—์„œ๋„ ์„ฑ๋Šฅ ๋ฌธ์ œ ๋ฐฉ์ง€

3. ์˜์‚ฌ๊ฒฐ์ •

์ด๋ฏธ์ง€ ์กฐํšŒ ๋กœ์ง ๋ถ„๋ฆฌ

  1. ์ƒํ’ˆ ์—”ํ‹ฐํ‹ฐ ํŽ˜์ด์ง•์œผ๋กœ ์กฐํšŒ
  2. ํ•ด๋‹น ํŽ˜์ด์ง€์˜ ์ƒํ’ˆ ID ๋ฆฌ์ŠคํŠธ ์ถ”์ถœ
  3. ์—ฐ๊ด€ ์ •๋ณด(์ด๋ฏธ์ง€)๋ฅผ ID ๋ฆฌ์ŠคํŠธ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ณ„๋„ ์กฐํšŒํ•˜์—ฌ Map์œผ๋กœ ๊ทธ๋ฃนํ•‘
  4. ๋ฐ˜ํ™˜๋œ ์—ฐ๊ด€ ์ •๋ณด์™€ ํ•จ๊ป˜ ์‘๋‹ต๊ฐ’ ์ƒ์„ฑ

์ด๋ฏธ์ง€ ๋กœ์ง์„ ๋ถ„๋ฆฌํ•œ ์ด์œ 

  • ํ•œ ๋ฒˆ์— ์ด๋ฏธ์ง€๋ฅผ ์กฐํšŒํ•˜๋ฉด ์ฟผ๋ฆฌ ์ˆ˜๊ฐ€ ์ ๊ณ  ์กฐํšŒ ์†๋„๊ฐ€ ๋น ๋ฅด๋‹ค๋Š” ์žฅ์ ์€ ์ธ์ง€
  • ๊ทธ๋Ÿฌ๋‚˜ ์ด๋ฏธ์ง€๋Š” ์—ฌ๋Ÿฌ ๋„๋ฉ”์ธ์—์„œ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅ โ†’ ์ƒํ’ˆ๊ณผ ํ•จ๊ป˜ ์กฐํšŒํ•˜๋ฉด ์ฑ…์ž„ ๋ถ„๋ฆฌ ์–ด๋ ค์›€
  • ์ด๋ฏธ์ง€์˜ ์ €์žฅยท์ˆ˜์ •ยท์‚ญ์ œ ๋กœ์ง์€ ์ด๋ฏธ์ง€ ์„œ๋น„์Šค์—์„œ ๊ด€๋ฆฌ โ†’ ๊ด€๋ฆฌ ์ผ๊ด€์„ฑ ํ™•๋ณด ํ•„์š”
  • ๊ตฌ์กฐ ๋ณต์žก์„ฑ์„ ๋ฐฉ์ง€ํ•˜๊ณ , ์—ฌ๋Ÿฌ ๋„๋ฉ”์ธ์—์„œ ์š”์ฒญ ์‹œ ๋™์ผํ•œ ๊ตฌ์กฐ๋ฅผ ์œ ์ง€ํ•˜๋ฉด ์ฝ”๋“œ ์ดํ•ด๋„ ํ–ฅ์ƒ๋  ๊ฒƒ์ด๋ผ ํŒ๋‹จ

4. ๊ตฌํ˜„ ์ƒ์„ธ

ItemRepositryCustomImpl

@RequiredArgsConstructor
public class ItemRepositoryCustomImpl implements ItemRepositoryCustom {
	. . . (๊ธฐ๋ณธ์ ์ธ ์‚ฌํ•ญ๋“ค์€ ์œ„์˜ ๊ตฌํ˜„ ๋‚ด์šฉ๊ณผ ๋™์ผ)
	
	private Page<Item> buildPagedItem(BooleanExpression whereCondition, Pageable pageable) {
		// ์ •๋ ฌ ์กฐ๊ฑด
		. . .

		// ํŽ˜์ด์ง€์˜ id ์กฐํšŒ
		. . .

		// ํŽ˜์ด์ง€์˜ ์ƒํ’ˆ๋“ค ์ •๋ณด ์กฐํšŒ <- '.join(item.images).fetchJoin()' ์ œ๊ฑฐ
		List<Item> items = queryFactory
			.selectFrom(item)
			.distinct()
			.join(item.user).fetchJoin()
			.join(item.itemTags, itemTag).fetchJoin()
			.join(itemTag.tag, tag).fetchJoin()
			.join(item.images).fetchJoin()
			.where(item.id.in(itemIds))
			.orderBy(orderSpecifier)
			.fetch();

		// ํŽ˜์ด์ง• ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ์ „์ฒด ์—ด ์นด์šดํŠธ
		. . .

		return new PageImpl<>(items, pageable, total);
	}
}

ItemService

@Slf4j
@Service
@RequiredArgsConstructor
public class ItemService {
  private final ItemRepository itemRepository;
  private final ImageService imageService;
	
	@Loggable
	@Transactional(readOnly = true)
	public PageResponseDto<ItemGetAllResponse> searchItems(String keyword, List<String> tags, Long userId,
		Pageable pageable) {

		searchKeywordService.addKeyword(keyword);

		Page<Item> pagedItems = itemRepository.search(keyword, tags, userId, pageable);

		return buildGetAllItems(pagedItems);
	}

	// ํ•ด๋‹น ํŽ˜์ด์ง€ ์ƒํ’ˆ์˜ ์ด๋ฏธ์ง€๋ฅผ ๋งตํ•‘ํ•˜์—ฌ ๋ฐ˜ํ™˜
	@Transactional(readOnly = true)
	protected PageResponseDto<ItemGetAllResponse> buildGetAllItems(Page<Item> pagedItems) {
		// ํ•ด๋‹น ๊ฒŒ์‹œ๊ธ€ ID ๋ชฉ๋ก์— ๋Œ€ํ•œ ์ด๋ฏธ์ง€ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜
		Map<Long, Image> mainImagesMap = imageService.getMainImagesForItems(pagedItems);

		// ์กฐํ•ฉ
		Page<ItemGetAllResponse> dtoPage = pagedItems.map(item -> {
			// --๋ฉ”์ธ์ด๋ฏธ์ง€ ์กฐํ•ฉ
			Image mainImage = mainImagesMap.getOrDefault(item.getId(), null);

			return ItemGetAllResponse.from(item, mainImage.getImageUrl());
		});

		return new PageResponseDto<>(dtoPage);
	}
}

ImageService

@Service
@RequiredArgsConstructor
public class ImageService {

	private final ImageRepository imageRepository;

	// ํ˜„์žฌ ์ƒํ’ˆ ํŽ˜์ด์ง€์— ์žˆ๋Š” ์ƒํ’ˆ์˜ ์ด๋ฏธ์ง€๋“ค์„ ๋งต์œผ๋กœ ๋ฌถ์–ด ๋ฐ˜ํ™˜
	@Transactional(readOnly = true)
	public Map<Long, Image> getMainImagesForItems(Page<Item> pagedItems) {
		// ํ˜„์žฌ ํŽ˜์ด์ง€์— ์กด์žฌํ•˜๋Š” itemId ๋ฆฌ์ŠคํŠธ
		List<Long> itemIds = pagedItems.getContent().stream()
			.map(Item::getId)
			.distinct()
			.collect(Collectors.toList());

		if (itemIds.isEmpty()) {
			return Collections.emptyMap();
		}

		// ํ•ด๋‹น ์ƒํ’ˆ๋“ค์˜ ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ ์กฐํšŒ
		List<Image> mainImages = imageRepository.findMainImagesByItemIds(itemIds);

		// ๊ฒฐ๊ณผ ๋ฆฌ์ŠคํŠธ๋ฅผ itemId๋ฅผ ํ‚ค๋กœ ํ•˜๋Š” Map์œผ๋กœ ๋ณ€ํ™˜
		return mainImages.stream()
			.collect(Collectors.toMap(
				img -> img.getItem().getId(),
				img -> img
			));
	}
}

5. ํ–ฅํ›„ ๊ณ ๋ ค ์‚ฌํ•ญ

ํ˜„์žฌ ๊ฐ•์ 

  • ์ด๋ฏธ์ง€ ๊ด€๋ จ ๋กœ์ง์„ ๋…๋ฆฝ์ ์œผ๋กœ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด ์œ ์ง€๋ณด์ˆ˜์„ฑ์ด ๋†’์Œ
  • ์—ฌ๋Ÿฌ ๋„๋ฉ”์ธ์—์„œ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅ
  • ์ƒํ’ˆ ๋กœ์ง๊ณผ ํ˜ผํ•ฉ๋˜์ง€ ์•Š์•„ ๋„๋ฉ”์ธ ๊ตฌ์กฐ ์ž์ฒด๋Š” ๋ช…ํ™•

์ œ์•ฝ ์‚ฌํ•ญ

  • ๊ฐœ์„ ํ•œ ๊ตฌ์กฐ๋Š” ์—ฐ๊ด€ ๊ฐ์ฒด๊ฐ€ ๋งŽ์•„์งˆ์ˆ˜๋ก ์ถ”๊ฐ€ ์กฐํšŒ ์ฟผ๋ฆฌ ์ˆ˜ ์ฆ๊ฐ€
  • ๊ฒฐ๊ณผ๋ฅผ ์ง์ ‘ ์กฐ๋ฆฝํ•ด์•ผ ํ•ด์„œ ์ฝ”๋“œ ๋ณต์žก๋„๊ฐ€ ์ƒ์Šน
  • ์กฐํšŒ ์ฟผ๋ฆฌ์™€ ๋ฐ์ดํ„ฐ ๋งคํ•‘์ด ๋ถ„์‚ฐ๋˜์–ด ์‹ค์ œ ์กฐํšŒ์˜ ๋ฐ์ดํ„ฐ ํ๋ฆ„ ํŒŒ์•…์ด ์–ด๋ ค์šธ ์ˆ˜ ์žˆ์Œ
๐Ÿ’ก ๊ฒ€์ƒ‰์–ด ๋žญํ‚น ๊ธฐ๋Šฅ Redis (Zset) ๋„์ž…

1. ๋ฐฐ๊ฒฝ

๊ธฐ์กด ์ธ๊ธฐ ๊ฒ€์ƒ‰์–ด ๋žญํ‚น ๊ธฐ๋Šฅ์€ RDB ๊ธฐ๋ฐ˜์œผ๋กœ ๊ตฌํ˜„๋˜์–ด ์žˆ์—ˆ์œผ๋‚˜,

๋Œ€๋Ÿ‰์˜ ์‹ค์‹œ๊ฐ„ ์“ฐ๊ธฐ์™€ ์ •๋ ฌ/์ง‘๊ณ„ ์—ฐ์‚ฐ์ด ๋™์‹œ์— ๋ฐœ์ƒํ•˜๋ฉด์„œ ์„ฑ๋Šฅ ์ €ํ•˜์™€ DB ์ปค๋„ฅ์…˜ ํ’€ ๊ณ ๊ฐˆ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜์˜€์Šต๋‹ˆ๋‹ค.

์ด์— ๋”ฐ๋ผ ์‹ค์‹œ๊ฐ„ ์ง‘๊ณ„์™€ ๋น ๋ฅธ ์กฐํšŒ๋ฅผ ๋™์‹œ์— ๋งŒ์กฑ์‹œํ‚ฌ ์ˆ˜ ์žˆ๋Š” ๋Œ€์•ˆ์ด ํ•„์š”ํ–ˆ์Šต๋‹ˆ๋‹ค.


2. ์š”๊ตฌ์‚ฌํ•ญ

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

3. ์˜์‚ฌ๊ฒฐ์ •

ํ•ด๊ฒฐ ๋ฐฉ์•ˆ์œผ๋กœ๋Š” RDB ํŠœ๋‹, ์ธ๋ฉ”๋ชจ๋ฆฌ ์บ์‹œ, NoSQL ๊ณ„์—ด DB ๋„์ž…์ด ๊ฒ€ํ† ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

๋Œ€์•ˆ ์žฅ์  ํ•œ๊ณ„
๊ธฐ์กด RDB ํŠœ๋‹ (์ฟผ๋ฆฌ ์ตœ์ ํ™”, ์ธ๋ฑ์Šค, ์ปค๋„ฅ์…˜ ํ’€ ํ™•๋Œ€, ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ) ์ถ”๊ฐ€ ์ธํ”„๋ผ ์—†์ด ๊ฐœ์„  ๊ฐ€๋Šฅ, ์•ˆ์ •์ ์ธ ์šด์˜ ๊ฒฝํ—˜ ๋Œ€๋Ÿ‰ ์‹ค์‹œ๊ฐ„ ์“ฐ๊ธฐ ์‹œ ๋ฌผ๋ฆฌ์  ํ•œ๊ณ„, ์ •๋ ฌ/์ง‘๊ณ„ ์—ฐ์‚ฐ ๋ถ€ํ•˜ ์ง€์†
์ธ๋ฉ”๋ชจ๋ฆฌ ์บ์‹œ (Caffeine ๋“ฑ) ๋‹จ์ผ ์„œ๋ฒ„ ํ™˜๊ฒฝ์—์„œ ๋งค์šฐ ๋น ๋ฅธ ์กฐํšŒ ์†๋„ ๋‹ค์ค‘ ์„œ๋ฒ„ ํ™˜๊ฒฝ ๋™๊ธฐํ™” ์–ด๋ ค์›€, ์„œ๋ฒ„ ์žฌ์‹œ์ž‘ ์‹œ ๋ฐ์ดํ„ฐ ์œ ์‹ค, ๋žญํ‚น ์ •๋ ฌ ๋กœ์ง ์ง์ ‘ ๊ตฌํ˜„ ํ•„์š”
NoSQL ๊ณ„์—ด (MongoDB, Cassandra, Redis ๋“ฑ) ๊ณ ์„ฑ๋Šฅ ์“ฐ๊ธฐ ์ฒ˜๋ฆฌ, ์ผ๋ถ€ ์ œํ’ˆ์€ TTLยท์ •๋ ฌ ์ง€์› ๋Œ€๋ถ€๋ถ„ ๋žญํ‚น ๋กœ์ง ๋ณ„๋„ ๊ตฌํ˜„ ํ•„์š”, ์ƒˆ๋กœ์šด DB ์šด์˜ ๋ถ€๋‹ด

๊ทธ์ค‘ Redis๋ฅผ ์„ ํƒํ•œ ์ด์œ :

  • ์ด๋ฏธ ์„œ๋น„์Šค ๋‚ด ๋‹ค๋ฅธ ๊ธฐ๋Šฅ์—์„œ Redis๋ฅผ ์šด์˜ ์ค‘ โ†’ ์ถ”๊ฐ€ ํ•™์Šตยท์šด์˜ ๋ถ€๋‹ด ์ตœ์†Œํ™”
  • Redis์˜ ZSet ์ž๋ฃŒ๊ตฌ์กฐ๋ฅผ ํ†ตํ•ด ๋žญํ‚น ์ •๋ ฌ์„ ๋ณ„๋„ ๊ตฌํ˜„ ์—†์ด ์ง€์›
  • ์ธ๋ฉ”๋ชจ๋ฆฌ ์บ์‹œ์˜ ์žฅ์ ์„ ํฌํ•จํ•˜๋ฉด์„œ๋„ NoSQL DB์˜ ์„ฑ๋Šฅ์  ์ด์ ์„ ํ™œ์šฉ ๊ฐ€๋Šฅ

4. ๊ตฌํ˜„ ์ƒ์„ธ

  1. ZSet(์ •๋ ฌ ์ง‘ํ•ฉ) ํ™œ์šฉ
    • ์ ์ˆ˜ ๊ธฐ๋ฐ˜ ์ž๋™ ์ •๋ ฌ ๋ฐ ์ƒ์œ„ N๊ฐœ ๊ฒ€์ƒ‰์–ด๋ฅผ ๋น ๋ฅด๊ฒŒ ์กฐํšŒ ๊ฐ€๋Šฅ
    • ๋ณ„๋„ ๋žญํ‚น ๊ตฌํ˜„ ํ•„์š” ์—†์Œ
  2. ์“ฐ๊ธฐยท์ฝ๊ธฐ ์„ฑ๋Šฅ ์ตœ์ ํ™”
    • ์ดˆ๋‹น ์ˆ˜์‹ญ๋งŒ ๊ฑด ์ˆ˜์ค€์˜ ์“ฐ๊ธฐ ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ
    • DB ์ปค๋„ฅ์…˜ ํ’€ ๊ณ ๊ฐˆ ๋ฌธ์ œ ๊ทผ๋ณธ์  ํ•ด๊ฒฐ
  3. ์šด์˜ ํšจ์œจ์„ฑ
    • ๊ธฐ์กด Redis ์šด์˜ ๊ฒฝํ—˜ ์žฌํ™œ์šฉ
    • ์ƒˆ๋กœ์šด ํ•™์Šต ๋ฐ ์šด์˜ ๋ถ€๋‹ด ์ตœ์†Œํ™”

5. ํ–ฅํ›„ ๊ณ ๋ ค ์‚ฌํ•ญ

  • Redis ์žฅ์•  ๋Œ€์‘: ๋‹จ์ผ ์ธ์Šคํ„ด์Šค ์žฅ์•  ์‹œ ๋ฐ์ดํ„ฐ ์œ ์‹ค ๊ฐ€๋Šฅ โ†’ ํด๋Ÿฌ์Šคํ„ฐ๋ง ๋ฐ ์˜์†ํ™” ์˜ต์…˜(RDB, AOF) ๊ฒ€ํ†  ํ•„์š”
  • ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ: ์ธ๋ฉ”๋ชจ๋ฆฌ ํŠน์„ฑ์ƒ ๋ฐ์ดํ„ฐ ํฌ๊ธฐ ๊ด€๋ฆฌ ํ•„์ˆ˜ โ†’ TTL ๋ฐ ๋ฐ์ดํ„ฐ ์ •๋ฆฌ ์ •์ฑ… ์ ์šฉ ํ•„์š”
  • ํ™•์žฅ์„ฑ ๊ณ ๋ ค: ํ–ฅํ›„ ํŠธ๋ž˜ํ”ฝ ๊ธ‰์ฆ ์‹œ Redis Cluster๋กœ ์ˆ˜ํ‰ ํ™•์žฅ ๊ฐ€๋Šฅ์„ฑ ํ™•๋ณด
๐Ÿ’ก ์ƒํ’ˆ ์ฃผ๋ฌธ ๋™์‹œ์„ฑ ์ œ์–ด

1. ๋ฐฐ๊ฒฝ

๋ฌธ์ œ ์ƒํ™ฉ

  • ๋‹ค์ค‘ ์‚ฌ์šฉ์ž ํ™˜๊ฒฝ์—์„œ ๋™์ผํ•œ ๋ฆฌ์†Œ์Šค์— ๋Œ€ํ•œ ๋™์‹œ ์ ‘๊ทผ์œผ๋กœ ์ธํ•œ ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ ๋ฌธ์ œ ๋ฐœ์ƒ
  • ํŠธ๋ž˜ํ”ฝ ์ฆ๊ฐ€์— ๋”ฐ๋ฅธ ๋™์‹œ์„ฑ ์ด์Šˆ ๋นˆ๋ฐœ๋กœ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์˜ ์ •ํ™•์„ฑ ๋ณด์žฅ ํ•„์š”

๋น„์ฆˆ๋‹ˆ์Šค ์ž„ํŒฉํŠธ

  • ๋ฐ์ดํ„ฐ ๋ถˆ์ผ์น˜๋กœ ์ธํ•œ ๊ณ ๊ฐ ๋ถˆ๋งŒ ๋ฐ ์‹ ๋ขฐ๋„ ํ•˜๋ฝ
  • ์žฌ๊ณ  ์˜ค๋ฒ„์…€๋ง, ์ค‘๋ณต ์˜ˆ์•ฝ ๋“ฑ์˜ ์šด์˜์ƒ ๋ฌธ์ œ
  • ์‹œ์Šคํ…œ ์•ˆ์ •์„ฑ ๋ฐ ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ ํ™•๋ณด ํ•„์š”

2. ์š”๊ตฌ์‚ฌํ•ญ

๊ธฐ๋Šฅ์  ์š”๊ตฌ์‚ฌํ•ญ

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

3. ์˜์‚ฌ๊ฒฐ์ •

๋™์‹œ์„ฑ ์ œ์–ด ๋ฐฉ์‹ ์„ ํƒ

๋น„๊ด€๋ฝ, ๋‚™๊ด€๋ฝ ์žฅ์ 

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

๋น„๊ด€๋ฝ, ๋‚™๊ด€๋ฝ ๋‹จ์ 

ํŠน์„ฑ ๋น„๊ด€๋ฝ ๋‚™๊ด€๋ฝ
์„ฑ๋Šฅ - ๋‚ฎ์€ ๋™์‹œ์„ฑ
- ์ˆœ์ฐจ ์ฒ˜๋ฆฌ๋กœ ์ธํ•œ ๋Œ€๊ธฐ
- ๋†’์€ ์ถฉ๋Œ๋ฅ ์—์„œ ์„ฑ๋Šฅ์ €ํ•˜
- ์ง€์†์ ์ธ ์žฌ์‹œ๋„ ์˜ค๋ฒ„ ํ—ค๋“œ
- CPU ์‚ฌ์šฉ๋Ÿ‰ ์ฆ๊ฐ€
- ๋ถˆ์•ˆ์ •ํ•œ ์‘๋‹ต์‹œ๊ฐ„
๋ฆฌ์†Œ์Šค ์ ์œ  - DB ์ปค๋„ฅ์…˜ ์žฅ์‹œ๊ฐ„ ์ ์œ 
- ์ปค๋„ฅ์…˜ ํ’€ ๊ณ ๊ฐˆ ์œ„ํ—˜
- ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์ฆ๊ฐ€
- ์žฌ์‹œ๋„๋กœ ์ธํ•œ ๋ฆฌ์†Œ์Šค ๋‚ญ๋น„
- ๋ฒ„์ „ ๊ด€๋ฆฌ ์˜ค๋ฒ„ํ—ค๋“œ
- ๋ณต์žกํ•œ ์ƒํƒœ๊ด€๋ฆฌ
์‹œ์Šคํ…œ ์œ„ํ—˜ - ๋ฐ๋“œ๋ฝ ์œ„ํ—˜
- ๋ฝ ํƒ€์ž„์•„์›ƒ ์ฒ˜๋ฆฌ ๋ณต์žก
- ์žฅ์•  ์ „ํŒŒ ์œ„ํ—˜
- ๋ณต์žกํ•œ ์—๋Ÿฌ ์ฒ˜๋ฆฌ
- ๋ถ€๋ถ„ ์‹คํŒจ ์ƒํ™ฉ ๊ด€๋ฆฌ
- ๋ฐ์ดํ„ฐ ์ •ํ•ฉ์„ฑ ๊ฒ€์ฆ ๋ณต์žก
๊ตฌํ˜„ ๋ณต์žก๋„ - ํŠธ๋žœ์žญ์…˜ ๊ฒฝ๊ณ„๊ด€๋ฆฌ ๋ณต์žก
- ๋ฝ ๋ฒ”์œ„ ์„ค์ • ์–ด๋ ค์›€
- ์ค‘์ฒฉ ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ ๋ณต์žก
- ๋ณต์žกํ•œ ์žฌ์‹œ๋„ ๋กœ์ง
- ๋ฒ„์ „ ์ถฉ๋Œ ํ•ด๊ฒฐ ์ „๋žต ํ•„์š”
- ๋””๋ฒ„๊น… ๋ณต์žก์„ฑ

๋น„๊ด€๋ฝ์„ ์„ ํƒํ•œ ์ด์œ 

  1. ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ ๋ณด์žฅ
  • ์ค‘๊ณ ๊ฑฐ๋ž˜์—์„œ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ๊ฒƒ์€ โ€œ์ •ํ™•ํ•œ 1๊ฐœ ํŒ๋งคโ€ ์žฌ๊ณ  ๋ถˆ์ผ์น˜๋กœ ์ธํ•œ ๊ณ ๊ฐ๋ถˆ๋งŒ ์ฐจ๋‹จ
  1. ์ค‘๊ณ ์ƒํ’ˆ์˜ ํŠน์„ฑ : ์žฌ๊ณ  ํฌ์†Œ์„ฑ
  • ์ค‘๊ณ ์ƒํ’ˆ์€ ๋Œ€๋ถ€๋ถ„ 1๊ฐœ ํ•œ์ •ํŒ๋งค ์ด๊ธฐ๋•Œ๋ฌธ์— ๋‚™๊ด€๋ฝ์˜ ์žฅ์ ์ธ ๋†’์€ ๋™์‹œ์„ฑ์ด ๋ฌด์˜๋ฏธํ•จ
  • ๋‚™๊ด€๋ฝ์˜ ๋‹จ์ ์ธ ๋†’์€ ์ถฉ๋Œ๋ฅ ์— ์˜ํ•˜์—ฌ ์„ฑ๋Šฅ ์ €ํ•˜๊ฐ€ ๋ฐœ์ƒ
  1. ๊ตฌํ˜„ ๋‹จ์ˆœ์„ฑ
  • ๋น„๊ด€๋ฝ : ๋ณต์žกํ•œ ์žฌ์‹œ๋„ ๋กœ์ง ์—†์ด ๊ฐ„๋‹จํ•˜๊ฒŒ ๊ตฌํ˜„๊ฐ€๋Šฅ, ์œ ์ง€๋ณด์ˆ˜์„ฑ ํ–ฅ์ƒ
  • ๋‚™๊ด€๋ฝ : ์„ฑ๋Šฅ์€ ์ข‹์ง€๋งŒ ๋ณต์žกํ•œ ์žฌ์‹œ๋„ ๋กœ์ง
  1. ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ํ†ตํ•ฉ
  • ์žฌ๊ณ ํ™•์ธ, ์‚ฌ์šฉ์ž ๊ฒ€์ฆ, ๊ฒฐ์ œ ์ฒ˜๋ฆฌ๋ฅผ ํ•˜๋‚˜์˜ ํŠธ๋žœ์žญ์…˜์œผ๋กœ ์•ˆ์ „ํ•˜๊ฒŒ ์ฒ˜๋ฆฌ
  1. ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ๊ฐœ์„ 
  • ๊ตฌ๋งค ๊ฐ€๋Šฅ ์—ฌ๋ถ€๋ฅผ ๋ฏธ๋ฆฌ ํ™•์‹คํ•˜๊ฒŒ ํ™•์ธ๊ฐ€๋Šฅ

DB ๋น„๊ด€๋ฝ vs Redis ๋น„๊ด€๋ฝ(๋ถ„์‚ฐ๋ฝ)

๊ตฌ๋ถ„ DB ๋น„๊ด€๋ฝ Redis ๋น„๊ด€๋ฝ (๋ถ„์‚ฐ๋ฝ)
์žฅ์  - ACID ๋ณด์žฅ
- ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ ์™„๋ฒฝ
- ํŠธ๋žœ์žญ์…˜ ๋กค๋ฐฑ ์ง€์›
- DB ๋ถ€ํ•˜ ๋ถ„์‚ฐ
- ๋งค์šฐ ๋น ๋ฅธ ๋ฝ ์ฒ˜๋ฆฌ ์†๋„
- ํ™•์žฅ์„ฑ ์šฐ์ˆ˜
- ํƒ€์ž„์•„์›ƒ ์ œ์–ด ๊ฐ€๋Šฅ
๋‹จ์  - DB ์ปค๋„ฅ์…˜ ์ ์œ 
- ์ปค๋„ฅ์…˜ ํ’€ ๊ณ ๊ฐˆ ์œ„ํ—˜
- ๋†’์€ DB ๋ถ€ํ•˜
- ํ™•์žฅ์„ฑ ์ œํ•œ
- ์ถ”๊ฐ€ ์ธํ”„๋ผ ํ•„์š” (Redis ์„œ๋ฒ„)
- ๋„คํŠธ์›Œํฌ ์ง€์—ฐ/์žฅ์•  ์˜ํ–ฅ
- Redis ์žฅ์•  ์‹œ ์œ„ํ—˜
ํ™•์žฅ์„ฑ ๋‚ฎ์Œ (DB ์Šค์ผ€์ผ์—… ํ•„์š”) ๋†’์Œ (์ˆ˜ํ‰ ํ™•์žฅ ๊ฐ€๋Šฅ)
์ ํ•ฉํ•œ ๊ฒฝ์šฐ ๋‹จ์ผ DB ํ™˜๊ฒฝ, ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ์ด ์ตœ์šฐ์„ ์ผ ๋•Œ ๋‹ค์ค‘ ์„œ๋ฒ„ ํ™˜๊ฒฝ, ๋Œ€๊ทœ๋ชจ ํŠธ๋ž˜ํ”ฝ, ๊ณ ์† ๋ฝ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ

Redis ๋ถ„์‚ฐ๋ฝ์„ ์„ ํƒํ•œ ์ด์œ 

  1. ํ™•์žฅ์„ฑ ๊ณ ๋ ค
  • ์„œ๋น„์Šค ์„ฑ์žฅ์— ๋”ฐ๋ฅธ ์ˆ˜ํ‰ ํ™•์žฅ์„ ๋Œ€๋น„ํ•ด ๋ถ„์‚ฐํ™˜๊ฒฝ์—์„œ๋„ ๋™์ž‘ํ•˜๋Š” ๋ฝ ํ•„์š”
  1. DB ๋ถ€ํ•˜ ๋ถ„์‚ฐ
  • ์ธ๊ธฐ ์ƒํ’ˆ์˜ ๊ฒฝ์šฐ ๋™์‹œ ์ ‘์†์ž๊ฐ€ ๊ธ‰์ฆํ•˜๋Š”๋ฐ DB ์ปค๋„ฅ์…˜์„ ์˜ค๋ž˜ ์ ์œ ํ•˜๋ฉด ์ „์ฒด ์‹œ์Šคํ…œ ์„ฑ๋Šฅ ์ €ํ•˜ ์œ ๋ฐœ
  • Redis๋กœ ๋ฝ ์ฒ˜๋ฆฌ๋ฅผ ๋ถ„๋ฆฌํ•ด DB๋Š” ์‹ค์ œ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ์—๋งŒ ์ง‘์ค‘
  1. ์œ ์—ฐํ•œ ํƒ€์ž„ ์•„์›ƒ ์ œ์–ด
  • Redis์—์„œ ๋ฝ ํƒ€์ž„์•„์›ƒ์„ ์œ ์—ฐํ•˜๊ฒŒ ์กฐ์ ˆํ•ด ๋ฐ๋“œ๋ฝ ๋ฐฉ์ง€ ๊ฐ€๋Šฅ
  1. ์„ฑ๋Šฅ ์ตœ์ ํ™”
  • ๋ฉ”๋ชจ๋ฆฌ ๊ธฐ๋ฐ˜ Redis๋Š” ๋ฝ ํš๋“/ํ•ด์ œ๊ฐ€ ๋งค์šฐ ๋น ๋ฆ„

4. ๊ตฌํ˜„ ์ƒ์„ธ

์žฌ๊ณ ๊ด€๋ฆฌ ์„œ๋น„์Šค

@Service
@RequiredArgsConstructor
public class StockService {
    @Lazy
    private final RedissonClient redissonClient;
    private final ItemRepository itemRepository;

    // ๋ถ„์‚ฐ๋ฝ์„ ํ†ตํ•œ ์žฌ๊ณ ๊ฐ์†Œ
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void decreaseStock(Long itemId, Long quantity) {
        String lockKey = "Lock:" + itemId;
        RLock lock = redissonClient.getLock(lockKey);

        try {
            // ๋ฝ ํš๋“ ๋Œ€๊ธฐ์‹œ๊ฐ„ 3์ดˆ, ์ž๋™ ํ•ด์ œ์‹œ๊ฐ„ 10์ดˆ
            boolean acquired = lock.tryLock(3000, 10000, TimeUnit.MILLISECONDS);
            if (!acquired) {
                throw new CustomException(ExceptionCode.STOCK_LOCK_FAILED);
            }

            try {
                // DB ๋ ˆ๋ฒจ ๋น„๊ด€์  ๋ฝ๊ณผ ์กฐํ•ฉ
                Item item = itemRepository.findByIdWithLock(itemId)
                    .orElseThrow(() -> new CustomException(ExceptionCode.NOT_FOUND_ITEM));

                item.decreaseStock(quantity);

            } finally {
                if (lock.isHeldByCurrentThread()) {
                    lock.unlock();
                }
            }

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new CustomException(ExceptionCode.OPERATION_INTERRUPTED);
        }
    }
}

Repository ๋ ˆ๋ฒจ ๋น„๊ด€์  ๋ฝ

@Repository
public interface ItemRepository extends JpaRepository<Item, Long>, ItemRepositoryCustom {
	@Lock(LockModeType.PESSIMISTIC_WRITE)
	@Query("SELECT i FROM Item i WHERE i.id = :itemId")
	Optional<Item> findByIdWithLock(@Param("itemId") Long itemId);
}

์ฃผ๋ฌธ ์„œ๋น„์Šค

@Service
@RequiredArgsConstructor
public class OrderService {
    private final StockService stockService;
    
    @Transactional
    public CreateOrderResponseDto createOrder(CreateOrderRequestDto requestDto, Long userId) {
        // ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๊ฒ€์ฆ...
        
        // ๋ถ„์‚ฐ ๋ฝ์„ ํ†ตํ•œ ์•ˆ์ „ํ•œ ์žฌ๊ณ  ์ฐจ๊ฐ
        stockService.decreaseStock(item.getId(), requestDto.getQuantity());
        
        // ์ฃผ๋ฌธ ์ƒ์„ฑ ๋กœ์ง...
    }
}

๋ถ„์‚ฐ๋ฝ ์ˆœ์„œ๋„

image

๋ฐ๋“œ๋ฝ ๋ฐฉ์ง€ ์ „๋žต

  • ๋ฝ ์ˆœ์„œ ์ •๋ ฌ: ์—ฌ๋Ÿฌ ๋ฆฌ์†Œ์Šค ๋ฝ ์‹œ ID ์ˆœ์œผ๋กœ ์ •๋ ฌํ•˜์—ฌ ํš๋“
  • ํƒ€์ž„์•„์›ƒ ์„ค์ •: ๋ชจ๋“  ๋ฝ์— ์ ์ ˆํ•œ ํƒ€์ž„์•„์›ƒ ์„ค์ •
  • ๋ฝ ๋ฒ”์œ„ ์ตœ์†Œํ™”: ํŠธ๋žœ์žญ์…˜ ๋ฒ”์œ„๋ฅผ ์ตœ์†Œํ•œ์œผ๋กœ ์œ ์ง€

5. ํ–ฅํ›„ ๊ณ ๋ ค ์‚ฌํ•ญ

๋ชจ๋‹ˆํ„ฐ๋ง

  • ๋ฝ ๋Œ€๊ธฐ์‹œ๊ฐ„ ๋ชจ๋‹ˆํ„ฐ๋ง: ํ‰๊ท  ๋Œ€๊ธฐ์‹œ๊ฐ„ ๋ฐ ์ตœ๋Œ€ ๋Œ€๊ธฐ์‹œ๊ฐ„ ์ถ”์ 
  • ๋ฐ๋“œ๋ฝ ๋ฐœ์ƒ๋ฅ  ์ถ”์ : ๋ฐ๋“œ๋ฝ ๋ฐœ์ƒ ๋นˆ๋„ ๋ฐ ํŒจํ„ด ๋ถ„์„
  • ์„ฑ๋Šฅ ๋ฉ”ํŠธ๋ฆญ ์ˆ˜์ง‘: ์ฒ˜๋ฆฌ๋Ÿ‰, ์‘๋‹ต์‹œ๊ฐ„, ์—๋Ÿฌ์œจ ์ง€์†์  ๋ชจ๋‹ˆํ„ฐ๋ง

ํ™•์žฅ์„ฑ ๊ณ ๋ ค์‚ฌํ•ญ

  • ์ƒค๋”ฉ ์ „๋žต: ๋ฐ์ดํ„ฐ ์ฆ๊ฐ€ ์‹œ ์ˆ˜ํ‰์  ํ™•์žฅ์„ ์œ„ํ•œ ์ƒค๋”ฉ ๊ณ„ํš
๐Ÿ’กย ์•„๋งˆ์กด SES๋ฅผ ์ด์šฉํ•œ ๋ฉ”์ผ ์•Œ๋žŒ ๊ธฐ๋Šฅ ๊ตฌํ˜„

1. ๋ฐฐ๊ฒฝ

์ƒˆ์†Œ์‹์€ ๋ฐœ๋งค ์˜ˆ์ •์ธ ๊ตฟ์ฆˆ๋‚˜ ์ด๋ฒคํŠธ์™€ ๊ฐ™์€ ๊ณต์‹์ ์ธ ์ •๋ณด๋ฅผ ์‚ฌ์šฉ์ž์—๊ฒŒ ์ „๋‹ฌํ•˜๋Š” ๊ณต๊ฐ„์ž…๋‹ˆ๋‹ค.

ํšŒ์›๋“ค์ด ์ƒˆ์†Œ์‹์— ๋” ๋งŽ์€ ๊ด€์‹ฌ์„ ๊ฐ€์ง€๊ณ  ์ž์ฃผ ์ด์šฉํ•˜๋„๋ก ํ•˜๊ธฐ ์œ„ํ•ด, ์ƒˆ์†Œ์‹์ด ๋“ฑ๋ก๋˜๋ฉด ์ด๋ฉ”์ผ๋กœ ์•Œ๋ฆผ์„ ๋ณด๋‚ด๋Š” ๋ฐฉ์•ˆ์„ ๊ฒ€ํ† ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

๊ฒ€ํ†  ๊ณผ์ •์—์„œ ๊ณ ๋ คํ•œ ์‚ฌํ•ญ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

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

โ†’ ๋”ฐ๋ผ์„œ, ์•Œ๋ฆผ ๋งค์ฒด๋กœ ์ด๋ฉ”์ผ ์ „์†ก ๊ธฐ๋Šฅ์„ ์šฐ์„ ์ ์œผ๋กœ ๋„์ž…ํ•˜๊ธฐ๋กœ ๊ฒฐ์ •ํ•˜์˜€์Šต๋‹ˆ๋‹ค.


2. ์š”๊ตฌ์‚ฌํ•ญ

  • ์ƒˆ์†Œ์‹ ๋“ฑ๋ก ์‹œ ํšŒ์›์—๊ฒŒ ์ด๋ฉ”์ผ ์ „์†ก
  • ๋ฉ”์ผ ๋ฐœ์†ก ์ค‘ API ์‘๋‹ต ์ง€์—ฐ ์ตœ์†Œํ™”
  • ๋ฉ”์ผ ๋ฐœ์†ก ์‹คํŒจ ์‹œ ๋กœ๊ทธ ๊ธฐ๋ก ๋ฐ ์žฌ์‹œ๋„ ๊ฐ€๋Šฅ
  • ํ™•์žฅ์„ฑ ๊ณ ๋ ค (๋‚˜์ค‘์— ์™ธ๋ถ€ MQ ์ ์šฉ ๊ฐ€๋Šฅ)

3. ์˜์‚ฌ๊ฒฐ์ •

SMTP ์„ ํƒ ์ด์œ 

  • ์ด๋ฉ”์ผ ์•Œ๋ฆผ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด **SMTP(Simple Mail Transfer Protocol)**๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ๋กœ ๊ฒฐ์ •
  • SMTP๋Š” ์ธํ„ฐ๋„ท์—์„œ ์ด๋ฉ”์ผ์„ ์ „์†กํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ํ‘œ์ค€ ํ†ต์‹  ๊ทœ์•ฝ์œผ๋กœ, ๋ฐœ์‹  ๋ฉ”์ผ์„ ๋ฐ›์•„ ์ˆ˜์‹  ์„œ๋ฒ„๋กœ ์ „๋‹ฌํ•˜๋Š” ์—ญํ• 
  • SMTP๋ฅผ ์‚ฌ์šฉํ•จ์œผ๋กœ์จ:
    • ๋ฒ”์šฉ์„ฑ: ๊ตฌ๊ธ€, ์•„๋งˆ์กด SES, ์‚ฌ๋‚ด ๋ฉ”์ผ ์„œ๋ฒ„ ๋“ฑ ๊ฑฐ์˜ ๋ชจ๋“  ๋ฉ”์ผ ์„œ๋ฒ„์—์„œ ์ง€์›
    • ์•ˆ์ •์„ฑ: ํ‘œ์ค€ํ™”๋œ ํ”„๋กœํ† ์ฝœ์ด๋ฏ€๋กœ ํ˜ธํ™˜์„ฑ ๋ฌธ์ œ ์ตœ์†Œํ™”
    • ์ง์ ‘ ์ œ์–ด ๊ฐ€๋Šฅ: ์ „์†ก ๊ณผ์ •์„ ์ฝ”๋“œ์—์„œ ๋ฐ”๋กœ ์ œ์–ดํ•  ์ˆ˜ ์žˆ์Œ

SMTP ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ๊ณผ ์‹ค์ œ ์„œ๋น„์Šค

  • ์ดˆ๊ธฐ ๊ฐœ๋ฐœ ๋‹จ๊ณ„์—์„œ๋Š” ๊ตฌ๊ธ€ SMTP๋กœ ํ…Œ์ŠคํŠธํ•˜์˜€์œผ๋‚˜ ์ „์†ก ํšŸ์ˆ˜ ์ œํ•œ์ด ์กด์žฌ
  • ๋”ฐ๋ผ์„œ, ์‹ค์ œ ์„œ๋น„์Šค์—์„œ๋Š” ์ „๋ฌธ ๋ฉ”์ผ ์†ก์‹  ์„œ๋น„์Šค์ธ ์•„๋งˆ์กด SES๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ตฌํ˜„

ํ ํ™œ์šฉ ๋ฐ ์„ค๊ณ„ ์ด์œ 

  • ๋ฉ”์ผ ๋ฐœ์†ก ์‹คํŒจ ์‹œ ์•ˆ์ •์ ์ธ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด Redis ํ๋ฅผ ํ™œ์šฉ
  • ์ƒˆ์†Œ์‹ ๋“ฑ๋ก ์‹œ ๋ชจ๋“  ํšŒ์›์—๊ฒŒ ๋ฉ”์ผ์„ ๋ณด๋‚ด๋„๋ก ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌํ•˜๋„๋ก ์„ค๊ณ„
  • ํ๋ฅผ ์‚ฌ์šฉํ•จ์œผ๋กœ์จ:
    • ๋Œ€๋Ÿ‰ ๋ฐœ์†ก ์‹œ API ์‘๋‹ต ์ง€์—ฐ ์ตœ์†Œํ™”
    • ์‹คํŒจํ•œ ๋ฉ”์ผ์— ๋Œ€ํ•œ ์žฌ์‹œ๋„ ๋ฐ ๋กœ๊น… ์šฉ์ด
    • ๋‚˜์ค‘์— RabbitMQ, Kafka, SQS ๋“ฑ ๋‹ค๋ฅธ ๋ฉ”์‹œ์ง€ ํ๋กœ ์ „ํ™˜ ์‹œ ๊ตฌ์กฐ์  ์œ ์—ฐ์„ฑ ํ™•๋ณด

4. ๊ตฌํ˜„ ์ƒ์„ธ

๊ณผ์ • 1. ๊ธฐ๋ณธ ๋ฉ”์ผ ์ „์†ก ๊ตฌํ˜„

build.gardle ์˜์กด์„ฑ ์ถ”๊ฐ€

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-mail'
}

application.yml ๋ฉ”์ผ ์„ค์ • ์ถ”๊ฐ€

spring:
  mail:
    host: smtp.gmail.com
    port: 587
    username: ${SES_USERNAME}
    password: ${SES_PASSWORD}
    properties:
      mail:
        smtp:
          auth: true
          starttls:
            enable: true
email: ${EMAIL}

MailSendService

@RequiredArgsConstructor
public class MailSendService {
	private final JavaMailSender mailSender;
	
	// ์†ก์‹ ์ž ๋ฉ”์ผ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๋ฐ›๊ธฐ
	@Value("${email}")
	private String from;

	public void sendNewsfeedNotification(String toEmail, String newsfeedTitle) {
		// ๋ฉ”์ผ ๋ฉ”์‹œ์ง€ ๊ฐ์ฒด ์ƒ์„ฑ
		SimpleMailMessage message = new SimpleMailMessage();
		
		// ์†ก์‹ ์ž ๋ฉ”์ผ
		message.setFrom(from)
		
		// ์ˆ˜์‹ ์ž ๋ฉ”์ผ
		message.setTo(toEmail);
		
		// ์ œ๋ชฉ
		message.setSubject("[์ƒˆ์†Œ์‹] '" + newsfeedTitle + "'๊ฐ€ ์ƒˆ๋กœ ์˜ฌ๋ผ์™”์–ด์š”!");

		// ๋ณธ๋ฌธ
		StringBuilder body = new StringBuilder();
		body.append("์•ˆ๋…•ํ•˜์„ธ์š”, Mania Place์ž…๋‹ˆ๋‹ค.\n\n");
		body.append("์ œ๋ชฉ: ").append(newsfeedTitle).append("\n");
		body.append("์ง€๊ธˆ ๋ฐ”๋กœ Mania Place์— ์ ‘์†ํ•˜๊ณ  ์ƒˆ์†Œ์‹์„ ํ™•์ธํ•ด๋ณด์„ธ์š”!");
		message.setText(body.toString());
		
		// ์ „์†ก
		mailSender.send(message);
	}
}
  • JavaMailSender๋กœ ๋ฉ”์ผ ๋ฐœ์†ก ์„œ๋น„์Šค ๊ตฌํ˜„
  • ๋™๊ธฐ(Synchronous) ๋ฐฉ์‹์œผ๋กœ ์ฒ˜๋ฆฌ๋˜์–ด ๊ฐ ๋ฉ”์ผ ๋ฐœ์†ก ์™„๋ฃŒ๊นŒ์ง€ ์‘๋‹ต ๋Œ€๊ธฐ โ†’ API ์ง€์—ฐ ๋ฐ ์„œ๋ฒ„ ๋ฆฌ์†Œ์Šค ์ ์œ  ๋ฐœ์ƒ
  • ์‹ค์ œ ์ธก์ • ๊ฒฐ๊ณผ: ๋ฉ”์ผ 1๊ฑด ๋ฐœ์†ก ์‹œ 4.34์ดˆ ์†Œ์š”, ์ˆ˜์‹ ์ž ์ˆ˜ ์ฆ๊ฐ€์— ๋น„๋ก€ํ•˜์—ฌ ์‘๋‹ต ์‹œ๊ฐ„์ด ์„ ํ˜•์ ์œผ๋กœ ์ฆ๊ฐ€ํ•จ ํ™•์ธ

๊ณผ์ • 2. ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ์‹œ๋„

  • MailService์— @Async๋ฅผ ์ ์šฉํ•ด ๋ฉ”์ผ ๋ฐœ์†ก์„ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ์‹œ๋„
  • API ์‘๋‹ต์€ ์ฆ‰์‹œ ๋ฐ˜ํ™˜๋˜๋ฉฐ, ๋ฉ”์ผ ์ „์†ก์€ ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์ง„ํ–‰
  • ๋น„๋™๊ธฐ ์ ์šฉ ํ›„ ์‘๋‹ต ์‹œ๊ฐ„: ํ‰๊ท  200ms ์ˆ˜์ค€
  • ๋น„๋™๊ธฐ๋กœ ์‘๋‹ต ์†๋„๋Š” ๋นจ๋ผ์กŒ์ง€๋งŒ, ๋ฉ”์ผ ์ „์†ก ์‹คํŒจ ์‹œ ๋ฐ”๋กœ ์•Œ๊ธฐ ์–ด๋ ต๊ณ  ์žฌ์ฒ˜๋ฆฌ๋„ ์‰ฝ์ง€ ์•Š๋‹ค๋Š” ๋‹จ์  ๋ฐœ์ƒ

๊ณผ์ • 3. ์•ˆ์ •์  ๋ฐœ์†ก์„ ์œ„ํ•œ ํ ์ ์šฉ

MailRequestDto

@Data
public class MailRequest implements Serializable {
	private String toEmail;
	private String title;
	private int retryCount = 0;  // ์žฌ์‹œ๋„ ํšŸ์ˆ˜ ๊ธฐ๋ณธ 0

	private MailRequest(String toEmail, String newsfeedTitle) {
		this.toEmail = toEmail;
		this.title = newsfeedTitle;
	}

	public static MailRequest of(String toEmail, String newsfeedTitle) {
		return new MailRequest(toEmail, newsfeedTitle);
	}
}

โ€‹ RedisMailQueueService

@Service
@RequiredArgsConstructor
public class RedisMailQueueService {
	private final RedissonClient redissonClient;

	private static final String MAIL_QUEUE_KEY = "newsfeed:mail:queue";

	public void enqueueMail(MailRequest mailRequest) {
		// ๋ฉ”์ผ ์š”์ฒญ์„ Redis ํ์— ๋„ฃ๋Š” ์ €์žฅยท์ „๋‹ฌ ๋กœ์ง
		RBlockingQueue<MailRequest> queue = redissonClient.getBlockingQueue(MAIL_QUEUE_KEY);
		queue.add(mailRequest);
	}
}

RedisMailWorker

@Slf4j
@Component
@RequiredArgsConstructor
public class RedisMailWorker implements InitializingBean, DisposableBean {
	private final RedissonClient redissonClient;
	private final MailSendService mailService;

	private static final String MAIL_QUEUE_KEY = "newsfeed:mail:queue";
	private volatile boolean running = true;
	private Thread workerThread;

	@Override
	public void afterPropertiesSet() {
		workerThread = new Thread(() -> {
			RBlockingQueue<MailRequest> queue = redissonClient.getBlockingQueue(MAIL_QUEUE_KEY);

			while (running) {
				MailRequest mailRequest = null;
				try {
					// ํ์—์„œ MailRequest ๊ฐ์ฒด๊ฐ€ ๋“ค์–ด์˜ฌ ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐ
					mailRequest = queue.take();

					// ๋ฐ›์€ ์š”์ฒญ์œผ๋กœ ๋ฉ”์ผ ์ „์†ก
					mailService.sendNewsfeedNotification(
						mailRequest.getToEmail(),
						mailRequest.getTitle()
					);
				} catch (InterruptedException e) {
					Thread.currentThread().interrupt(); // ์ธํ„ฐ๋ŸฝํŠธ ์ฒ˜๋ฆฌ
				} catch (Exception e) {
					// ๋ฉ”์ผ ์ „์†ก ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ๋กœ๊ทธ ๊ธฐ๋ก
					log.error("[{}] {} - {} - (USER ID: {}) ({} ms) - ERROR: {} | Params: {}",
						"MAIL_SEND", MAIL_QUEUE_KEY, "sendNewsfeedNotification",
						"system", 0L, e.getMessage(),
						mailRequest != null ? mailRequest.toString() : "null"
					);

					if (mailRequest != null && mailRequest.getRetryCount() < 3) {
						mailRequest.setRetryCount(mailRequest.getRetryCount() + 1);
						try {
							// ์‹คํŒจํ•œ ์š”์ฒญ ๋‹ค์‹œ ํ์— ๋„ฃ๊ธฐ
							Thread.sleep(1000); // 1์ดˆ ๋Œ€๊ธฐ
							queue.offer(mailRequest);

						} catch (Exception ex) {
							// ํ์— ๋‹ค์‹œ ๋„ฃ๋Š” ์ค‘ ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ๋กœ๊ทธ ๊ธฐ๋ก
							log.error("[{}] {} - {} - (USER ID: {}) ({} ms) - ERROR: {} | Params: {}",
								"MAIL_SEND_REQUEUE", MAIL_QUEUE_KEY, "queue.offer",
								"system", 0L, ex.getMessage(),
								mailRequest != null ? mailRequest.toString() : "null"
							);
						}
					}
				}
			}
		});

		workerThread.setDaemon(true);
		workerThread.start();
	}

	@Override
	public void destroy() {
		running = false;
		workerThread.interrupt();
	}
}
  • ์•ž์„  ๋‹จ๊ณ„์—์„œ ์ ์šฉํ•œ @Async ์ œ๊ฑฐ
  • Redis ํ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฉ”์ผ ๋ฐœ์†ก ์š”์ฒญ์„ ๋น„๋™๊ธฐ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋„๋ก ๋ณ€๊ฒฝ
  • ์›Œ์ปค๊ฐ€ ํ๋ฅผ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜๋ฉฐ ๋ฉ”์ผ ๋ฐœ์†ก ์ˆ˜ํ–‰
  • ๋ฉ”์ผ ์ „์†ก์— ์‹คํŒจํ•˜๋ฉด ๋‹ค์‹œ ํ์— ๋„ฃ์–ด ์ „์†ก ์žฌ์‹œ๋„ ์ˆ˜ํ–‰ ์ด๋•Œ, ๋ฌดํ•œ ์žฌ์‹œ๋„๋กœ ์ธํ•œ ํ ์ ์ฒด ๋ฐ ๋ฆฌ์†Œ์Šค ๋‚ญ๋น„๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ์ตœ๋Œ€ 3ํšŒ๋กœ ์ œํ•œ

5. ํ–ฅํ›„ ๊ณ ๋ ค ์‚ฌํ•ญ

  • ์žฌ์‹œ๋„ ์ •์ฑ… ๊ฐœ์„ 
    • ์ง€์ˆ˜ ๋ฐฑ์˜คํ”„: ์žฌ์‹œ๋„ ๊ฐ„๊ฒฉ์„ ์ ์ฐจ ๋Š˜๋ ค ๊ณผ๋ถ€ํ•˜ ๋ฐฉ์ง€
    • ๋ฐฐ์น˜ ์žฌ์ „์†ก: ์‹คํŒจ ๋ฉ”์ผ์„ ๋ชจ์•„ ์ผ์ • ์‹œ์ ์— ์ผ๊ด„ ์žฌ๋ฐœ์†ก
  • ๋ฉ”์‹œ์ง€ ํ ํ™•์žฅ์„ฑ
    • RabbitMQ: ์•ˆ์ •์ ์ธ ๋ฉ”์‹œ์ง€ ์ „์†ก, ack/queue ๊ด€๋ฆฌ ์šฉ์ด
    • Kafka: ๋†’์€ ์ฒ˜๋ฆฌ๋Ÿ‰(TPS), ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆฌ๋ฐ ์ ํ•ฉ
    • SQS (AWS): ์™„์ „ ๊ด€๋ฆฌํ˜• ์„œ๋น„์Šค๋กœ ์„œ๋ฒ„ ์šด์˜ ๋ถ€๋‹ด ์—†์Œ
๐Ÿ’กย ์‹ค์‹œ๊ฐ„์ฑ„ํŒ… ๋„์ž…

1. ๋ฐฐ๊ฒฝ

http ํ”„๋กœํ† ์ฝœ์€ ๋‹จ๋ฐฉํ–ฅํ†ต์‹ ์œผ๋กœ ์„ค๊ณ„๋˜์—ˆ๋Š”๋ฐ ์‹ค์‹œ๊ฐ„ ์ฑ„ํŒ…์„ ๊ตฌํ˜„ํ•œ๋‹ค๊ณ  ๊ฐ€์ •ํ–ˆํ• ๋•Œ, ์ฑ„ํŒ…์„๋ณด๋‚ด๊ณ  ์‘๋‹ต์„ ๋ฐ›๋Š”์š”์ฒญ์„ ๋˜ ๋ณด๋‚ด์•ผ ํ•˜๊ธฐ์— ๊ตฌ์กฐ์ ์œผ๋กœ ์˜์•„ํ•จ์„ ๋А๊ปด ์ข€ ๋” ๋‚˜์€ ๋ฐฉ์‹๋“ค์ด ์žˆ๋Š”์ง€ ์ฐพ์•„๋ณด์•˜์Šต๋‹ˆ๋‹ค.


2. ์š”๊ตฌ์‚ฌํ•ญ

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


3. ์˜์‚ฌ๊ฒฐ์ •

  1. ํด๋ง : ์Šคํ•€๋ฝ์ฒ˜๋Ÿผ ๊ณ„์†ํ•ด์„œ ์ƒˆ๋กœ์šด ๋ฉ”์„ธ์ง€๋ฅผ ํ™•์ธํ•˜๋Š” ๋ฐฉ์‹

๋‹จ์  : ๋ฉ”์„ธ์ง€๊ฐ€ ์™”๋Š”์ง€ ๊ณ„์† ํ™•์ธ์„ ํ•˜๋Š”๊ณผ์ •์—์„œ ๊ณ„์†ํ•ด์„œ ๋ฆฌ์†Œ์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋Š”๋ฐ, ์ด๋Ÿฐ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด 1์ดˆ๋งˆ๋‹ค ํ™•์ธํ•˜๋Š”๊ฒƒ์„ 5์ดˆ๋งˆ๋‹ค ํ™•์ธํ•˜๋Š”์‹์œผ๋กœ ์ˆ˜์ •ํ•œ๋‹ค๋ฉด ์‹ค์‹œ๊ฐ„์ฑ„ํŒ…์„ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•œ ๋ชฉ์ ์ด ๋ถ€์ •๋˜๋Š” ๋ชจ์ˆœ์ด ๋ฐœ์ƒํ•ด ์ ํ•ฉํ•˜์ง€ ์•Š์€ ๋ฐฉ์‹์ด๋ผ๊ณ  ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค.

  1. ๋กฑํด๋ง : ์š”์ฒญ์„ ๋ณด๋‚ด๊ณ  ์ƒˆ๋กœ์šด ๋ฉ”์„ธ์ง€๊ฐ€ ์ƒ๊ธฐ๋ฉด ์‘๋‹ตํ•˜๋Š”๋ฐฉ์‹

๋‹จ์  : ํด๋ง์˜ ๋‹จ์ ์ธ ๋ฌด์˜๋ฏธํ•œ ์š”์ฒญ์„ ๊ณ„์† ๋ณด๋‚ด๋Š”๊ฒƒ์€ ํ•ด๊ฒฐํ–ˆ์ง€๋งŒ ๋ฉ”์„ธ์ง€๊ฐ€ ์ƒ๊ธธ๋•Œ๊นŒ์ง€ ์—ฐ๊ฒฐ์„ ์œ ์ง€ํ•˜๊ธฐ๋•Œ๋ฌธ์— ์“ฐ๋ ˆ๋“œ๋ฅผ ์ ์œ ํ•˜๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ์–ด ๋Œ€๊ทœ๋ชจ ํŠธ๋ž˜ํ”ฝ์ƒํ™ฉ์—์„œ ๋ถ€์ ํ•ฉํ•œ ๋ฐฉ์‹์ด๋ผ๊ณ  ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค.

  1. SSE : ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์„œ๋ฒ„์— ์—ฐ๊ฒฐ์„ ์š”์ฒญํ•˜๋ฉด ์„œ๋ฒ„๋Š” ์ด ์—ฐ๊ฒฐ์„ ์œ ์ง€ํ•˜๋ฉฐ ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๊ฐ€ ์ƒ๊ธธ๋•Œ๋งˆ๋‹ค ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์‘๋‹ต์„์ฃผ๋Š” ์‹ค์‹œ๊ฐ„ ๋‹จ๋ฐฉํ–ฅ ํ†ต์‹ ๋ฐฉ์‹

๋‹จ์  : ์š”์ฒญ์€ http, ์‘๋‹ต์€ sse๋กœ ๊ตฌํ˜„์€ ๊ฐ€๋Šฅํ•˜์ง€๋งŒ ๊ธฐ๋ณธ์ ์œผ๋กœ ๋‹จ๋ฐฉํ–ฅ ํ†ต์‹ ์„ ์œ„ํ•ด ๋‚˜์˜จ๋ฐฉ์‹์ด๊ธฐ๊ณ  ์ฑ„ํŒ…๊ณผ ๊ฐ™์€ ์–‘๋ฐฉํ–ฅ ๊ตฌ์กฐ์—” ์ ํ•ฉํ•˜์ง€ ์•Š๋‹ค๊ณ  ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค.

  1. websocket + stomp

websockete : ํ•œ ๋ฒˆ ์—ฐ๊ฒฐ๋˜๋ฉด ์–‘๋ฐฉํ–ฅ์œผ๋กœ ๊ณ„์† ํ†ต์‹ ํ•˜๋Š” ์–‘๋ฐฉํ–ฅ ํ†ต์‹ ์ง€์› ํ”„๋กœํ† ์ฝœ statefulํ•œ ํ”„๋กœํ† ์ฝœ์ด๊ธฐ์— ์„œ๋กœ์˜ ์ƒํƒœ๋ฅผ ์•Œ์ˆ˜์žˆ์–ด ์ฑ„ํŒ…์ฝ์Œ, ์ฑ„ํŒ…๋ฐฉ ๋‚˜๊ฐ๋“ฑ์˜ ์ •๋ณด๋„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์•Œ์ˆ˜๊ฐ€ ์žˆ์Œ.

stompํ”„๋กœํ† ์ฝœ : ์›น์†Œ์บฃ ํ”„๋กœํ† ์ฝœ์˜ ์œ„์—์„œ ๋™์ž‘ํ•˜๋Š” ํ”„๋กœํ† ์ฝœ๋กœ send, subscribe, publish ๋“ฑ์˜ ๊ทœ์ •๋œ ํ‹€์„ ์ œ๊ณตํ•˜๋Š” ์„œ๋ธŒ ํ”„๋กœํ† ์ฝœ ๋‹จ์  : ๋‹จ์ผ ์„œ๋ฒ„์—์„œ๋Š” ์ˆ˜๋งŽ์€ websocket์—ฐ๊ฒฐ์„ ์œ ์ง€ํ•˜๊ณ  stomp ํ†ต์‹ ์ฒ˜๋ฆฌํ•˜๋Š”๊ฒƒ์ด ๋ถ€๋‹ด๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  1. ๊ฒฐ์ •

์œ„์˜ ๋ฐฉ์•ˆ๋“ค์ค‘ ์„œ๋น„์Šค์˜ ์•ˆ์ •์„ฑ, ์ž์› ์†Œ๋ชจ๋ฅผ ๊ณ ๋ คํ–ˆ์„๋•Œ ์•„๋ž˜์™€๊ฐ™์€ ์ด์œ ๋กœ 4๋ฒˆ์„ ์„ ํƒํ–ˆ์Šต๋‹ˆ๋‹ค.

์›น์†Œ์ผ“ ์—ฐ๊ฒฐ ์ž์ฒด๋Š” ์Šค๋ ˆ๋“œ๋ฅผ ๊ณ„์† ์ ์œ ํ•˜์ง€ ์•Š๊ณ  ๋Œ€๊ธฐํ•˜๋‹ค๊ฐ€ stompํ†ต์‹ ์ด ์˜ค๊ฐˆ๋•Œ๋งŒ ์Šค๋ ˆ๋“œ๊ฐ€ ํ• ๋‹น๋˜์–ด ์œ ์ง€ํ•˜๋Š”๊ฒƒ์—๋Š” ๋ฌธ์ œ๊ฐ€์—†๋‹ค๊ณ  ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค.

๋˜ํ•œ ๋Œ€์šฉ๋Ÿ‰ ํŠธ๋ž˜ํ”ฝ ์ƒํ™ฉ์—์„œ๋Š” TaskExecutor ์„ค์ •์„ ํ†ตํ•ด ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ์Šค๋ ˆ๋“œ ํ’€์˜ ํฌ๊ธฐ๋ฅผ ์œ ์—ฐํ•˜๊ฒŒ ์กฐ์ ˆํ•  ์ˆ˜ ์žˆ์–ด ์œ ์—ฐํ•œ ๋Œ€์ฒ˜์— ์ ํ•ฉํ•˜๋‹ค๊ณ  ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค.

๋‹จ์ผ์„œ๋ฒ„๋กœ ์ธํ•ด ์ƒ๊ธฐ๋Š” ๋ฌธ์ œ์ ์€ ๋ฉ”์„ธ์ง€ ํ๋“ฑ์„ ์‚ฌ์šฉํ•˜์—ฌ ์—ฌ๋Ÿฌ ์„œ๋ฒ„๋กœ ์ƒํƒœ๋ฅผ ๊ณต์œ ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„ํ•ด ํ•ด๊ฒฐํ•ด ๋ณผ ์ˆ˜ ์žˆ์„ ๊ฒƒ ๊ฐ™์•„ websocket + stomp ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„ํ•˜๊ธฐ๋กœ ๊ฒฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.


4. ๊ตฌํ˜„ ์ƒ์„ธ

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws/chat") //stomp ์—ฐ๊ฒฐ์„ ์œ„ํ•œ ์—”๋“œํฌ์ธํŠธ ์„ค์ •
                .setAllowedOriginPatterns("*")// ๊ฐœ๋ฐœ ๋‹จ๊ณ„์—์„œ ๋ชจ๋“  origin ํ—ˆ์šฉ
                .withSockJS();// socket+javascript
    }
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/sub");
        // ๋ฉ”์„ธ์ง€๋ธŒ๋กœ์ปค์—์„œ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๊ตฌ๋…ํ•  prefix
        registry.setApplicationDestinationPrefixes("/pub");
        // ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์„œ๋ฒ„๋กœ ๋ฉ”์„ธ์ง€ ๋ณด๋‚ผ ๋•Œ prefix
    }
    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.interceptors(interceptors);
    }

5. ํ–ฅํ›„ ๊ณ ๋ ค ์‚ฌํ•ญ

๋ฉ”์„ธ์ง€์˜ ์˜์†์„ฑ, ๋ฉ”์„ธ์ง€์˜ ์œ ์‹ค, ๋Œ€๊ทœ๋ชจ ํŠธ๋ž˜ํ”ฝ์ƒํ™ฉ์—์„œ์˜ ์•ˆ์ •์ ์ธ ์“ฐ๋ ˆ๋“œ ํ• ๋‹น์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ’กย rabbitMQ ๋„์ž…

1. ๋ฐฐ๊ฒฝ

๊ธฐ์กด ์ฑ„ํŒ… ์‹œ์Šคํ…œ์€ ๋ฉ”์„ธ์ง€๊ฐ€ ์ƒ์„ฑ๋  ๋•Œ๋งˆ๋‹ค ๋งค๋ฒˆ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ง์ ‘ INSERT ์ฟผ๋ฆฌ๋ฅผ ๋ณด๋‚ด๋Š” ๋™๊ธฐ์  ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

์ด ๋ฐฉ์‹์€ ํŠธ๋ ˆํ”ฝ์ด ๋ชฐ๋ฆฌ๋Š” ์ƒํ™ฉ์ด๋ผ๋ฉด DB์— ๋ถ€ํ•˜๊ฐ€ ์ง‘์ค‘๋˜์–ด ์„ฑ๋Šฅ์ด ๋–จ์–ด์งˆ ์ˆ˜ ์žˆ๊ณ , DB ์ ‘๊ทผ ๊ณผ์ •์—์„œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค๋ฉด ์„œ๋น„์Šค ์‘๋‹ต ์ž์ฒด๊ฐ€ ์ง€์—ฐ๋˜๋Š” ๋ฌธ์ œ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๊ธฐ๋„ ํ•ฉ๋‹ˆ๋‹ค.

   //controller
   @MessageMapping("/chatroom/{roomId}")
    public void sendMessageV1(
            @DestinationVariable Long roomId,
            @Payload ChatMessageRequest request,
            CustomPrincipal principal
    ) {
        chatMessageService.saveMessage(roomId, request, principal);
        messagingTemplate.convertAndSend("/sub/chatroom/" + roomId, request);
    }
    //service
    @Transactional
    public ChatMessage saveMessage(Long roomId,ChatMessageRequest request, CustomPrincipal principal) {
        User sender = userService.findByIdOrElseThrow(principal.getId());
        ChatRoom room = chatRoomService.findByIdOrElseThrow(roomId);
        ChatMessage message = ChatMessage.of(room, sender, request.getContent(), null);
        return chatMessageRepository.save(message);
    }

2. ์š”๊ตฌ์‚ฌํ•ญ

๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ : ๋ฉ”์„ธ์ง€ ์ €์žฅ ๋กœ์ง์„ ๋ถ„๋ฆฌํ•˜์—ฌ ์‚ฌ์šฉ์ž๊ฐ€ ๋ฉ”์„ธ์ง€๋ฅผ ๋ณด๋ƒˆ์„ ๋•Œ ์ฆ‰๊ฐ์ ์œผ๋กœ ์‘๋‹ต์„ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด์•ผํ•จ.

์„ฑ๋Šฅ ๋ฐ ์•ˆ์ •์„ฑ : DB์— ์ง์ ‘์ ์ธ ๋ถ€ํ•˜๋ฅผ ์ค„์ด๊ณ , ํŠธ๋ž˜ํ”ฝ์ด ๋ชฐ๋ ค๋„ ์•ˆ์ •์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด์•ผํ•จ.

์œ ์—ฐํ•œ ์‹œ์Šคํ…œ ํ™•์žฅ : ํ–ฅํ›„ ํŠธ๋ž˜ํ”ฝ ์ฆ๊ฐ€์— ๋Œ€๋น„ํ•˜์—ฌ ์‹œ์Šคํ…œ์„ ํ™•์žฅํ•  ์ˆ˜ ์žˆ์–ด์•ผํ•จ.


3. ์˜์‚ฌ๊ฒฐ์ •

๊ธฐ์กด ๋™๊ธฐ์  ๋ฐฉ์‹์€ ์ฑ„ํŒ… ์ €์žฅ ๋กœ์ง๊ณผ ์‚ฌ์šฉ์ž ์‘๋‹ต ๋กœ์ง์ด ๊ฐ•ํ•˜๊ฒŒ ๊ฒฐํ•ฉ๋˜์–ด ์žˆ์–ด ์„œ๋น„์Šค์˜ ์•ˆ์ •์„ฑ์„ ๋ณด์žฅํ•˜๊ธฐ ์–ด๋ ต๋‹ค๊ณ  ํŒ๋‹จํ•ด ๋ฉ”์„ธ์ง€ ์ €์žฅ ๋กœ์ง์„ ๋น„๋™๊ธฐ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๋ฉ”์„ธ์ง€ ๋ธŒ๋กœ์ปค๊ฐ€ ํ•„์š”ํ•˜๋‹ค๊ณ  ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค.

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

  1. redis pub/sub : ์ธ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•ด ๋น ๋ฅธ ์„œ๋น„์Šค๋ฅผ ์ œ๊ณตํ•˜์ง€๋งŒ ์˜์†์„ฑ์„ ์ง€์›ํ•˜์ง€ ์•Š์•„ ๋ฉ”์„ธ์ง€ ์œ ์‹ค ๊ฐ€๋Šฅ์„ฑ์ด์žˆ์Šต๋‹ˆ๋‹ค.
  2. ์นดํ”„์นด ๋†’์€ ์ฒ˜๋ฆฌ๋Ÿ‰๊ณผ ํ™•์žฅ์„ฑ์— ๊ฐ•ํ•œ ๋Œ€ํ‘œ์ ์ธ ๋ฉ”์„ธ์ง€ ๊ธฐ์ˆ ์Šคํƒ์ด์ง€๋งŒ ์ด๋ฒˆ ์ฑ„ํŒ…๊ธฐ๋Šฅ์˜ ๊ฒฝ์šฐ ์„œ๋ธŒ๋น„์ฆˆ๋‹ˆ์Šค๋กœ์ง์ด๊ธฐ์— ์˜ค๋ฒ„์ŠคํŒฉ์ด๋ผ๊ณ  ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค.
  3. rabbitMQ : ๋ฉ”์„ธ์ง€ ์ „์†ก๋ณด์žฅ๊ณผ ์œ ์‹ค๋ฐฉ์ง€๋ฅผ ์ง€์›ํ•˜์ง€๋งŒ ์นดํ”„์นด๋Œ€๋น„ ๋‚ฎ์€ ์ฒ˜๋ฆฌ๋Ÿ‰, ๋ฉ”์„ธ์ง€๊ฐ€ ํ์— ์Œ“์—ฌ ๋ณ‘๋ชฉ์ด ๋ฐœ์ƒํ•˜์ง€ ์•Š๋„๋ก ๊ด€๋ฆฌ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค
  4. acitveMQ : ์˜ค๋ž˜๋œ ์ž๋ฐ”๊ธฐ๋ฐ˜์˜ jms ๋ธŒ๋กœ์ปค๋กœ ์ž๋ฐ”์ƒํƒœ๊ณ„์— ํŠนํ™”๋˜์–ด์žˆ๊ณ  ์—ฐ๋™์ด ์‰ฝ์Šต๋‹ˆ๋‹ค.
  5. ๊ฒฐ๋ก  ๋„ค๊ฐœ์˜ ๋ฉ”์„ธ์ง• ๊ธฐ์ˆ ์ค‘ rabbitMQ์™€ activeMQ๋ฅผ ๊ณ ๋ฏผํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž๋ฐ”๊ธฐ๋ฐ˜์˜ ์–ธ์–ด์ธ activeMQ๊ฐ€ ํ˜ธํ™˜์„ฑ ๋ฌธ์ œ ๋“ฑ ์ ํ•ฉํ•˜๋‹ค๊ณ ๋„ ์ƒ๊ฐํ–ˆ์ง€๋งŒ, ์˜คํžˆ๋ ค ์ž๋ฐ”์— ๋Œ€ํ•œ ๋†’์€ ์ข…์†์„ฑ์ด ์ถ”ํ›„ ํ™•์žฅ์„ ๊ณ ๋ คํ–ˆ์„ ๋•Œ ๊ฒฐํ•ฉ๋„๋ฅผ ๋†’์ผ ์ˆ˜ ์žˆ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค.

์ด๋ฒˆ ๊ณ ๋ฏผ์˜ ๊ฐ€์žฅ ๊ทผ๋ณธ์ ์ธ ๋ถ€๋ถ„์ธ ์œ ์—ฐ์„ฑ๊ณผ ํ™•์žฅ์„ฑ, ๋А์Šจํ•œ ๊ฒฐํ•ฉ์ด๋ผ๋Š” ๊ด€์ ์—์„œ ๋ณด์•˜์„๋•Œ java message service ๊ธฐ๋ฐ˜์ธ activeMQ๋ณด๋‹ค๋Š” AMPQ๊ธฐ๋ฐ˜์˜ RabbitMQ๋ฅผ ์„ ํƒํ•˜๋Š”๊ฒŒ ๊ณ ๋ฏผํ–ˆ๋˜ ๋ถ€๋ถ„์„ ํ•ด์†Œํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค.


4. ๊ตฌํ˜„ ์ƒ์„ธ

์ปจํŠธ๋กค๋Ÿฌ ๋ ˆ์ด์–ด์—์„œ ์„œ๋น„์Šค๋ ˆ์ด์–ด์˜ ๋น„์ฆˆ๋‹ˆ๋กœ์ง์„ ํ˜ธ์ถœํ•˜๋Š”๊ฒƒ์ด ์•„๋‹Œ RabbitTemplate๊ณผ MessageTemplate์œผ๋กœ ๋ฐœํ–‰ํ•˜๊ธฐ๋งŒ ํ•˜๊ณ  , ์„œ๋น„์Šค๋ ˆ์ด์–ด์—์„œ๋Š” @RabbitListener ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ ๋น„๋™๊ธฐ์ ์œผ๋กœ ์†Œ๋น„ ํ›„ ์ผ๊ด„์ ์œผ๋กœ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.

์ด๋Ÿฌํ•œ ๋ฉ”์„ธ์ง€ ๋ธŒ๋กœ์ปค ๊ธฐ๋ฐ˜์˜ ๊ตฌ์กฐ๋Š” ์Šค์ผ€์ผ ์•„์›ƒ์— ์œ ๋ฆฌํ•˜์—ฌ ๋ฉ”์„ธ์ง€ ์ฒ˜๋ฆฌ๋Ÿ‰์„ ๋Š˜๋ ค์•ผ ํ• ๊ฒฝ์šฐ ๋‹จ์ˆœํžˆ @RabbitListener๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์„œ๋น„์Šค ์„œ๋ฒ„๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ๋ถ€ํ•˜๋ฅผ ๋ถ„์‚ฐํ•จ์œผ๋กœ์จ ์—ฌ๋Ÿฌ ์„œ๋ฒ„๊ฐ€ ๋ฉ”์„ธ์ง€๋ฅผ ๋ถ„์‚ฐํ™˜๊ฒฝ์—์„œ ์ฒ˜๋ฆฌ ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ด์ค๋‹ˆ๋‹ค.

    //controller
    @MessageMapping("/chatroom/{roomId}")
    public void sendMessageV2(
            @DestinationVariable Long roomId,
            @Valid @Payload ChatMessageRequest request,
            CustomPrincipal principal
    ) {
        if (principal == null) {
            throw new CustomException(ExceptionCode.NOT_FOUND_USER);
        }
        MessageQueue messageQueue = MessageQueue.of(request.getContent(), principal.getId(), roomId, LocalDateTime.now());
        rabbitTemplate.convertAndSend(CHAT_QUEUE_NAME, messageQueue);
        messagingTemplate.convertAndSend("/sub/chatroom/" + roomId, messageQueue);
    }
    //service
    @RabbitListener(queues = "chat.queue")
    public void consumeAndBufferMessage(MessageQueue request) {
        messageBuffer.add(request);
        log.info("๋ฒ„ํผ์— ๋ฉ”์„ธ์ง€ ์ถ”๊ฐ€: {}, ๋‹ด๊ธด๋ฉ”์„ธ์ง€ ๊ฐœ์ˆ˜: {}", request.getContent(), messageBuffer.size());
    }

    @Scheduled(fixedDelay = 20000)
    public void saveMessageFromBuffer() {
        if(messageBuffer.isEmpty()) {
            return;
        }
        List<MessageQueue> messageSave = new ArrayList<>(messageBuffer);
        messageBuffer.clear();

        List<ChatMessage> chatMessages = messageSave.stream()
                .map(this::createChatMessageFromQueue)
                .toList();
        chatMessageRepository.saveAll(chatMessages);
        log.info("๋ฒ„ํผ์— ์žˆ๋Š” ๋ฉ”์„ธ์ง€ {} ๊ฐœ 20์ดˆ๋งˆ๋‹ค ์ผ๊ด„์ €์žฅ", chatMessages.size());
    }

5. ํ–ฅํ›„ ๊ณ ๋ ค ์‚ฌํ•ญ

ํŠธ๋ž˜ํ”ฝ ์ฆ๊ฐ€ ์‹œ RabbitMQ ํ์— ๋ฉ”์‹œ์ง€๊ฐ€ ์Œ“์—ฌ ๋ณ‘๋ชฉ ํ˜„์ƒ์ด ๋ฐœ์ƒํ•˜์ง€ ์•Š๋„๋ก ํ์˜ ์ƒํƒœ ๋ชจ๋‹ˆํ„ฐ๋ง์‹œ์Šคํ…œ ๊ตฌ์ถ•์ด ํ•„์š”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฉ”์‹œ์ง€ ์ „์†ก ์‹คํŒจ ์‹œ ์žฌ์‹œ๋„ ๋กœ์ง์ด๋‚˜ ๋ฐ๋“œ ๋ ˆํ„ฐ ํ(Dead Letter Queue)๋ฅผ ํ™œ์šฉํ•˜๋Š” ๋ฐฉ์•ˆ์„ ๊ณ ๋ฏผํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ’กย Pinpoint APM ๋„์ž…

1. ๋ฐฐ๊ฒฝ

ํ”„๋กœ์ ํŠธ๊ฐ€ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ๋‹จ๊ณ„๋ฅผ ์™„๋ฃŒํ•˜๊ณ  ์„ฑ๋Šฅ ์ตœ์ ํ™” ๋‹จ๊ณ„์— ์ง„์ž…ํ•˜๋ฉด์„œ, ๊ธฐ์กด์˜ ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ ๋ฐฉ์‹์˜ ํ•œ๊ณ„๊ฐ€ ๋“œ๋Ÿฌ๋‚ฌ์Šต๋‹ˆ๋‹ค. JMeter๋ฅผ ํ™œ์šฉํ•œ ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ๋ฅผ ํ†ตํ•ด ์„ฑ๋Šฅ ๋ณ‘๋ชฉ ํ˜„์ƒ์€ ํ™•์ธํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ์‹ค์ œ ์–ด๋–ค ๊ตฌ๊ฐ„์—์„œ ์ง€์—ฐ์ด ๋ฐœ์ƒํ•˜๋Š”์ง€ ๊ตฌ์ฒด์ ์ธ ์›์ธ์„ ํŒŒ์•…ํ•˜๊ธฐ ์–ด๋ ค์šด ์ƒํ™ฉ์ด์—ˆ์Šต๋‹ˆ๋‹ค.

๋กœ๊น… ์‹œ์Šคํ…œ์„ ํ†ตํ•ด ์ผ๋ถ€ ์ถ”์ ์ด ๊ฐ€๋Šฅํ•˜๋‚˜, ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฌธ์ œ์ ๋“ค๋กœ ์ธํ•ด ์ƒˆ๋กœ ๋„์ž…ํ•˜๊ธฐ๋กœ ํ–ˆ์Šต๋‹ˆ๋‹ค.

  • ๋กœ๊ทธ ๋ถ„์„์— ๋งŽ์€ ์‹œ๊ฐ„์ด ์†Œ์š”๋จ
  • ์ „์ฒด ์š”์ฒญ ํ”Œ๋กœ์šฐ์—์„œ ๋ณ‘๋ชฉ ๊ตฌ๊ฐ„์„ ํŠน์ •ํ•˜๊ธฐ ์–ด๋ ค์›€
  • ๋ฆฌ์†Œ์Šค ์‚ฌ์šฉ๋ฅ ๊ณผ ์„ฑ๋Šฅ ์ง€ํ‘œ๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ํ™•์ธํ•  ์ˆ˜ ์—†์Œ
  • ์„ฑ๋Šฅ ๊ฐœ์„  ์ „ํ›„ ๋น„๊ต ๋ถ„์„์„ ์œ„ํ•œ ์ •๋Ÿ‰์  ์ง€ํ‘œ ๋ถ€์กฑ

์ค‘๊ณ ๊ฑฐ๋ž˜ ํ”Œ๋žซํผ์€ ์ƒํ’ˆ ๊ฒ€์ƒ‰, ์ƒ์„ธ ์กฐํšŒ, ์ฑ„ํŒ…, ๊ฒฐ์ œ ๋“ฑ ์—ฌ๋Ÿฌ ๊ธฐ๋Šฅ์ด ํ•จ๊ป˜ ์ž‘๋™ํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ์„ฑ๋Šฅ ๋ณ‘๋ชฉ์„ ์ •ํ™•ํžˆ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ๋Š” ๋„๊ตฌ๊ฐ€ ํ•„์š”ํ•˜๋‹ค๊ณ  ๋А๊ผˆ์Šต๋‹ˆ๋‹ค.


2. ์š”๊ตฌ์‚ฌํ•ญ

๊ธฐ๋Šฅ์  ์š”๊ตฌ์‚ฌํ•ญ

  • ์š”์ฒญ ์ถ”์ : ์‚ฌ์šฉ์ž ์š”์ฒญ๋ถ€ํ„ฐ ์‘๋‹ต๊นŒ์ง€์˜ ์ „์ฒด ํ”Œ๋กœ์šฐ ์ถ”์ 
  • ๋ณ‘๋ชฉ ์ง€์  ์‹๋ณ„: ๊ฐ ๋ฉ”์„œ๋“œ์™€ ์„œ๋น„์Šค ๋ ˆ์ด์–ด๋ณ„ ์‘๋‹ต ์‹œ๊ฐ„ ์ธก์ •
  • ์‹ค์‹œ๊ฐ„ ๋ชจ๋‹ˆํ„ฐ๋ง: CPU, ๋ฉ”๋ชจ๋ฆฌ ๋“ฑ ์‹œ์Šคํ…œ ๋ฆฌ์†Œ์Šค ์‚ฌ์šฉ๋ฅ  ์‹ค์‹œ๊ฐ„ ํ™•์ธ
  • ์˜ค๋ฅ˜ ์ถ”์ : ์˜ˆ์™ธ ๋ฐœ์ƒ ์‹œ์ ๊ณผ ์Šคํƒ ํŠธ๋ ˆ์ด์Šค ์ƒ์„ธ ์ •๋ณด ์ œ๊ณต
  • ์„ฑ๋Šฅ ์ง€ํ‘œ ์‹œ๊ฐํ™”: ์‘๋‹ต ์‹œ๊ฐ„, TPS, ์ฒ˜๋ฆฌ๋Ÿ‰ ๋“ฑ ์ฃผ์š” ์ง€ํ‘œ์˜ ๊ทธ๋ž˜ํ”„ ํ‘œ์‹œ

๋น„๊ธฐ๋Šฅ์  ์š”๊ตฌ์‚ฌํ•ญ

  • ๋‚ฎ์€ ์˜ค๋ฒ„ํ—ค๋“œ: ์šด์˜ ํ™˜๊ฒฝ์—์„œ๋„ ์ตœ์†Œํ•œ์˜ ์„ฑ๋Šฅ ์˜ํ–ฅ
  • ๊ฐ„ํŽธํ•œ ์„ค์น˜ ๋ฐ ์„ค์ •: ๋ณต์žกํ•œ ์„ค์ • ์—†์ด ๋น ๋ฅธ ๋„์ž… ๊ฐ€๋Šฅ
  • ์˜คํ”ˆ์†Œ์Šค: ๋ผ์ด์„ ์Šค ๋น„์šฉ ๋ถ€๋‹ด ์—†์ด ํ™œ์šฉ ๊ฐ€๋Šฅ
  • ํ•œ๊ตญ์–ด ์ง€์›: ๊ตญ๋‚ด ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์— ์ตœ์ ํ™”๋œ ๋ฌธ์„œ์™€ ์ปค๋ฎค๋‹ˆํ‹ฐ

3. ์˜์‚ฌ๊ฒฐ์ •

์—ฌ๋Ÿฌ APM ๋„๊ตฌ๋ฅผ ๋น„๊ต ๊ฒ€ํ† ํ•œ ๊ฒฐ๊ณผ Pinpoint๋ฅผ ์„ ํƒํ–ˆ์Šต๋‹ˆ๋‹ค.

์„ ํƒ ์ด์œ  :

  • ๋„ค์ด๋ฒ„์—์„œ ๊ฐœ๋ฐœํ•œ ์˜คํ”ˆ์†Œ์Šค๋กœ ํ•œ๊ตญ์–ด ๋ฌธ์„œ์™€ ์ปค๋ฎค๋‹ˆํ‹ฐ ์ง€์›์ด ํ’๋ถ€
  • Java ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ํŠนํ™”๋˜์–ด Spring Boot ํ”„๋กœ์ ํŠธ์™€์˜ ํ˜ธํ™˜์„ฑ์ด ์šฐ์ˆ˜
  • ์—์ด์ „ํŠธ ๋ฐฉ์‹์œผ๋กœ ์ฝ”๋“œ ์ˆ˜์ • ์—†์ด ๋„์ž… ๊ฐ€๋Šฅ
  • ์ƒ์„ธํ•œ ํŠธ๋ ˆ์ด์‹ฑ: ๋ฉ”์„œ๋“œ ๋ ˆ๋ฒจ๊นŒ์ง€ ์„ธ๋ฐ€ํ•œ ์„ฑ๋Šฅ ์ถ”์  ์ œ๊ณต
  • ์ง๊ด€์ ์ธ UI: ์„œ๋น„์Šค ๋งต๊ณผ ์„ฑ๋Šฅ ์ฐจํŠธ๋ฅผ ํ†ตํ•œ ์‹œ๊ฐ์  ๋ถ„์„ ๊ฐ€๋Šฅ

๋‹ค๋ฅธ ๋„๊ตฌ ๋Œ€๋น„ ์žฅ์ :

  • Zipkin: ๋” ์ƒ์„ธํ•œ ๋ฉ”ํŠธ๋ฆญ ์ œ๊ณต
  • New Relic: ๋ฌด๋ฃŒ๋กœ ์‚ฌ์šฉ ๊ฐ€๋Šฅ, ์ƒ์„ธํ•œ ํ•œ๊ตญ์–ด ๋ฌธ์„œ

4. ๊ตฌํ˜„ ์ƒ์„ธ

๋‹จ๊ณ„์  ๋„์ž…

  1. 1๋‹จ๊ณ„: ๋กœ์ปฌ ํ™˜๊ฒฝ์—์„œ Docker๋กœ Pinpoint๋ฅผ ๊ตฌ์„ฑํ•˜์—ฌ ์„ฑ๋Šฅ ๋ถ„์„ ์šฉ๋„๋กœ๋งŒ ํ™œ์šฉ
  2. 2๋‹จ๊ณ„: ๋กœ์ปฌ์—์„œ์˜ ์•ˆ์ •์„ฑ ํ™•์ธ ํ›„ EC2 ์„œ๋ฒ„์˜ ๊ธฐ์กด ์ปจํ…Œ์ด๋„ˆ์— ํ†ตํ•ฉ ๋ฐฐํฌ
  3. 3๋‹จ๊ณ„: ์•„๋ž˜ ๊ณ ๋ ค์‚ฌํ•ญ์œผ๋กœ ์ธํ•ด Pinpoint ์ „์šฉ ์ปจํ…Œ์ด๋„ˆ๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ๋…๋ฆฝ ์šด์˜

๊ณ ๋ ค์‚ฌํ•ญ

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

์•„ํ‚คํ…์ฒ˜

image Pinpoint ์•„ํ‚คํ…์ฒ˜

์ธ์Šคํ„ด์Šค 1์—์„œ ๊ตฌ๋™๋˜๋Š” Spring ์•ฑ์— Pinpoint Agent๋ฅผ ๋ถ€์ฐฉํ•˜์—ฌ ํŠธ๋žœ์žญ์…˜๊ณผ ์„ฑ๋Šฅ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ง‘ํ•˜๊ณ , ์ด๋ฅผ ์ธ์Šคํ„ด์Šค 2์˜ Pinpoint Collector๋กœ ์ „์†กํ•ฉ๋‹ˆ๋‹ค. Collector๋Š” ์ˆ˜์ง‘๋œ ๋ฐ์ดํ„ฐ๋ฅผ HBase์— ์ €์žฅํ•˜๋ฉฐ, HBase์— ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ๋Š” Pinpoint Web์„ ํ†ตํ•ด ์‹œ๊ฐํ™”๋˜์–ด ์›น ๋Œ€์‹œ๋ณด๋“œ๋กœ ์ œ๊ณต๋ฉ๋‹ˆ๋‹ค.

Pinpoint ๊ตฌ์ถ• ๋ฐ ์—ฐ๊ฒฐ ์ˆœ์„œ

  1. ์Šคํ”„๋ง ์•ฑ ์ปจํ…Œ์ด๋„ˆ(์ปจํ…Œ์ด๋„ˆ 1)
    • Pinpoint Agent๋ฅผ ๋ถ€์ฐฉ (VM ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์„ค์ •)
// ์˜ˆ์‹œ
java -javaagent:/home/ubuntu/pinpoint-agent-2.5.3/pinpoint-bootstrap.jar \
-Dpinpoint.agentId=mania001 \
-Dpinpoint.applicationName=mania_place.dev \
-Dpinpoint.config=/home/ubuntu/pinpoint-agent-2.5.3/profiles/release/pinpoint.config \
-jar Mania-Place-0.0.1-SNAPSHOT.jar
  1. Pinpoint ์„œ๋ฒ„ ์ปจํ…Œ์ด๋„ˆ(์ปจํ…Œ์ด๋„ˆ 2)
    • Docker๋กœ Pinpoint๋ฅผ ๊ตฌ๋™ํ•˜๊ณ  Collector, HBase, Web ์ค€๋น„
  2. ์ปจํ…Œ์ด๋„ˆ ๊ฐ„ ๋„คํŠธ์›Œํฌ ์„ค์ •
    • ํ”„๋ผ์ด๋น— IP ๊ธฐ๋ฐ˜ ํ†ต์‹  ๊ตฌ์„ฑ
    • ํฌํŠธ 8081๋กœ ์ธ๋ฐ”์šด๋“œ/์•„์›ƒ๋ฐ”์šด๋“œ ํŠธ๋ž˜ํ”ฝ ํ—ˆ์šฉ

5. ํ–ฅํ›„ ๊ณ ๋ ค ์‚ฌํ•ญ

๊ฐœ์„  ๋ฐ ํ™•์žฅ ๊ณ„ํš

  • ์•Œ๋ฆผ ์‹œ์Šคํ…œ ๊ตฌ์ถ•: ์„ฑ๋Šฅ ์ž„๊ณ„์น˜ ์ดˆ๊ณผ ์‹œ ์ž๋™ ์•Œ๋ฆผ ์„ค์ •
  • ๋ชจ๋‹ˆํ„ฐ๋ง ๋„๊ตฌ ํ™•์žฅ: Prometheus, Grafana์™€์˜ ํ†ตํ•ฉ ๋ชจ๋‹ˆํ„ฐ๋ง ๊ตฌ์ถ•

์œ„ํ—˜ ์š”์†Œ ๋ฐ ๋Œ€์‘ ๋ฐฉ์•ˆ

์ฃผ์š” ์œ„ํ—˜ ์š”์†Œ:

  • Agent ์˜ค๋ฒ„ํ—ค๋“œ๋กœ ์ธํ•œ ์„ฑ๋Šฅ ์˜ํ–ฅ
  • HBase ์ €์žฅ์†Œ ์šฉ๋Ÿ‰ ๊ด€๋ฆฌ ์ด์Šˆ

๋Œ€์‘ ๋ฐฉ์•ˆ:

  • ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ ์ถฉ๋ถ„ํ•œ ๊ฒ€์ฆ ํ›„ ๋‹จ๊ณ„์  ํ™•๋Œ€
  • ๋ฐ์ดํ„ฐ ๋ณด์กด ๊ธฐ๊ฐ„ ์ •์ฑ… ์ˆ˜๋ฆฝ์œผ๋กœ ์šฉ๋Ÿ‰ ๊ด€๋ฆฌ

โšก๏ธ ์„ฑ๋Šฅ ๊ฐœ์„ 

โšก๏ธ ์ธ๊ธฐ ๊ฒ€์ƒ‰์–ด ๋žญํ‚น

1. ๊ธฐ๋Šฅ ์†Œ๊ฐœ

Mania Place์˜ ์ธ๊ธฐ ๊ฒ€์ƒ‰์–ด ๋žญํ‚น ๊ธฐ๋Šฅ์€

์ง€๋‚œ 24์‹œ๊ฐ„ ๋™์•ˆ ์‚ฌ์šฉ์ž๋“ค์ด ๊ฐ€์žฅ ๋งŽ์ด ๊ฒ€์ƒ‰ํ•œ ํ‚ค์›Œ๋“œ๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ง‘๊ณ„ํ•˜์—ฌ ๋ณด์—ฌ์ฃผ๋Š” ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค.

์‚ฌ์šฉ์ž๋Š” ์ตœ์‹  ํŠธ๋ Œ๋“œ๋ฅผ ๋น ๋ฅด๊ฒŒ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ๊ณ , ํŒ๋งค์ž๋Š” ์ˆ˜์š”๊ฐ€ ๋†’์€ ์ƒํ’ˆ์„ ์˜ˆ์ธกํ•˜์—ฌ ํŒ๋งค ์ „๋žต์„ ์„ธ์šธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


2. ๋ฌธ์ œ ์ •์˜

๊ธฐ์กด ์‹œ์Šคํ…œ์—์„œ๋Š” ์ธ๊ธฐ ๊ฒ€์ƒ‰์–ด ์ง‘๊ณ„๊ฐ€ ๋А๋ฆฌ๊ณ  ์ •ํ™•๋„๊ฐ€ ๋–จ์–ด์กŒ์Šต๋‹ˆ๋‹ค.

  • ๊ฒ€์ƒ‰์–ด ๋ฐ์ดํ„ฐ๊ฐ€ DB์—๋งŒ ์ €์žฅ๋˜์–ด, ์‹ค์‹œ๊ฐ„ ์ง‘๊ณ„๊ฐ€ ์–ด๋ ต์Šต๋‹ˆ๋‹ค.
  • 24์‹œ๊ฐ„ ๊ธฐ์ค€ ์ง‘๊ณ„ ๋กœ์ง์ด ๋ณต์žกํ•˜๊ณ , ๊ณ„์‚ฐ ๋น„์šฉ์ด ๋†’์Šต๋‹ˆ๋‹ค.
  • ํŠธ๋ž˜ํ”ฝ์ด ๊ธ‰์ฆํ•˜๋ฉด DB ์ปค๋„ฅ์…˜์ด ๊ณ ๊ฐˆ๋˜์–ด ์‹œ์Šคํ…œ ์‘๋‹ต ์†๋„๊ฐ€ ์ €ํ•˜๋ฉ๋‹ˆ๋‹ค.

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

  • Redis ZSet์„ ํ™œ์šฉํ•˜์—ฌ ์‹ค์‹œ๊ฐ„ ๊ฒ€์ƒ‰์–ด ์ ์ˆ˜ ์ง‘๊ณ„
  • 1์‹œ๊ฐ„ ๋‹จ์œ„ ์Šค๋ƒ…์ƒท์„ ํ†ตํ•ด ๊ณผ๊ฑฐ 24์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ๋ฅผ ํšจ์œจ์ ์œผ๋กœ ๊ณ„์‚ฐ
  • ํ‚ค๋ณ„ ์ ์ˆ˜ ํ•ฉ์‚ฐ ๋ฐ ์ •๋ ฌ๋กœ ์ƒ์œ„ N๊ฐœ ๊ฒ€์ƒ‰์–ด๋ฅผ ๋น ๋ฅด๊ฒŒ ์กฐํšŒ

4. ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ

image image

[์ธ๊ธฐ ๊ฒ€์ƒ‰์–ด ์กฐํšŒ/๊ฐœ์„  ์ „]

  • TPS (์ดˆ๋‹น ์ฒ˜๋ฆฌ๋Ÿ‰): 113.5 / sec
    • Latency (ํ‰๊ท  ์‘๋‹ต ์‹œ๊ฐ„): 4,030 ms
    • Error Rate (์—๋Ÿฌ์œจ): 0 %

[๋ถ„์„ ๋ฐ ์ฃผ์š” ๋ฌธ์ œ์ ]

  • ๋†’์€ ์‘๋‹ต ์ง€์—ฐ ์‹œ๊ฐ„ (High Response Latency): ํ‰๊ท  ์‘๋‹ต ์‹œ๊ฐ„์ด 4์ดˆ(4,030ms)๋ฅผ ์ดˆ๊ณผํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ์‹ค์‹œ๊ฐ„์„ฑ์ด ์ค‘์š”ํ•œ ๋žญํ‚น ์„œ๋น„์Šค์—์„œ ์‚ฌ์šฉ์ž๊ฐ€ ์‹ฌํ•œ ์ง€์—ฐ์„ ์ฒด๊ฐํ•  ์ˆ˜ ์žˆ๋Š” ์ˆ˜์ค€์ž…๋‹ˆ๋‹ค.
  • ํ™•์žฅ์„ฑ ํ•œ๊ณ„ (Scalability Limitation): ์ดˆ๋‹น ์•ฝ 113๊ฑด์˜ ์š”์ฒญ๋งŒ ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅํ•ด, ํ–ฅํ›„ ์‚ฌ์šฉ์ž ์ฆ๊ฐ€๋‚˜ ์ด๋ฒคํŠธ ๋ฐœ์ƒ ์‹œ ๊ธ‰์ฆํ•˜๋Š” ํŠธ๋ž˜ํ”ฝ์„ ๊ฐ๋‹นํ•˜์ง€ ๋ชปํ•˜๊ณ  ์„œ๋น„์Šค ์žฅ์• ๋กœ ์ด์–ด์งˆ ์œ„ํ—˜์ด ๋†’์Šต๋‹ˆ๋‹ค.

image image

[์ธ๊ธฐ ๊ฒ€์ƒ‰์–ด ์กฐํšŒ/๊ฐœ์„  ํ›„]

  • TPS (์ดˆ๋‹น ์ฒ˜๋ฆฌ๋Ÿ‰): 132.6 / sec
  • Latency (ํ‰๊ท  ์‘๋‹ต ์‹œ๊ฐ„): 2,393 ms
  • Error Rate (์—๋Ÿฌ์œจ): 0 %

[๋ถ„์„ ๋ฐ ์ฃผ์š” ๊ฐœ์„ ์ ]

DB ๋ถ€ํ•˜๋ฅผ Redis๋กœ ์ด์ „ํ•˜์—ฌ 1์ฐจ์ ์ธ ์‹œ์Šคํ…œ ์•ˆ์ •์„ฑ ํ™•๋ณด์—๋Š” ์„ฑ๊ณตํ–ˆ์ง€๋งŒ, ์‘๋‹ต ์†๋„ ์ธก๋ฉด์—์„œ๋Š” ์ถ”๊ฐ€ ๊ฐœ์„ ์ด ํ•„์š”ํ•œ ์ƒํƒœ์ž…๋‹ˆ๋‹ค.

  • 1์ฐจ ๋ชฉํ‘œ ๋‹ฌ์„ฑ (ํ™•์žฅ์„ฑ ํ™•๋ณด): ์ฒ˜๋ฆฌ๋Ÿ‰(TPS)์€ 17% ์ฆ๊ฐ€ํ•˜๊ณ  ์‘๋‹ต ์‹œ๊ฐ„์€ 41% ๋‹จ์ถ•๋˜์–ด, ๋Œ€๊ทœ๋ชจ ํŠธ๋ž˜ํ”ฝ์„ ์—๋Ÿฌ ์—†์ด ์•ˆ์ •์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ํ™•์žฅ์„ฑ์„ ํ™•๋ณดํ–ˆ์Šต๋‹ˆ๋‹ค.
  • ํ–ฅํ›„ ๊ณผ์ œ (์‘๋‹ต ์†๋„ ์ตœ์ ํ™”): 1์ฐจ ๋ชฉํ‘œ๋Š” ๋‹ฌ์„ฑํ–ˆ์œผ๋‚˜, 2.4์ดˆ ์ˆ˜์ค€์˜ ์‘๋‹ต ์‹œ๊ฐ„์€ ์‹ค์‹œ๊ฐ„ ์„œ๋น„์Šค์—์„œ ๋” ๊ฐœ์„ ๋  ํ•„์š”๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ ๋‹จ๊ณ„๋กœ, Redis์˜ ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์ง‘๊ณ„ ๋ฐฉ์‹์„ ์ถ”๊ฐ€ ์ตœ์ ํ™”ํ•˜์—ฌ ์‘๋‹ต ์‹œ๊ฐ„์„ 1์ดˆ ๋ฏธ๋งŒ์œผ๋กœ ๋‹จ์ถ•ํ•˜๋Š” ๊ฒƒ์„ ๋ชฉํ‘œ๋กœ ํ•ฉ๋‹ˆ๋‹ค.

5. ํ•ด๊ฒฐ ์™„๋ฃŒ

  • Redis ZSet ํ™œ์šฉ ์‹ค์‹œ๊ฐ„ ์ง‘๊ณ„
    • ์‹œ๊ฐ„๋ณ„ Redis ํ‚ค(keyword_rankings:yyyy-MM-dd-HH)๋กœ ๊ฒ€์ƒ‰์–ด ์ ์ˆ˜ ๋ˆ„์ 
    • ๊ฐ ํ‚ค๋Š” 25์‹œ๊ฐ„ TTL ์ ์šฉ โ†’ ์ž๋™ ๋งŒ๋ฃŒ๋กœ ์˜ค๋ž˜๋œ ๋ฐ์ดํ„ฐ ์ œ๊ฑฐ
  • ๊ฒ€์ƒ‰์–ด ์ถ”๊ฐ€
    • ์ž…๋ ฅ ํ‚ค์›Œ๋“œ๋Š” ์‹ค์‹œ๊ฐ„์œผ๋กœ ํ•ด๋‹น ์‹œ๊ฐ„๋Œ€ Redis ZSet์— ์ ์ˆ˜ 1์”ฉ ์ฆ๊ฐ€
  • 24์‹œ๊ฐ„ ๋žญํ‚น ์กฐํšŒ
    • ํ˜„์žฌ ์‹œ๊ฐ„๋ถ€ํ„ฐ 23์‹œ๊ฐ„ ์ „๊นŒ์ง€ ์ด 24๊ฐœ์˜ ์‹œ๊ฐ„๋ณ„ Redis ํ‚ค ์กฐํšŒ
    • ZUNIONSTORE๋ฅผ ํ™œ์šฉํ•˜์—ฌ 24์‹œ๊ฐ„ ํ‚ค์˜ ์ ์ˆ˜๋ฅผ Redis์—์„œ ๋ฐ”๋กœ ํ•ฉ์‚ฐ
    • reverseRangeWithScores๋กœ ์ ์ˆ˜ ๋‚ด๋ฆผ์ฐจ์ˆœ ์กฐํšŒ โ†’ ์ƒ์œ„ N๊ฐœ ๋ฐ˜ํ™˜
    • 0 ์ดํ•˜ ์ ์ˆ˜๋Š” ๋žญํ‚น์—์„œ ์ œ์™ธ
  • ์Šค์ผ€์ค„๋Ÿฌ ๊ธฐ๋ฐ˜ DB ์Šค๋ƒ…์ƒท ์ €์žฅ (์•ˆ์ •์„ฑ ํ™•๋ณด)
    • ๋งค ์ •์‹œ(๋งค์‹œ๊ฐ„ 0๋ถ„ 0์ดˆ) Redis์—์„œ ํ‚ค์›Œ๋“œ ์ ์ˆ˜ ์กฐํšŒ โ†’ DB ์Šค๋ƒ…์ƒท ํ…Œ์ด๋ธ” ์ €์žฅ
    • Redis ์„œ๋ฒ„๊ฐ€ ๋‹ค์šด๋˜๋”๋ผ๋„ ์Šค๋ƒ…์ƒท ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ธ๊ธฐ ๊ฒ€์ƒ‰์–ด ์กฐํšŒ ๊ธฐ๋Šฅ์ด ์œ ์ง€
    • Redis ์„œ๋ฒ„ ๋ณต๊ตฌ ํ›„์—๋„ ์Šค๋ƒ…์ƒท ๋ฐ์ดํ„ฐ๋ฅผ ์ด์šฉํ•ด ์ •์ƒ ์„œ๋น„์Šค ๋ณต๊ตฌ ๊ฐ€๋Šฅ
  • ๋ฐ์ดํ„ฐ ์ •๋ฆฌ
    • 30์ผ ์ด์ƒ ๋œ DB ์Šค๋ƒ…์ƒท ๋ฐ์ดํ„ฐ ์ฃผ๊ธฐ์  ์‚ญ์ œ๋กœ ์šฉ๋Ÿ‰ ๊ด€๋ฆฌ

6. ํ–ฅํ›„ ๊ฐœ์„  ์‚ฌํ•ญ

1. DB ์Šค๋ƒ…์ƒท์„ ํ™œ์šฉํ•œ ์žฅ์•  ๋Œ€์‘ ๋Šฅ๋ ฅ ๊ฐ•ํ™”

ํ˜„์žฌ ๋งค์‹œ๊ฐ„ ์ƒ์„ฑํ•˜๊ณ  ์žˆ๋Š” DB ์Šค๋ƒ…์ƒท์„ ์žฌํ•ด ๋ณต๊ตฌ ๋ฐ ์„œ๋น„์Šค ์—ฐ์†์„ฑ ๋ณด์žฅ์— ์ ๊ทน์ ์œผ๋กœ ํ™œ์šฉํ•˜์—ฌ ์‹œ์Šคํ…œ์˜ ์•ˆ์ •์„ฑ์„ ํ•œ ๋‹จ๊ณ„ ๋Œ์–ด์˜ฌ๋ฆฝ๋‹ˆ๋‹ค.

  • Fallback ๋กœ์ง ๋„์ž…: Redis ์žฅ์• ๊ฐ€ ๋ฐœ์ƒํ•˜๋”๋ผ๋„ ์„œ๋น„์Šค๊ฐ€ ์ค‘๋‹จ๋˜์ง€ ์•Š๋„๋ก, ๊ฐ€์žฅ ์ตœ์‹  DB ์Šค๋ƒ…์ƒท์„ ์กฐํšŒํ•˜์—ฌ ์ธ๊ธฐ ๊ฒ€์ƒ‰์–ด ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” Fallback(๋Œ€์ฒด ์ž‘๋™) ๋กœ์ง์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ ์œ ์‹ค์„ ์ตœ๋Œ€ 1์‹œ๊ฐ„ ์ด๋‚ด๋กœ ์ œํ•œํ•˜๊ณ  ์„œ๋น„์Šค ๋‹ค์šดํƒ€์ž„์„ ์ตœ์†Œํ™”ํ•ฉ๋‹ˆ๋‹ค.
  • ์‹ ์†ํ•œ ๋ณต๊ตฌ ์ง€์›: ์žฅ์• ๋กœ๋ถ€ํ„ฐ Redis ์„œ๋ฒ„๊ฐ€ ๋ณต๊ตฌ๋˜์—ˆ์„ ๋•Œ, **DB์˜ ์ตœ์‹  ์Šค๋ƒ…์ƒท ๋ฐ์ดํ„ฐ๋ฅผ Redis์— ์ž๋™์œผ๋กœ ๋‹ค์‹œ ์ ์žฌ(Cache Warming)**ํ•˜๋Š” ํ”„๋กœ์„ธ์Šค๋ฅผ ๊ตฌ์ถ•ํ•˜์—ฌ ๋น ๋ฅด๊ณ  ์•ˆ์ •์ ์œผ๋กœ ์„œ๋น„์Šค๋ฅผ ์ •์ƒํ™”ํ•ฉ๋‹ˆ๋‹ค.

2. ๋žญํ‚น ์กฐํšŒ API ์„ฑ๋Šฅ ์ตœ์ ํ™”

ํ˜„์žฌ 2.4์ดˆ(2,393ms) ์ˆ˜์ค€์ธ ์ธ๊ธฐ ๊ฒ€์ƒ‰์–ด ์กฐํšŒ API์˜ ์‘๋‹ต ์‹œ๊ฐ„์„ 1์ดˆ ๋ฏธ๋งŒ์œผ๋กœ ๋‹จ์ถ•ํ•˜์—ฌ, ์‚ฌ์šฉ์ž์—๊ฒŒ ์‹ค์‹œ๊ฐ„์— ๊ฐ€๊นŒ์šด ๊ฒฝํ—˜์„ ์ œ๊ณตํ•˜๋Š” ๊ฒƒ์„ ๋ชฉํ‘œ๋กœ ํ•ฉ๋‹ˆ๋‹ค.

  • ์‹ค์‹œ๊ฐ„ ์ง‘๊ณ„ ๋กœ์ง ๊ฐœ์„ : ๋ชจ๋“  ์‚ฌ์šฉ์ž ์š”์ฒญ ์‹œ๋งˆ๋‹ค 24๊ฐœ์˜ ํ‚ค๋ฅผ ZUNIONSTORE๋กœ ์ง‘๊ณ„ํ•˜๋Š” ํ˜„์žฌ ๋ฐฉ์‹ ๋Œ€์‹ , ๋ณ„๋„์˜ ์Šค์ผ€์ค„๋Ÿฌ๊ฐ€ ๋žญํ‚น ๊ฒฐ๊ณผ๋ฅผ ์ฃผ๊ธฐ์ ์œผ๋กœ ๋ฏธ๋ฆฌ ๊ณ„์‚ฐํ•˜์—ฌ ์บ์‹œํ•ด๋‘๋Š” ๋ฐฉ์‹์œผ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ์กฐํšŒ ์†๋„๋ฅผ ํš๊ธฐ์ ์œผ๋กœ ๊ฐœ์„ ํ•ฉ๋‹ˆ๋‹ค.

3. ๊ฒ€์ƒ‰ ๊ด€๋ จ ๊ธฐ๋Šฅ ํ™•์žฅ

์•ˆ์ •ํ™”๋œ ๋žญํ‚น ์‹œ์Šคํ…œ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋” ํ’๋ถ€ํ•œ ๊ฐ€์น˜๋ฅผ ์ œ๊ณตํ•˜๊ธฐ ์œ„ํ•œ ์‹ ๊ทœ ๊ธฐ๋Šฅ ํ™•์žฅ์„ ๊ฒ€ํ† ํ•ฉ๋‹ˆ๋‹ค.

  • ์—ฐ๊ด€ ๊ฒ€์ƒ‰์–ด ์ถ”์ฒœ: ์‚ฌ์šฉ์ž๊ฐ€ ํŠน์ • ํ‚ค์›Œ๋“œ๋ฅผ ๊ฒ€์ƒ‰ํ–ˆ์„ ๋•Œ, ์—ฐ๊ด€์„ฑ์ด ๋†’์€ ๋‹ค๋ฅธ ํ‚ค์›Œ๋“œ๋ฅผ ํ•จ๊ป˜ ์ถ”์ฒœํ•˜์—ฌ ํƒ์ƒ‰์˜ ํŽธ์˜์„ฑ์„ ๋†’์ž…๋‹ˆ๋‹ค.
  • ์‹œ๊ฐ„๋Œ€๋ณ„ ํŠธ๋ Œ๋“œ ๋ถ„์„: ์‹œ๊ฐ„์˜ ํ๋ฆ„์— ๋”ฐ๋ฅธ ํŠน์ • ํ‚ค์›Œ๋“œ์˜ ์ธ๊ธฐ๋„ ๋ณ€ํ™”๋ฅผ ์‹œ๊ฐ์ ์œผ๋กœ ๋ณด์—ฌ์ฃผ์–ด, ์‚ฌ์šฉ์ž์™€ ํŒ๋งค์ž ๋ชจ๋‘์—๊ฒŒ ์œ ์šฉํ•œ ์ธ์‚ฌ์ดํŠธ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
โšก๏ธย ์บ์‹ฑ์„ ์ด์šฉํ•˜์—ฌ ์ƒˆ์†Œ์‹ ์กฐํšŒ๋ฅผ ๋”์šฑ ๋น ๋ฅด๊ฒŒ!

1. ๊ธฐ๋Šฅ ์†Œ๊ฐœ

์ƒˆ์†Œ์‹์€ ๋ฐœ๋งค ์˜ˆ์ •์ธ ๊ตฟ์ฆˆ๋‚˜ ์ด๋ฒคํŠธ ๊ฐ™์€ ๊ณต์‹์ ์ธ ์†Œ์‹์„ ์‚ฌ์šฉ์ž์—๊ฒŒ ์•Œ๋ฆฌ๋Š” ๊ณต๊ฐ„์œผ๋กœ,

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


2. ๋ฌธ์ œ ์ •์˜

๊ณต์‹ ์†Œ์‹์ด ์˜ฌ๋ผ์˜ค๋Š” ์ฐฝ๊ตฌ์ด๋ฏ€๋กœ ์‚ฌ์šฉ์ž๋Š” ์‹ ๊ทœ ์ƒˆ์†Œ์‹์— ๋งค์šฐ ๋ฏผ๊ฐํ•˜๊ฒŒ ๋ฐ˜์‘ํ•ฉ๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ ๋ฉ”์ผ ์•Œ๋ฆผ์„ ๋ฐ›์€ ๋‹ค์ˆ˜์˜ ์‚ฌ์šฉ์ž๊ฐ€ ์ฆ‰์‹œ ์ ‘์†ํ•  ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์•„, ์ด์— ๋”ฐ๋ฅธ ์‹œ์Šคํ…œ ์„ฑ๋Šฅ ์ €ํ•˜๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.


3. ํ•ด๊ฒฐ ๋ฐฉ์•ˆ : ์บ์‹ฑ์„ ํ†ตํ•œ ์„ฑ๋Šฅ ๊ฐœ์„ 

  • ๊ด€๋ฆฌ์ž๋งŒ ์ƒˆ์†Œ์‹ ๋“ฑ๋ก, ์ˆ˜์ •, ์‚ญ์ œ์˜ ๊ถŒํ•œ์ด ์žˆ์œผ๋ฉฐ, ์ƒˆ์†Œ์‹ ๋“ฑ๋ก์€ ๋นˆ๋ฒˆํ•˜์ง€ ์•Š๊ณ  ์ˆ˜์ • ๋ฐ ์‚ญ์ œ๋Š” ๋งค์šฐ ๋“œ๋ญ…๋‹ˆ๋‹ค.
  • ๊ณต์ง€ ์„ฑ๊ฒฉ์ƒ ๋งŽ์€ ์‚ฌ์šฉ์ž๊ฐ€ ๊ฐ™์€ ๋ฐ์ดํ„ฐ๋ฅผ ์งง์€ ์‹œ๊ฐ„์— ๋ฐ˜๋ณต์ ์œผ๋กœ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.
  • ์ƒˆ์†Œ์‹์€ ์ „์ฒด ์กฐํšŒ๋งŒ ๊ฐ€๋Šฅํ•˜๋ฉฐ, ์‚ฌ์šฉ์ž๋“ค์ด ๋‹ค์–‘ํ•œ ์†Œ์‹์„ ๊ณจ๊ณ ๋ฃจ ์ ‘ํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ฐœ์ธํ™”๋‚˜ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ์€ ์ œํ•œํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๋ณ€๊ฒฝ ์ฃผ๊ธฐ๋Š” ๋‚ฎ์ง€๋งŒ ์กฐํšŒ ๋นˆ๋„๊ฐ€ ๋†’์€ ๋ฐ์ดํ„ฐ์ด๋ฏ€๋กœ, ๋งค๋ฒˆ DB์—์„œ ์กฐํšŒํ•˜๋Š” ๋Œ€์‹  ์บ์‹œ์— ์ €์žฅํ•˜๋ฉด ์‘๋‹ต ์†๋„๋ฅผ ๋†’์ด๊ณ  ์„œ๋ฒ„ ๋ถ€ํ•˜๋ฅผ ํฌ๊ฒŒ ์ค„์ผ ์ˆ˜ ์žˆ๋‹ค๊ณ  ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค.

4. ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ

โœ”๏ธ ํ…Œ์ŠคํŠธ ์กฐ๊ฑด

[ํ™˜๊ฒฝ]

  • ๋กœ์ปฌ ํ™˜๊ฒฝ์—์„œ ํ…Œ์ŠคํŠธ: ์นฉ Apple M1 | ๋ฉ”๋ชจ๋ฆฌ 16GB
  • JMeter ์‚ฌ์šฉ
  • ๊ฒŒ์‹œ๊ธ€ 5000๊ฑด ์กด์žฌ

[์Šค๋ ˆ๋“œ ์†์„ฑ]

  • ์‚ฌ์šฉ์ž ์ˆ˜: 500๋ช…
  • Ramp-up ์‹œ๊ฐ„: 1์ดˆ
  • ๋ฃจํ”„ ์นด์šดํŠธ: 10ํšŒ

[๋Œ€์ƒ]

  • ์บ์‹ฑ ์ „ ์ƒˆ์†Œ์‹ ์ „์ฒด ์กฐํšŒ
  • ์ธ๋ฉ”๋ชจ๋ฆฌ ์บ์‹ฑ ์ ์šฉ ํ›„ ์ƒˆ์†Œ์‹ ์ „์ฒด์กฐํšŒ
  • Caffeine ์บ์‹ฑ ์ ์šฉ ํ›„ ์ƒˆ์†Œ์‹ ์ „์ฒด์กฐํšŒ
  • Redis ์บ์‹ฑ ์ ์šฉ ํ›„ ์ƒˆ์†Œ์‹ ์ „์ฒด์กฐํšŒ

โœ”๏ธ ํ…Œ์ŠคํŠธ ๋ชฉ์ 

  1. ๋กœ์ปฌ ํ™˜๊ฒฝ์—์„œ 500๋ช… ๋™์‹œ ์‚ฌ์šฉ์ž ๋ถ€ํ•˜๋ฅผ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•ด ๋‹ค์–‘ํ•œ ์บ์‹ฑ ๊ธฐ๋ฒ• ์ ์šฉ ์ „ํ›„์˜ ์ƒˆ์†Œ์‹ ์กฐํšŒ ์„ฑ๋Šฅ๊ณผ ์•ˆ์ •์„ฑ์„ ๋น„๊ต
  2. ์„œ๋น„์Šค ํ™˜๊ฒฝ์—์„œ ์ ์šฉ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์€ ์บ์‹œ ์†”๋ฃจ์…˜์„ ๋„์ถœ

โœ”๏ธ ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ

[Response Times Over Time(์‹œ๊ฐ„ ๊ฒฝ๊ณผ๋ณ„ ์‘๋‹ต ์‹œ๊ฐ„)]

| image | image | | [๊ฐœ์„  ์ „] : 3,000ms ์ด์ƒ ๊ตฌ๊ฐ„์—์„œ ์š”๋™
(y์ถ•: 04000ms/ x์ถ•: 033s) | [์ธ๋ฉ”๋ชจ๋ฆฌ ์บ์‹ฑ ์ ์šฉ] : 500660ms ๋ฒ”์œ„์—์„œ ์•ˆ์ •
(y์ถ•: 0
660ms/ x์ถ•: 07s) | | image | image | | [์นดํŽ˜์ธ ์บ์‹ฑ ์ ์šฉ] : 500600ms ๋ฒ”์œ„
(y์ถ•: 0600ms/ x์ถ•: 06s) | [๋ ˆ๋””์Šค ์บ์‹ฑ ์ ์šฉ] : 550700ms ๋ฒ”์œ„
(y์ถ•: 0
700ms/ x์ถ•: 0~6s) |

  • ์บ์‹ฑ ์ „: ํ‰๊ท  ์‘๋‹ต ์‹œ๊ฐ„์ด 3,000ms ์ด์ƒ์ด๋ฉฐ, ๋ชจ๋“  ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐ ์•ฝ 33์ดˆ๊ฐ€ ์†Œ์š”๋œ๋‹ค.
  • ์บ์‹ฑ ํ›„: ์‘๋‹ต ์‹œ๊ฐ„์ด ๋Œ€์ฒด๋กœ 400~700ms ์‚ฌ์ด๋กœ ๊ฐœ์„ ๋˜์—ˆ๊ณ , ๋ชจ๋“  ์š”์ฒญ ์ฒ˜๋ฆฌ ์‹œ๊ฐ„์€ ์•ฝ 10์ดˆ ๋‚ด์™ธ์ด๋‹ค.

[Transactions per Second(์ดˆ๋‹น ์ฒ˜๋ฆฌ ๊ฑด์ˆ˜)]

image [๊ฐœ์„  ์ „] : 100~190๊ฑด ์‚ฌ์ด์˜ ๊ฐ’์—์„œ ์š”๋™ (y์ถ•: 0~200/ x์ถ•:0~33s) image [์ธ๋ฉ”๋ชจ๋ฆฌ ์บ์‹ฑ ์ ์šฉ] : 950๊ฑด๊นŒ์ง€ ์ƒ์Šนํ•˜๋‹ค ํ•˜๊ฐ• (y์ถ•: 0~950/ x์ถ•:0~7s) image [์นดํŽ˜์ธ ์บ์‹ฑ ์ ์šฉ] : 1000๊ฑด๊นŒ์ง€ ์ƒ์Šนํ•˜๋‹ค ํ•˜๊ฐ• (y์ถ•: 0~1000/ x์ถ•:0~7s) image [๋ ˆ๋””์Šค ์บ์‹ฑ ์ ์šฉ] : 850๊ฑด๊นŒ์ง€ ์ƒ์Šนํ•˜๋‹ค ํ•˜๊ฐ• (y์ถ•: 0~900/ x์ถ•:0~6s)
  • ์บ์‹ฑ ์ „: ๊ทธ๋ž˜ํ”„๊ฐ€ ํ”๋“ค๋ฆฌ๋Š” ๋ชจ์–‘์„ ๋„๋ฉฐ, ๋Œ€์ฒด์ ์œผ๋กœ 100~190๊ฑด์˜ ์š”์ฒญ์„ ์ดˆ๋งˆ๋‹ค ์‘๋‹ตํ•˜๊ณ  ์žˆ๋‹ค.
  • ์บ์‹ฑ ํ›„: ๊ทธ๋ž˜ํ”„๊ฐ€ ์œ„๋กœ ๋ณผ๋กํ•œ ๊ณก์„ (3์ฐจ ํ•จ์ˆ˜ ํ˜•ํƒœ)์„ ๋ณด์ด๋ฉฐ, ์ดˆ๋‹น ์ตœ๋Œ€ ์•ฝ 1,000๊ฑด์˜ ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•œ๋‹ค.

**[์ดํ•ฉ ๋ณด๊ณ ์„œ]**
์บ์‹œ ์ข…๋ฅ˜ ํ‰๊ท  99% ์ตœ์†Œ ์ตœ๋Œ€ ์˜ค๋ฅ˜๋ฅ  ์ฒ˜๋ฆฌ๋Ÿ‰
์บ์‹œ ์—†์Œ 3010 4765 75 6009 0.00% 152.3/sec
์ธ๋ฉ”๋ชจ๋ฆฌ ์บ์‹œ 539 1092 12 2086 0.00% 765.3/sec
Caffeine ์บ์‹œ 506 1031 11 1405 0.00% 806.6/sec
Redis ์บ์‹œ 551 1097 13 1500 0.00% 733.9/sec
  1. ํ‰๊ท  ์‘๋‹ต ์‹œ๊ฐ„
    • Caffeine(506ms) < ์ธ๋ฉ”๋ชจ๋ฆฌ(539ms) < Redis(551ms) โ‰ช ์บ์‹œ ์—†์Œ(3010ms)
  2. 99ํผ์„ผํƒ€์ผ ์‘๋‹ต ์‹œ๊ฐ„ (์ตœ์•… ์„ฑ๋Šฅ)
    • Caffeine(1031ms) < ์ธ๋ฉ”๋ชจ๋ฆฌ(1092ms) โ‰ˆ Redis(1097ms) โ‰ช ์บ์‹œ ์—†์Œ(4765ms)
  3. ์‘๋‹ต ์‹œ๊ฐ„ ์ผ๊ด€์„ฑ
    • Caffeine โ‰ˆ Redis โ‰ˆ ์ธ๋ฉ”๋ชจ๋ฆฌ > ์บ์‹œ ์—†์Œ
  4. ์ฒ˜๋ฆฌ๋Ÿ‰ (TPS)
    • Caffeine(806.6) > ์ธ๋ฉ”๋ชจ๋ฆฌ(765.3) > Redis(733.9) โ‰ซ ์บ์‹œ ์—†์Œ(152.3)

โœ”๏ธย ย  ์ตœ์ข… ์„ ํƒ

๋ฐฉ์‹ ์žฅ์  ๋‹จ์  ์ ํ•ฉํ•œ ํ™˜๊ฒฝ
์บ์‹œ ์—†์Œ ๊ตฌํ˜„ ๋‹จ์ˆœ ์„ฑ๋Šฅยท์•ˆ์ •์„ฑ ๋ชจ๋‘ ์—ด์„ธ ๊ถŒ์žฅํ•˜์ง€ ์•Š์Œ
์ธ๋ฉ”๋ชจ๋ฆฌ ์บ์‹ฑ ๊ตฌํ˜„ ๊ฐ„๋‹จ, ๋‹จ์ผ ์ธ์Šคํ„ด์Šค์— ์ ํ•ฉ RedisยทCaffeine ๋Œ€๋น„ ์„ฑ๋Šฅ ๋‚ฎ์Œ ๋‹จ์ผ ์„œ๋ฒ„, ๊ฐ„๋‹จํ•œ ๊ตฌ์กฐ
Caffeine ์บ์‹ฑ ์†๋„ยท์ฒ˜๋ฆฌ๋Ÿ‰ ์šฐ์ˆ˜, ๋ณ€๋™ ํญ ์ตœ์†Œ ๋‹จ์ผ ์ธ์Šคํ„ด์Šค ํ•œ์ • ์ตœ๊ณ  ์„ฑ๋Šฅ์ด ํ•„์š”ํ•œ ๋‹จ์ผ ์„œ๋ฒ„
Redis ์บ์‹ฑ TPS ์œ ์ง€๋ ฅ ์šฐ์ˆ˜, ๋ฉ€ํ‹ฐ ์„œ๋ฒ„ ํ™˜๊ฒฝ ์ง€์› ๋„คํŠธ์›Œํฌ ์˜ค๋ฒ„ํ—ค๋“œ ๋ฐœ์ƒ ๋Œ€๊ทœ๋ชจ ๋ถ„์‚ฐ ํ™˜๊ฒฝ
  1. ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ Caffeine์ด ์‘๋‹ต ์‹œ๊ฐ„๊ณผ ์ฒ˜๋ฆฌ๋Ÿ‰ ๋ฉด์—์„œ ๊ฐ€์žฅ ์šฐ์ˆ˜ํ–ˆ์œผ๋‚˜(ํ‰๊ท ยทp99ยทTPS ๋ชจ๋‘ ์„ ๋‘), Caffeine๊ณผ ๋‹ค๋ฅธ ์บ์‹œ๋“ค ๊ฐ„ ์„ฑ๋Šฅ ๊ฒฉ์ฐจ๋Š” ํฌ์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.
  2. ์ธ๋ฉ”๋ชจ๋ฆฌ ์บ์‹œ๋Š” JVM ๋‚ด๋ถ€ ํ•œ์ •์œผ๋กœ ๋ฉ€ํ‹ฐ ์„œ๋ฒ„ ํ™˜๊ฒฝ์—์„œ ํ™•์žฅ์„ฑ๊ณผ ์šด์˜์— ํ•œ๊ณ„๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.
  3. ๋ณธ ํ”„๋กœ์ ํŠธ๋Š” ๋‹ค๋ฅธ ์˜์—ญ์—์„œ ๋ถ„์‚ฐ ํ™˜๊ฒฝ ํ™•์žฅ์„ฑ์„ ๊ณ ๋ คํ•ด ์ ํ•ฉํ•œ ๊ธฐ์ˆ ์„ ๋„์ž…ํ–ˆ์œผ๋ฉฐ, ์บ์‹ฑ๋„ ๊ฐ™์€ ๊ด€์ ์—์„œ ๊ฒฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ธฐ์—… ํ™˜๊ฒฝ์—์„œ๋Š” ์ด์™€ ๊ฐ™์€ ์ด์œ ๋กœ Redis ๋“ฑ ๋ถ„์‚ฐ ์บ์‹œ๋ฅผ ๋„๋ฆฌ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ ์บ์‹œ ๋ฏธ์‚ฌ์šฉ ๋Œ€๋น„ ํ˜„์ €ํ•œ ์„ฑ๋Šฅ ํ–ฅ์ƒ์ด ํ™•์ธ๋˜์—ˆ๊ณ , ์ „์ฒด์ ์ธ ์„ฑ๋Šฅ ๊ฒฉ์ฐจ๊ฐ€ ํฌ์ง€ ์•Š๋‹ค๋Š” ์ ๊ณผ ๋ถ„์‚ฐ ๊ตฌ์กฐ์—์„œ์˜ ์ ํ•ฉ์„ฑ์„ ์ข…ํ•ฉ์ ์œผ๋กœ ๊ณ ๋ คํ•˜์—ฌ ์ตœ์ข…์ ์œผ๋กœ Redis๋ฅผ ์„ ํƒํ–ˆ์Šต๋‹ˆ๋‹ค.

โœ”๏ธ ์ตœ์ข… ๊ตฌํ˜„

// build.gradle
dependencies {
	// redis ์บ์‹œ
	implementation 'org.springframework.boot:spring-boot-starter-data-redis'

	//json ์ง๋ ฌํ™”๋ฅผ ์œ„ํ•œ jackson
	implementation 'com.fasterxml.jackson.core:jackson-databind'
	implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
}
@Configuration
@EnableCaching
public class CacheConfig {

	@Bean
	public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
		// ์ž๋ฐ” ํƒ€์ž„ ๋ชจ๋“ˆ ๋“ฑ๋กํ•˜์—ฌ LocalDateTime -> ISO-8601 ํ˜•ํƒœ๋กœ ์ง๋ ฌํ™”
		ObjectMapper mapper = new ObjectMapper();
		mapper.registerModule(new JavaTimeModule());
		mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

		// ํƒ€์ž… ์ •๋ณด ํฌํ•จ (์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™” ์‹œ ํด๋ž˜์Šค ์ •๋ณด ์œ ์ง€)
		mapper.activateDefaultTyping(
			LaissezFaireSubTypeValidator.instance,
			ObjectMapper.DefaultTyping.NON_FINAL,
			JsonTypeInfo.As.PROPERTY
		);

		GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(mapper);

		// ์บ์‹œ ์„ค์ •
    RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
        .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) // ํ‚ค๋Š” ๋ฌธ์ž์—ด ์ง๋ ฌํ™”
        .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer)) // ๊ฐ’์€ JSON ์ง๋ ฌํ™”
        .disableCachingNullValues() // null์€ ์บ์‹œ์— ์ €์žฅํ•˜์ง€ ์•Š์Œ
        .entryTtl(Duration.ofMinutes(10)); // TTL 10๋ถ„

    return RedisCacheManager.builder(redisConnectionFactory)
        .cacheDefaults(cacheConfig) // ์บ์‹œ ์„ค์ •
        .build();
	}
}
@Service
@RequiredArgsConstructor
public class NewsfeedService {

	private final NewsfeedRepository newsfeedRepository;
	private final UserService userService;
	private final ImageService imageService;
	
	@Loggable
	@Transactional(readOnly = true)
	@Cacheable(
		value = "listCache",
		key = "#pageable.pageNumber + '-' + #pageable.pageSize + '-' + #pageable.sort.toString()"
	)
	public PageResponseDto<NewsfeedListResponse> getAllNewsfeeds(Pageable pageable) {

		// ์ƒˆ์†Œ์‹ ์ „์ฒด ์กฐํšŒ(์†Œํ”„ํŠธ๋”œ๋ฆฌํŠธ ๋นผ๊ณ )
		Page<Newsfeed> pagedNewsfeeds = newsfeedRepository.findByIsDeletedFalseWithFetchJoin(pageable);

		// ํŽ˜์ด์ง€์— ๋“ค์–ด๊ฐˆ ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ ์ผ๊ด„ ์กฐํšŒ
		Map<Long, Image> mainImageMap = imageService.getMainImagesForNewsfeeds(pagedNewsfeeds);

		List<NewsfeedListResponse> contentList = pagedNewsfeeds.stream().map(newsfeed -> {
        Image mainImage = mainImageMap.getOrDefault(newsfeed.getId(), null);
        return NewsfeedListResponse.of(newsfeed, mainImage != null ? mainImage.getImageUrl() : null);
    })
    .collect(Collectors.toList()); // ๊ฐ€๋ณ€ ๋ฆฌ์ŠคํŠธ๋กœ ๋ณ€ํ™˜

    return new PageResponseDto<>(
        new PageImpl<>(contentList, pageable, pagedNewsfeeds.getTotalElements())
    );
	}
}
  • JPA์—์„œ ๋ฐ˜ํ™˜ํ•˜๋Š” Page.getContent()๋Š” ๋ณดํ†ต ๋ถˆ๋ณ€ ๋ฆฌ์ŠคํŠธ๋กœ ๊ฐ์‹ธ์ ธ ์žˆ์–ด์„œ, ์ง์ ‘ ์บ์‹œ์— ๋„ฃ์œผ๋ฉด ์ง๋ ฌํ™” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒ
  • PageResponseDto ๋‚ด๋ถ€ content ํ•„๋“œ๋Š” Page.getContent()๋ฅผ ๋ฐ›์•„์„œ ์ดˆ๊ธฐํ™”ํ•˜๋ฏ€๋กœ, ์ „๋‹ฌํ•˜๋Š” Page์˜ ๋ฆฌ์ŠคํŠธ๋กœ ๊ฐ€๋ณ€ ๋ฆฌ์ŠคํŠธ ํ•„์š”
  • ๋”ฐ๋ผ์„œ, ์„œ๋น„์Šค ๋ ˆ์ด์–ด์—์„œ Page<T>์˜ ๋‚ด์šฉ์„ ๊ฐ€๋ณ€ ๋ฆฌ์ŠคํŠธ๋กœ ์ƒˆ๋กœ ์ˆ˜์ง‘ ํ›„, ๊ทธ ๋ฆฌ์ŠคํŠธ๋ฅผ ์‚ฌ์šฉํ•ด PageImpl์„ ์ƒˆ๋กœ ์ƒ์„ฑํ•˜์—ฌ ์ „๋‹ฌ
  • PageImpl์€ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ณต์‚ฌํ•˜๊ฑฐ๋‚˜ ๋ถˆ๋ณ€์œผ๋กœ ๊ฐ์‹ธ์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์—, ๊ฐœ๋ฐœ์ž๊ฐ€ ๋„˜๊ธด ๊ฐ€๋ณ€ ๋ฆฌ์ŠคํŠธ๋ฅผ ๊ทธ๋Œ€๋กœ ์œ ์ง€

5. ํ•ด๊ฒฐ ์™„๋ฃŒ

๊ฒฐ๊ณผ์™€ ํšจ๊ณผ

  • ๋ถ„์‚ฐ ํ™˜๊ฒฝ๊ณผ ํ™•์žฅ์„ฑ: ์„œ๋ฒ„ ๊ฐ„ ์บ์‹œ ๋ฐ์ดํ„ฐ ๊ณต์œ ๊ฐ€ ๊ฐ€๋Šฅํ•˜์—ฌ ๋ถ„์‚ฐ ํ™˜๊ฒฝ ๊ตฌ์ถ•๊ณผ ํ™•์žฅ์ด ์šฉ์ดํ•ฉ๋‹ˆ๋‹ค.
  • ๋†’์€ ์ฒ˜๋ฆฌ๋Ÿ‰๊ณผ ์•ˆ์ •์„ฑ: TPS ์œ ์ง€๋ ฅ์ด ๋›ฐ์–ด๋‚˜ ๋†’์€ ๋ถ€ํ•˜๋„ ์›ํ™œํžˆ ์ฒ˜๋ฆฌํ•˜๋ฉฐ, ๊ฐ€์šฉ์„ฑยท๋ณต์ œยท์˜์†์„ฑ ์˜ต์…˜์œผ๋กœ ์•ˆ์ •์ ์ธ ์šด์˜์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.
  • ์œ ์—ฐํ•œ ๊ด€๋ฆฌ: ์บ์‹œ ๋งค๋‹ˆ์ €๋ฅผ ํ†ตํ•œ ์„ค์ • ๊ด€๋ฆฌ์™€ ๋‹ค์–‘ํ•œ ๋ชจ๋‹ˆํ„ฐ๋ง ๋„๊ตฌ๋ฅผ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.
  • ๋‹ค์–‘ํ•œ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ ๋ฐ ํ˜ธํ™˜์„ฑ: Hash, List, Set, Sorted Set ๋“ฑ ๋‹ค์–‘ํ•œ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ๋ฅผ ์ €์žฅํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์—ฌ๋Ÿฌ ์–ธ์–ด์™€ ํ”Œ๋žซํผ๊ณผ ํ˜ธํ™˜๋ฉ๋‹ˆ๋‹ค.

์ฃผ์˜ ์‚ฌํ•ญ / ํ•œ๊ณ„

  • ์™ธ๋ถ€ ์„œ๋ฒ„ ์˜์กด: ์บ์‹œ๊ฐ€ ์™ธ๋ถ€ ์„œ๋ฒ„์— ์˜์กดํ•ฉ๋‹ˆ๋‹ค.
  • ์šด์˜ ๋ฐ ์œ ์ง€๋ณด์ˆ˜ ๋ถ€๋‹ด: ๊ด€๋ฆฌ ๋น„์šฉ๊ณผ ์„ค์ •, ์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™” ๊ตฌํ˜„์ด ํ•„์š”ํ•˜๋ฉฐ, ํŠœ๋‹ ํ•™์Šต์ด ์š”๊ตฌ๋ฉ๋‹ˆ๋‹ค.
  • ๋„คํŠธ์›Œํฌ ์œ„ํ—˜: ๋„คํŠธ์›Œํฌ ์˜ค๋ฒ„ํ—ค๋“œ ๋ฐœ์ƒ ๊ฐ€๋Šฅ์„ฑ๊ณผ ์žฅ์•  ์‹œ ์บ์‹œ ์ ‘๊ทผ ์ง€์—ฐ ๋˜๋Š” ์‹คํŒจ ์œ„ํ—˜์ด ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์ดˆ๊ธฐ ์ง€์—ฐ ๊ฐ€๋Šฅ์„ฑ: ์บ์‹œ ๋ฏธ์Šค ๋ฐœ์ƒ ์‹œ ์ดˆ๊ธฐ ์ง€์—ฐ(์ฝœ๋“œ ์Šคํƒ€ํŠธ)์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

6. ํ–ฅํ›„ ๊ฐœ์„  ์‚ฌํ•ญ

  • ์ฝœ๋“œ ์Šคํƒ€ํŠธ ๋ฌธ์ œ ํ•ด๊ฒฐ
    • ์ƒˆ ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ ์‹œ ์บ์‹œ์— ๋ฏธ๋ฆฌ ์ ์žฌํ•˜๋Š” ํ”„๋ฆฌํžˆํŒ… ์ „๋žต ๋„์ž…ํ•ฉ๋‹ˆ๋‹ค.
    • TTL์„ ๋™์ ์œผ๋กœ ์กฐ์ ˆํ•˜์—ฌ ์‹ ๊ทœ ์ƒˆ์†Œ์‹ ๋“ฑ๋ก ์‹œ ์บ์‹ฑ์„ ๋” ์˜ค๋žœ ์‹œ๊ฐ„(์˜ˆ: 1์‹œ๊ฐ„) ๋™์•ˆ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.
  • ์žฅ์•  ๋Œ€์‘ ๊ฐ•ํ™”
    • ๋„คํŠธ์›Œํฌ ์žฅ์• ๋‚˜ ์„œ๋ฒ„ ์˜ค๋ฅ˜ ์‹œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
    • ํด๋ฐฑ(fallback) ๋ฉ”์ปค๋‹ˆ์ฆ˜๊ณผ ํƒ€์ž„์•„์›ƒ ์„ค์ • ๊ฐ•ํ™”, ์„œ๋น„์Šค ์—ฐ์†์„ฑ์„ ํ™•๋ณดํ•ฉ๋‹ˆ๋‹ค.
  • ๊ฐ€์šฉ์„ฑยท๋ณต์ œยท์˜์†์„ฑ ์˜ต์…˜ ์ ์šฉ
    • ์ œ๊ณต๋˜๋Š” ์˜ต์…˜์„ ์ ๊ทน ๋„์ž…ํ•˜์—ฌ ์‹œ์Šคํ…œ ์•ˆ์ •์„ฑ๊ณผ ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ ๊ฐ•ํ™”ํ•ฉ๋‹ˆ๋‹ค.
โšก๏ธย ์žฌ๊ณ  ๊ด€๋ฆฌ ๋™์‹œ์„ฑ ์ด์Šˆ ํ•ด๊ฒฐ

1. ๊ธฐ๋Šฅ ์†Œ๊ฐœ

์ƒํ’ˆ ์ฃผ๋ฌธ ๋ฐ ์žฌ๊ณ  ๊ด€๋ฆฌ ์‹œ์Šคํ…œ

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


2. ๋ฌธ์ œ ์ •์˜

๊ธฐ์กด ์‹œ์Šคํ…œ์—์„œ๋Š” ๋™์‹œ์„ฑ ์ œ์–ด ๋ฉ”์ปค๋‹ˆ์ฆ˜์ด ์—†์–ด ์—ฌ๋Ÿฌ ์‚ฌ์šฉ์ž๊ฐ€ ๋™์‹œ์— ์ฃผ๋ฌธํ•  ๋•Œ ์‹ฌ๊ฐํ•œ ๋ฐ์ดํ„ฐ ๋ถˆ์ผ์น˜ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.

์ƒํ’ˆ: 5๊ฐœ
์‹ค์ œ ํŒ๋งค: 12๊ฐœ
์žฌ๊ณ  ์˜ค์ฐจ: 7๊ฐœ 

์ฃผ์š” ๋ฌธ์ œ์ 

  • ๋™์‹œ ์ ‘๊ทผ ์‹œ ์žฌ๊ณ  ๊ณ„์‚ฐ ์˜ค๋ฅ˜
  • ์žฌ๊ณ ๋ณด๋‹ค ๋งŽ์€ ์ˆ˜๋Ÿ‰ ํŒ๋งค (์˜ค๋ฒ„์…€๋ง)
  • ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ ๊นจ์ง์œผ๋กœ ์ธํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์˜ค๋ฅ˜

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

Redisson ๋ถ„์‚ฐ ๋ฝ + ๋น„๊ด€์  ๋ฝ ์กฐํ•ฉ

๋‹ค์ค‘ ์ธ์Šคํ„ด์Šค ํ™˜๊ฒฝ์—์„œ ์•ˆ์ „ํ•œ ๋™์‹œ์„ฑ ์ œ์–ด๋ฅผ ์œ„ํ•ด ์ด์ค‘ ๋ฝ ๋ฉ”์ปค๋‹ˆ์ฆ˜์„ ์‚ฌ์šฉ

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

  • Redisson ๋ถ„์‚ฐ ๋ฝ์œผ๋กœ ์ธ์Šคํ„ด์Šค ๊ฐ„ ๋™๊ธฐํ™”
  • DB ๋ ˆ๋ฒจ ๋น„๊ด€์  ๋ฝ์œผ๋กœ ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ ๋ณด์žฅ
  • REQUIRES_NEW ์ „ํŒŒ ์˜ต์…˜์œผ๋กœ ๋…๋ฆฝ์  ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ

์ ์šฉ ์ฝ”๋“œ

// ์ˆ˜์ • ์ „: ๋™์‹œ์„ฑ ์ด์Šˆ ๋ฐœ์ƒ ์ฝ”๋“œ
public void decreaseStock(Long itemId, Long quantity) {
    Item item = itemRepository.findById(itemId).orElseThrow();
    item.decreaseStock(quantity); // Race Condition ๋ฐœ์ƒ ์ง€์ 
}

// ์ˆ˜์ • ํ›„: ๋ถ„์‚ฐ ๋ฝ ์ ์šฉ
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void decreaseStock(Long itemId, Long quantity) {
    String lockKey = "Lock:" + itemId;
    RLock lock = redissonClient.getLock(lockKey);
    // ๋ถ„์‚ฐ ๋ฝ + ๋น„๊ด€์  ๋ฝ์œผ๋กœ ์•ˆ์ „ํ•œ ์žฌ๊ณ  ์ฐจ๊ฐ
}

4. ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ

Redis ๋ถ„์‚ฐ๋ฝ ์‚ฌ์šฉ์ „ ํ›„ ๋น„๊ต

๋™์‹œ์„ฑ ํ…Œ์ŠคํŠธ

์ดˆ๊ธฐ ์žฌ๊ณ  : 5๊ฐœ

๋™์‹œ ์š”์ฒญ : 100๋ช… ๊ฐ 1๋ฒˆ์”ฉ ์‹œํ–‰

19๋ฒˆ ์•„์ดํ…œ 100๋ช…์ด 1๋ฒˆ์”ฉ ๊ตฌ๋งค ์‹œ๋„ image

์‹œํ–‰ ๊ฒฐ๊ณผ

๋ถ„์‚ฐ๋ฝ ์ ์šฉ : 5๊ฐœ ์„ฑ๊ณต, 95๊ฐœ ์‹คํŒจ, ์žฌ๊ณ  0๊ฐœ

๋ถ„์‚ฐ๋ฝ ๋ฏธ์ ์šฉ : 12๊ฐœ ์„ฑ๊ณต, 88๊ฐœ ์‹คํŒจ, ์žฌ๊ณ  0๊ฐœ


5. ํ•ด๊ฒฐ ์™„๋ฃŒ

๋™์‹œ์„ฑ ์ด์Šˆ ํ•ด๊ฒฐ

  • ๋ฐ์ดํ„ฐ ์ •ํ•ฉ์„ฑ 100% ๋ณด์žฅ: ์žฌ๊ณ  ์˜ค์ฐจ ์™„์ „ ์ œ๊ฑฐ
  • ์˜ค๋ฒ„์…€๋ง ๋ฐฉ์ง€: ์žฌ๊ณ  ํ•œ๋„ ๋‚ด์—์„œ๋งŒ ์ฃผ๋ฌธ ์ฒ˜๋ฆฌ
  • ๋‹ค์ค‘ ์ธ์Šคํ„ด์Šค ์•ˆ์ •์„ฑ: MSA ํ™˜๊ฒฝ์—์„œ๋„ ์•ˆ์ „ํ•œ ๋™์‹œ์„ฑ ์ œ์–ด
  • ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๊ฐ•ํ™”: ๋ฝ ํš๋“ ์‹คํŒจ ์‹œ ๋ช…ํ™•ํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ œ๊ณต

๋™์‹œ์„ฑ ์ด์Šˆ ํ•ด๊ฒฐํ•จ์œผ๋กœ์„œ ์–ป์–ด์ง€๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ์ž„ํŒฉํŠธ

  • ๊ณ ๊ฐ ์‹ ๋ขฐ๋„ ํ–ฅ์ƒ (์žฌ๊ณ  ์˜ค๋ฅ˜๋กœ ์ธํ•œ ์ฃผ๋ฌธ ์ทจ์†Œ 0๊ฑด)
  • ์šด์˜ํŒ€ ์—…๋ฌด ํšจ์œจ์„ฑ ์ฆ๋Œ€ (์ˆ˜๋™ ์žฌ๊ณ  ์ •์ • ์ž‘์—… ๋ถˆํ•„์š”)
  • ๋งค์ถœ ์†์‹ค ๋ฐฉ์ง€ (์ •ํ™•ํ•œ ์žฌ๊ณ  ๊ด€๋ฆฌ๋กœ ํŒ๋งค ๊ธฐํšŒ ๊ทน๋Œ€ํ™”)

๐Ÿšจ ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…

๐Ÿšจย ์ปค๋„ฅ์…˜ํ’€ ๊ณ ๊ฐˆ ํ˜„์ƒ

1. ๋ฌธ์ œ ์ƒํ™ฉ

  • Mania Place์˜ ์ƒํ’ˆ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ ์ค‘, ๋™์‹œ ์‚ฌ์šฉ์ž ์ˆ˜ ์ฆ๊ฐ€ ์‹œ ๊ฒ€์ƒ‰ ์‹คํŒจ์œจ์ด ๊ธ‰์ฆํ•˜๊ณ  ์‘๋‹ต ์‹œ๊ฐ„์ด ํ˜„์ €ํžˆ ์ง€์—ฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
  • ๊ฒ€์ƒ‰ ์š”์ฒญ ์ฒ˜๋ฆฌ ๋งˆ๋น„์˜ ์ฃผ๋œ ์›์ธ์€ ์ธ๊ธฐ ๊ฒ€์ƒ‰์–ด ๋žญํ‚น ์ง‘๊ณ„ ๊ธฐ๋Šฅ์ด ๊ฒ€์ƒ‰ ํŠธ๋žœ์žญ์…˜์— ํฌํ•จ๋˜์–ด ์žˆ์—ˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

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

[ํ™˜๊ฒฝ]

Docker ์ปจํ…Œ์ด๋„ˆ ํ™˜๊ฒฝ

(์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— CPU 1์ฝ”์–ด, ๋ฉ”๋ชจ๋ฆฌ 1.5GB ์ž์› ํ• ๋‹น)

[์‚ฌ์šฉ ๋„๊ตฌ] Docker Compose, Apache JMeter

[๋ฐ์ดํ„ฐ ์กฐ๊ฑด]

  • ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ: ์•„์ดํ…œ = 1600๊ฐœ ์ƒ์„ฑ ํ›„ ์‹คํ—˜

[์Šค๋ ˆ๋“œ ์†์„ฑ]

  • ์‚ฌ์šฉ์ž ์ˆ˜: 30๋ช…
  • Ramp-up ์‹œ๊ฐ„: 1์ดˆ
  • ๋ฃจํ”„ ์นด์šดํŠธ: 10ํšŒ

[๋Œ€์ƒ]

  • ์•„์ดํ…œ ๊ฒ€์ƒ‰

[๋ชฉ์ ]

  • ์˜ค๋ฅ˜ ๋ฐœ์ƒ ํ™•์ธ
image

jp@gc - Transactions per Second

image

[๊ฒ€์ƒ‰๊ธฐ๋Šฅ]

  • TPS (์ดˆ๋‹น ์ฒ˜๋ฆฌ๋Ÿ‰): 1.4 / sec
  • Latency (ํ‰๊ท  ์‘๋‹ต ์‹œ๊ฐ„): 20,468 ms
  • Error Rate (์—๋Ÿฌ์œจ): 31.33 %

[๋ถ„์„ ๋ฐ ์ฃผ์š” ๋ฌธ์ œ์ ]

  • ์‹œ์Šคํ…œ ๋งˆ๋น„ ์ˆ˜์ค€์˜ ์„ฑ๋Šฅ: 31.33%์˜ ๋†’์€ ์—๋Ÿฌ์œจ๊ณผ 20์ดˆ๊ฐ€ ๋„˜๋Š” ์‘๋‹ต ์‹œ๊ฐ„์€ ์‚ฌ์‹ค์ƒ ์‹œ์Šคํ…œ์ด ๋ถ€ํ•˜๋ฅผ ์ „ํ˜€ ๊ฐ๋‹นํ•˜์ง€ ๋ชปํ•˜๊ณ  ๋งˆ๋น„ ์ƒํƒœ์— ์ด๋ฅด๋ €์Œ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.
  • DB ์ปค๋„ฅ์…˜ ํ’€ ๊ณ ๊ฐˆ: ๋ถ€ํ•˜๋ฅผ ๊ฒฌ๋””์ง€ ๋ชปํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ์‘๋‹ต ์ง€์—ฐ์ด ์›์ธ์ด ๋˜์–ด ์ปค๋„ฅ์…˜ ํ’€์ด ์™„์ „ํžˆ ๊ณ ๊ฐˆ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด๋กœ ์ธํ•ด ์ƒˆ๋กœ์šด ์š”์ฒญ์€ ํŠธ๋žœ์žญ์…˜์กฐ์ฐจ ์‹œ์ž‘ํ•˜์ง€ ๋ชปํ•˜๊ณ  ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.

[๊ฒฐ๋ก ]

์‹œ์Šคํ…œ ๋งˆ๋น„ ๋ฐœ์ƒ. 1์ดˆ ๋งŒ์— 30๋ช…์˜ ๋™์‹œ ์‚ฌ์šฉ์ž๊ฐ€ ์œ ์ž…๋˜๋Š” ์ŠคํŒŒ์ดํฌ ํŠธ๋ž˜ํ”ฝ ์ƒํ™ฉ์—์„œ, ์‹œ์Šคํ…œ์€ ๋ถ€ํ•˜๋ฅผ ์ „ํ˜€ ๊ฐ๋‹นํ•˜์ง€ ๋ชปํ•˜๊ณ  ์‚ฌ์‹ค์ƒ ๋งˆ๋น„ ์ƒํƒœ์— ์ด๋ฅด๋ €์Šต๋‹ˆ๋‹ค.

[์ƒ์„ธ ๋ถ„์„]

  • ์›์ธ: ๋ฐœ์ƒํ•œ ์˜ค๋ฅ˜์˜ 100%๊ฐ€ HikariCP ์ปค๋„ฅ์…˜ ํ’€ ํƒ€์ž„์•„์›ƒ์ด์—ˆ์Šต๋‹ˆ๋‹ค.

    ์ด๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ์‘๋‹ต์ด ๋„ˆ๋ฌด ๋А๋ ค ์ปค๋„ฅ์…˜์„ ์ œ๋•Œ ๋ฐ˜๋‚ฉํ•˜์ง€ ๋ชปํ–ˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

image
  • ๊ฒฐ๊ณผ: ์ปค๋„ฅ์…˜ ๋ถ€์กฑ์œผ๋กœ ์ƒˆ๋กœ์šด ์š”์ฒญ์€ ํŠธ๋žœ์žญ์…˜์กฐ์ฐจ ์‹œ์ž‘ํ•˜์ง€ ๋ชปํ•˜๊ณ  ์‹คํŒจํ–ˆ์œผ๋ฉฐ, ์ด๋กœ ์ธํ•ด 31.33%์˜ ์š”์ฒญ์ด ์œ ์‹ค๋˜๊ณ , ์‘๋‹ต ๊ฐ€๋Šฅํ•œ ์š”์ฒญ๋งˆ์ € ํ‰๊ท  20์ดˆ ์ด์ƒ์ด ์†Œ์š”๋˜์–ด ์ •์ƒ์ ์ธ ์„œ๋น„์Šค ์ œ๊ณต์ด ๋ถˆ๊ฐ€๋Šฅํ•จ์„ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค.
  • ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์Šค๋ ˆ๋“œ์™€ DB ์ปค๋„ฅ์…˜ ํ’€์˜ ์ž์› ๋ถˆ๊ท ํ˜•: 60๋ช…์˜ ๋™์‹œ ์‚ฌ์šฉ์ž ์š”์ฒญ์ด ์œ ์ž…๋˜์ž 100๊ฐœ ์ด์ƒ์˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์Šค๋ ˆ๋“œ๊ฐ€ ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ปค๋„ฅ์…˜ ํ’€(HikariCP)์˜ ์ตœ๋Œ€์น˜๋Š” 10๊ฐœ๋กœ ์„ค์ •๋˜์–ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.
  • ์ปค๋„ฅ์…˜ ๋ณ‘๋ชฉ ํ˜„์ƒ: ๊ฒ€์ƒ‰ ์š”์ฒญ์ด ๋ฐœ์ƒํ•  ๋•Œ๋งˆ๋‹ค ์‹คํ–‰๋˜๋Š” '์ธ๊ธฐ ๊ฒ€์ƒ‰์–ด ์นด์šดํŠธ ์—…๋ฐ์ดํŠธ' ๋กœ์ง์œผ๋กœ ์ธํ•ด DB ํŠธ๋žœ์žญ์…˜์ด ๊ธธ์–ด์กŒ์Šต๋‹ˆ๋‹ค. ๋จผ์ € ์ปค๋„ฅ์…˜์„ ์ฐจ์ง€ํ•œ 10๊ฐœ์˜ ์Šค๋ ˆ๋“œ๊ฐ€ ์ž‘์—…์„ ๋งˆ์น  ๋•Œ๊นŒ์ง€ ๋‹ค๋ฅธ ๋ชจ๋“  ์Šค๋ ˆ๋“œ(100๊ฐœ ์ด์ƒ)๋Š” ์ปค๋„ฅ์…˜์„ ๋ฌดํ•œ์ • ๋Œ€๊ธฐํ–ˆ์Šต๋‹ˆ๋‹ค.
  • ํƒ€์ž„์•„์›ƒ ๋ฐ ์˜ˆ์™ธ ๋ฐœ์ƒ: ๋Œ€๊ธฐํ•˜๋˜ ์Šค๋ ˆ๋“œ๋“ค์€ ๊ฒฐ๊ตญ ์ปค๋„ฅ์…˜ ํƒ€์ž„์•„์›ƒ(30์ดˆ)์„ ์ดˆ๊ณผํ•˜๊ฒŒ ๋˜์—ˆ๊ณ , ์ด๋กœ ์ธํ•ด SQLTransientConnectionException์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. Spring/JPA๋Š” ์ด ์˜ˆ์™ธ๋ฅผ CannotCreateTransactionException์œผ๋กœ ๊ฐ์‹ธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ์ „๋‹ฌ, ๊ฒฐ๊ณผ์ ์œผ๋กœ ๋Œ€๋Ÿ‰์˜ ๊ฒ€์ƒ‰ ์‹คํŒจ(์—๋Ÿฌ์œจ 31.33%)๋กœ ์ด์–ด์กŒ์Šต๋‹ˆ๋‹ค.
  • ๊ทผ๋ณธ ์›์ธ: ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ(์ฝ๊ธฐ)๊ณผ ๋žญํ‚น ์ง‘๊ณ„(์“ฐ๊ธฐ)๊ฐ€ ๋™์ผํ•œ DB ์ปค๋„ฅ์…˜ ํ’€์„ ๊ณต์œ ํ•˜๋ฉฐ ๊ฒฝํ•ฉํ•˜๋Š” ๊ตฌ์กฐ๊ฐ€ ๋ฌธ์ œ์˜ ํ•ต์‹ฌ์ด์—ˆ์Šต๋‹ˆ๋‹ค.

3. ๋ฌธ์ œ ํ•ด๊ฒฐ

  1. ์ปค๋„ฅ์…˜ ํ’€ ์ฆ์„ค ์‹œ๋„ (10 - > 20 ์ฆ์„ค)
image image image

์œ„ ํ…Œ์ŠคํŠธ์™€ ๋™์ผํ•˜๊ฒŒ ํ–ˆ์„๋•Œ ์ •์ƒ ์ž‘๋™์„ ํ™•์ธํ–ˆ์ง€๋งŒ

2๋ฐฐ ์ฆ๊ฐ€ํ•œ ์ปค๋„ฅ์…˜ ํ’€๋งŒํผ 2๋ฐฐ์˜ ์‚ฌ์šฉ์ž๋ฅผ ์ถ”๊ฐ€ ์š”์ฒญ ๋ณด๋ƒˆ์„๋•Œ(์‚ฌ์šฉ์ž 30 - > ์‚ฌ์šฉ์ž 60)

image image image

๋˜‘๊ฐ™์€ ์˜ค๋ฅ˜๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

  • DB ์ปค๋„ฅ์…˜ ํ’€(HikariCP) ์ตœ๋Œ€์น˜๋ฅผ ๋Š˜๋ ค ๋™์‹œ ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ ๋Šฅ๋ ฅ์„ ํ™•์žฅ
  • ๊ฒฐ๊ณผ: ๋™์‹œ ์š”์ฒญ ์ˆ˜๊ฐ€ ์ฆ๊ฐ€ํ•˜๋ฉด ์—ฌ์ „ํžˆ ์ปค๋„ฅ์…˜์ด ๊ณ ๊ฐˆ๋˜๊ณ  ๊ฒ€์ƒ‰ ์‹คํŒจ๊ฐ€ ๋ฐœ์ƒ โ†’ ๊ทผ๋ณธ์ ์ธ ํ•ด๊ฒฐ์ฑ… ์•„๋‹˜
  1. ๊ทผ๋ณธ์  ํ•ด๊ฒฐ: Redis ๋„์ž…
    • ์ธ๊ธฐ ๊ฒ€์ƒ‰์–ด ์ง‘๊ณ„ ๊ธฐ๋Šฅ์„ RDB โ†’ Redis ZSet์œผ๋กœ ์ด์ „
    • ํšจ๊ณผ:
      • Redis์˜ ์ธ๋ฉ”๋ชจ๋ฆฌ ๊ตฌ์กฐ๋กœ ๋น ๋ฅธ ์ฝ๊ธฐ/์“ฐ๊ธฐ ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ
      • ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ๊ณผ ๋žญํ‚น ์ง‘๊ณ„ ๊ธฐ๋Šฅ์˜ ์ž์› ๊ฒฝํ•ฉ ํ•ด์†Œ
      • DB๋Š” ํ•ต์‹ฌ ๊ฒ€์ƒ‰ ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ์—๋งŒ ์ง‘์ค‘ ๊ฐ€๋Šฅ โ†’ ์•ˆ์ •์„ฑ ํ™•๋ณด

###[๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ๊ฐœ์„  ๊ฒฐ๊ณผ]

image

jp@gc - Transactions per Second

image

[๊ฒ€์ƒ‰๊ธฐ๋Šฅ/๊ฐœ์„  ํ›„]

  • TPS (์ดˆ๋‹น ์ฒ˜๋ฆฌ๋Ÿ‰): 59.9 / sec
  • Latency (ํ‰๊ท  ์‘๋‹ต ์‹œ๊ฐ„): 380 ms
  • Error Rate (์—๋Ÿฌ์œจ): 0 %

[๋ถ„์„ ๋ฐ ์ฃผ์š” ๊ฐœ์„ ์ ]

  • ํš๊ธฐ์ ์ธ ์„ฑ๋Šฅ ํ–ฅ์ƒ: ํ‰๊ท  ์‘๋‹ต ์‹œ๊ฐ„์ด 380ms๋กœ ํฌ๊ฒŒ ๋‹จ์ถ•๋˜์—ˆ๊ณ , ์ดˆ๋‹น ์ฒ˜๋ฆฌ๋Ÿ‰(TPS)์€ ์•ฝ 42๋ฐฐ ์ฆ๊ฐ€ํ•˜์—ฌ ์‚ฌ์šฉ์ž์—๊ฒŒ ์พŒ์ ํ•œ ๊ฒ€์ƒ‰ ๊ฒฝํ—˜์„ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
  • ์‹œ์Šคํ…œ ์•ˆ์ •์„ฑ ์™„๋ฒฝ ํ™•๋ณด: ์—๋Ÿฌ์œจ์ด **0%**๋กœ ํ•ด์†Œ๋˜์—ˆ๊ณ , DB ์ปค๋„ฅ์…˜ ๋ณ‘๋ชฉ ํ˜„์ƒ์ด ์‚ฌ๋ผ์ ธ ๋Œ€๊ทœ๋ชจ ํŠธ๋ž˜ํ”ฝ์—๋„ ์•ˆ์ •์ ์œผ๋กœ ์„œ๋น„์Šค๋ฅผ ์šด์˜ํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋ฐ˜์„ ๋งˆ๋ จํ–ˆ์Šต๋‹ˆ๋‹ค.

4. ํšŒ๊ณ 

  • ์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„์˜ ์ค‘์š”์„ฑ: ์žฆ์€ ์“ฐ๊ธฐ์™€ ์‹ค์‹œ๊ฐ„ ์ง‘๊ณ„ ๊ธฐ๋Šฅ์„ ๋ฉ”์ธ DB์— ํ†ตํ•ฉํ•œ ๊ฒƒ์ด ์„ฑ๋Šฅ ์ €ํ•˜์˜ ์ง์ ‘์ ์ธ ์›์ธ์ด์—ˆ์Šต๋‹ˆ๋‹ค.
  • ์ž์› ์ดํ•ด์˜ ํ•„์š”์„ฑ: ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์Šค๋ ˆ๋“œ์™€ DB ์ปค๋„ฅ์…˜ ํ’€ ๊ฐ„์˜ ์ƒํ˜ธ์ž‘์šฉ์„ ์ดํ•ดํ•˜๋Š” ๊ฒƒ์ด ๋ณ‘๋ชฉ ํ˜„์ƒ์„ ํ•ด๊ฒฐํ•˜๋Š” ํ•ต์‹ฌ์ด์—ˆ์Šต๋‹ˆ๋‹ค.
  • ํ–ฅํ›„ ๋Œ€์‘ ๋ฐฉ์•ˆ: ์‹ค์‹œ๊ฐ„ ์ง‘๊ณ„ ๊ธฐ๋Šฅ์„ ๊ฐœ๋ฐœํ•  ๋•Œ๋Š” ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ๊ณผ์˜ ์˜์กด์„ฑ์„ ๋ถ„๋ฆฌํ•˜๋Š” ๊ฒƒ์„ ์šฐ์„ ์ ์œผ๋กœ ๊ณ ๋ คํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
๐Ÿšจย ํƒœ๊ทธ ์ €์žฅ ๋™์‹œ์„ฑ ๋ฌธ์ œ [https://www.notion.so/teamsparta/24e2dc3ef514805eac6cce2bafe76037](https://www.notion.so/24e2dc3ef514805eac6cce2bafe76037?pvs=21)

๋ณธ ๊ธ€์€ ํ•ด๋‹น ๊ฒŒ์‹œ๊ธ€์„ ์š”์•ฝํ•œ ๋‚ด์šฉ์ด๋ฉฐ, ์ž์„ธํ•œ ๋‚ด์šฉ์€ ์œ„ ๋งํฌ์—์„œ ํ™•์ธํ•˜์‹ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

1. ๋ฌธ์ œ ์ƒํ™ฉ

์ €ํฌ ํŒ€์—์„œ ๊ฐœ๋ฐœ ์ค‘์ธ ์„œ๋ธŒ์ปฌ์ฒ˜ ์ค‘๊ณ ๊ฑฐ๋ž˜ ์‚ฌ์ดํŠธ์—๋Š” ๊ฐœ์ธํ™” ์„œ๋น„์Šค๋ฅผ ์œ„ํ•œ 'ํƒœ๊ทธ' ๊ธฐ๋Šฅ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

์‚ฌ์šฉ์ž๋Š” ๊ด€์‹ฌ์‚ฌ์— ๋งž๋Š” ํƒœ๊ทธ๋ฅผ ์ตœ๋Œ€ 10๊ฐœ๊นŒ์ง€ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ด ํƒœ๊ทธ๋Š” ํšŒ์› ํ”„๋กœํ•„๊ณผ ์ค‘๊ณ  ๋ฌผํ’ˆ์— ๋ชจ๋‘ ์ ์šฉ๋˜์–ด ๊ฐœ์ธ ๋งž์ถค ์ƒํ’ˆ ์ถ”์ฒœ์— ํ™œ์šฉ๋ฉ๋‹ˆ๋‹ค.

๊ทธ๋Ÿฐ๋ฐ ์—ฌ๋Ÿฌ ์‚ฌ์šฉ์ž๊ฐ€ ๋™์‹œ์— ํƒœ๊ทธ๋ฅผ ์ €์žฅํ•˜๋Š” ๊ณผ์ •์—์„œ ๋™์‹œ์„ฑ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ํŠนํžˆ ํšŒ์›๊ฐ€์ž… ์‹œ์ ์ด๋‚˜ ์‚ฌ์šฉ์ž๊ฐ€ ํƒœ๊ทธ๋ฅผ ์ผ๊ด„ ์ˆ˜์ •ํ•  ๋•Œ, ๋ฌผํ’ˆ ๋“ฑ๋ก์‹œ ํƒœ๊ทธ๋ฅผ ์ €์žฅํ• ๋•Œ ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ ๋ฌธ์ œ์™€ ์ค‘๋ณต ์ €์žฅ ์˜ค๋ฅ˜๊ฐ€ ๋ฐ˜๋ณต์ ์œผ๋กœ ๋‚˜ํƒ€๋‚ฌ์Šต๋‹ˆ๋‹ค.

[ ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค ] ์ธ๊ธฐ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๊ตฟ์ฆˆ์˜ ํ•œ์ •ํŒ๋งค๋กœ ์ธํ•ด ํŒ๋งค ์‹œ์ž‘ ์ „ ๋Œ€๋Ÿ‰์˜ ์‚ฌ์šฉ์ž๊ฐ€ ๋™์‹œ์— ํšŒ์›๊ฐ€์ž…์„ ์‹œ๋„ํ•˜๋Š” ์ƒํ™ฉ์„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ํ–ˆ์Šต๋‹ˆ๋‹ค. ์•ฝ 100๋ช…์˜ ์‚ฌ์šฉ์ž๊ฐ€ ๋™์‹œ์— ๊ฐ€์ž…์„ ์ง„ํ–‰ํ•˜๋ฉฐ, ์ด ๊ณผ์ •์—์„œ ์ค‘๋ณต๋˜๋Š” ํƒœ๊ทธ๋ฅผ ์ž…๋ ฅํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ๊ฐ€์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

image.png

  • ์ „์ฒด ์š”์ฒญ ์ค‘ ์•ฝ 16%์˜ ์—๋Ÿฌ์œจ ๋ฐœ์ƒ
  • ์ฃผ์š” ์—๋Ÿฌ :
  1. Duplicate entry ... for key
    • ๊ฐ™์€ ํƒœ๊ทธ๋ช…์ด ์ด๋ฏธ ์กด์žฌํ•˜๋Š”๋ฐ INSERT ์‹œ๋„
    • MySQL์˜ UNIQUE ์ œ์•ฝ์กฐ๊ฑด์— ๊ฑธ๋ ค SQLIntegrityConstraintViolationException ๋ฐœ์ƒ

image.png

  1. Deadlock found when trying to get lock
    • ๋™์‹œ์— ์—ฌ๋Ÿฌ ํŠธ๋žœ์žญ์…˜์ด ๊ฐ™์€ ํ…Œ์ด๋ธ”/์ธ๋ฑ์Šค๋ฅผ ๊ฐฑ์‹ ํ•˜๋ ค๋‹ค๊ฐ€ MySQL์˜ ์ž ๊ธˆ ๊ฒฝํ•ฉ์œผ๋กœ ๋ฐ๋“œ๋ฝ ๋ฐœ์ƒ
    • MySQL์ด ๊ต์ฐฉ ์ƒํƒœ๋ฅผ ๊ฐ์ง€ํ•˜๊ณ  ํ•œ ํŠธ๋žœ์žญ์…˜์„ ๊ฐ•์ œ ๋กค๋ฐฑ

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

๋™์‹œ์„ฑ ๋ฐœ์ƒ ์ฃผ์š” ์›์ธ์ธ findOrCreateTag ๋ฉ”์„œ๋“œ ์ฝ”๋“œ์™€ ์ž‘๋™ ๋ฐฉ์‹ :

public Tag findOrCreateTag(String tagName) {
		return tagRepository.findByTagName(tagName) // SELECT
			.orElseGet(() -> tagRepository.save(Tag.of(tagName))); //INSERT
	}
  1. ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ํƒœ๊ทธ ์ด๋ฆ„์„ DB์—์„œ ๋จผ์ € ์กฐํšŒ๋ฅผ ํ•ฉ๋‹ˆ๋‹ค.
  2. ์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฉด ํ•ด๋‹น ํƒœ๊ทธ๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.

์ดˆ๊ธฐ ๊ฐœ๋ฐœ ๋‹น์‹œ์—๋Š” findOrCreateTag() ๋ฉ”์„œ๋“œ์—์„œ ํƒœ๊ทธ์˜ ์ค‘๋ณต ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•œ ํ›„ ์ €์žฅํ•˜๋Š” ๋กœ์ง์œผ๋กœ ๊ตฌํ˜„ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์—, ์ค‘๋ณต๋œ ํƒœ๊ทธ๊ฐ€ ์ƒ์„ฑ๋  ๊ฐ€๋Šฅ์„ฑ์„ ๊ณ ๋ คํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.

SELECT ์ฟผ๋ฆฌ๋กœ ํƒœ๊ทธ๋ฅผ ์กฐํšŒํ•œ ํ›„, ํƒœ๊ทธ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฉด **INSERT**์ฟผ๋ฆฌ๋กœ ํƒœ๊ทธ๋ฅผ ์ €์žฅํ•˜๋Š” ๋ฐฉ์‹์ธ๋ฐ,

์ด๋Ÿฌํ•œ ๋กœ์ง์„ ์—ฌ๋Ÿฌ ์š”์ฒญ์ด ๋™์‹œ์— ์‹คํ–‰ํ•  ๊ฒฝ์šฐ ์•„๋ž˜์™€ ๊ฐ™์€ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•œ๊ฒƒ์ด์—ˆ์Šต๋‹ˆ๋‹ค.

1๋ฒˆ ์˜ค๋ฅ˜ -> UNIQUE ์ œ์•ฝ์กฐ๊ฑด ์œ„๋ฐ˜ 
Thread A : SELECT tag WHERE name='์ž๋ฐ”' โ†’ ์—†์Œ
Thread B : SELECT tag WHERE name='์ž๋ฐ”' โ†’ ์—†์Œ

Thread A : INSERT '์ž๋ฐ”'
Thread B : INSERT '์ž๋ฐ”'  โ† UNIQUE ์ œ์•ฝ์กฐ๊ฑด ์˜ค๋ฅ˜ ๋ฐœ์ƒ , ๋กค๋ฐฑ

2๋ฒˆ ์˜ค๋ฅ˜ -> ๊ต์ฐฉ์ƒํƒœ ๋ฐœ์ƒ
Thread A : SELECT tag WHERE name='์ž๋ฐ”' โ†’ ์—†์Œ
Thread B : SELECT tag WHERE name='์Šคํ”„๋ง' โ†’ ์—†์Œ

Thread A : INSERT '์ž๋ฐ”'
Thread B : INSERT '์Šคํ”„๋ง'

Thread A : SELECT tag WHERE name='์Šคํ”„๋ง' โ†’ ์—†์Œ
Thread B : SELECT tag WHERE name='์ž๋ฐ”' โ†’ ์—†์Œ

Thread A : INSERT '์Šคํ”„๋ง' 
Thread B : INSERT '์ž๋ฐ”' -> ๊ต์ฐฉ์ƒํƒœ ๋ฐœ์ƒ , ๋กค๋ฐฑ

3. ๋ฌธ์ œ ํ•ด๊ฒฐ

1. S-lock, X-lock ๋ฝ ์ ์šฉ

  • S Lock๊ณผ X Lock ์ด๋ž€?

    MySQL์˜ ๋น„๊ด€์  ๋ฝ : S-lock๊ณผ X-lock

    โ†’ MySQL์˜ S-lock๊ณผ X-lock์€ ๋น„๊ด€์  ๋ฝ์„ ๊ตฌํ˜„ํ•œ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

    ๊ธฐ๋ณธ ๊ฐœ๋…

    ๊ตฌ๋ถ„ ์ด๋ฆ„ ์„ค๋ช… ๋™์‹œ์— ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ ํŠธ๋žœ์žญ์…˜
    S-lock Shared Lock (๊ณต์œ  ๋ฝ) ์ฝ๊ธฐ ์ „์šฉ ๋ฝ. ๋‹ค๋ฅธ ํŠธ๋žœ์žญ์…˜๋„ ์ฝ๊ธฐ๋Š” ๊ฐ€๋Šฅํ•˜์ง€๋งŒ, ์“ฐ๊ธฐ๋Š” ๋ถˆ๊ฐ€๋Šฅ ๋‹ค๋ฅธ S-lock์€ ํ—ˆ์šฉ / X-lock์€ ๋ถˆ๊ฐ€
    X-lock Exclusive Lock (๋ฐฐํƒ€ ๋ฝ) ์“ฐ๊ธฐ ์ „์šฉ ๋ฝ. ์ฝ๊ธฐยท์“ฐ๊ธฐ ๋ชจ๋‘ ๋‹ค๋ฅธ ํŠธ๋žœ์žญ์…˜์ด ์ ‘๊ทผ ๋ถˆ๊ฐ€ ์•„๋ฌด๋„ ์ ‘๊ทผ ๋ถˆ๊ฐ€

    ๋™์ž‘ ๋ฐฉ์‹

    S-lock (๊ณต์œ  ๋ฝ)

    • ์–ด๋–ค ํŠธ๋žœ์žญ์…˜์ด ๋ฐ์ดํ„ฐ์— S-lock์„ ๊ฑธ๋ฉด, ๋‹ค๋ฅธ ํŠธ๋žœ์žญ์…˜์€ ํ•ด๋‹น ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ์„ ์ˆ˜๋Š” ์žˆ์ง€๋งŒ ์ˆ˜์ •์€ ๋ชปํ•จ.

    • ์˜ˆ:

      SELECT * FROM tags WHERE tag_name = 'choco' LOCK IN SHARE MODE;

      โ†’ ๋‹ค๋ฅธ ํŠธ๋žœ์žญ์…˜์€ UPDATE๋‚˜ DELETE, INSERT(ํ•ด๋‹น ๋ ˆ์ฝ”๋“œ ํ‚ค ๊ฐ’) ๋ถˆ๊ฐ€.

    X-lock (๋ฐฐํƒ€ ๋ฝ)

    • ์–ด๋–ค ํŠธ๋žœ์žญ์…˜์ด ๋ฐ์ดํ„ฐ์— X-lock์„ ๊ฑธ๋ฉด, ๋‹ค๋ฅธ ํŠธ๋žœ์žญ์…˜์€ ์ฝ๊ธฐยท์“ฐ๊ธฐ ๋ชจ๋‘ ๋ถˆ๊ฐ€๋Šฅ.

    • ์˜ˆ:

      SELECT * FROM tags WHERE tag_name = 'choco' FOR UPDATE;

      โ†’ ํ•ด๋‹น ํ–‰์„ ๋‹ค๋ฅธ ํŠธ๋žœ์žญ์…˜์ด ์ฝ์œผ๋ ค๊ณ  ํ•ด๋„ ๋Œ€๊ธฐ ์ƒํƒœ.

    INSERT, UPDATE, DELETE ์‹œ ๊ธฐ๋ณธ ๋ฝ

    • INSERT: ์ƒˆ๋กœ์šด ํ–‰์— X-lock (๋ฐฐํƒ€ ๋ฝ)
    • UPDATE: ํ•ด๋‹น ํ–‰์— X-lock
    • DELETE: ํ•ด๋‹น ํ–‰์— X-lock
    • SELECT: ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ฝ ์—†์Œ (๋‹จ, LOCK IN SHARE MODE ๋˜๋Š” FOR UPDATE ์˜ต์…˜ ์‚ฌ์šฉ ์‹œ S/X-lock ๊ฑธ๋ฆผ)

๊ฐ€์žฅ ๋จผ์ € ์ƒ๊ฐํ•œ ํ•ด๊ฒฐ์ฑ…์€ ํƒœ๊ทธ ์กฐํšŒ ์‹œ์ ์— X-lock์„ ๊ฑธ์–ด์„œ ๋‹ค๋ฅธ ํŠธ๋žœ์žญ์…˜์ด ๋™์ผํ•œ ํƒœ๊ทธ์— ๋Œ€ํ•ด ์กฐํšŒ์กฐ์ฐจ ๋ชปํ•˜๊ฒŒ ํ•˜๋Š” ๊ฒƒ์ด์—ˆ์Šต๋‹ˆ๋‹ค.

S-lock(๊ณต์œ ๋ฝ)์˜ ๊ฒฝ์šฐ ์—ฌ๋Ÿฌ ํŠธ๋žœ์žญ์…˜์ด ๋™์‹œ์— ๊ฐ™์€ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์—, ์กฐํšŒ ํ›„ ๊ฐ์ž INSERT๋ฅผ ์‹œ๋„ํ•˜๋ฉด UNIQUE ์ œ์•ฝ์กฐ๊ฑด ์œ„๋ฐ˜์€ ๋ง‰์„ ์ˆ˜ ์žˆ๊ฒ ์ง€๋งŒ ์—ฌ์ „ํžˆ ๋ฐ๋“œ๋ฝ์ด ๋ฐœ์ƒํ•  ๊ฒƒ์œผ๋กœ ์˜ˆ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ์ƒ๋Œ€๋กœ ๋ ์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด S-lock๊ณผ X-lock์„ ๋ฒˆ๊ฐˆ์•„ ์ ์šฉํ•˜๋ฉฐ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค.

  • S-lock, X-lock ํ…Œ์ŠคํŠธ ๊ณผ์ •๊ณผ Gap lock

    S-lock ์ ์šฉ ํ…Œ์ŠคํŠธ:

    @Repository
    public interface TagRepository extends JpaRepository<Tag, Long> {
        @Lock(LockModeType.PESSIMISTIC_READ) // S Lock
        Optional<Tag> findByTagName(String tagName);
        boolean existsByTagName(String tagName);
    }

    findByTagName ๋ฉ”์„œ๋“œ์— S-lock์„ ์ ์šฉํ•˜๊ณ  ๋™์‹œ์„ฑ ํ…Œ์ŠคํŠธ๋ฅผ ์ˆ˜ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค.

    ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ :

    image.png

    image.png

    • ์˜ˆ์ƒ๋Œ€๋กœ ๋ฐ๋“œ๋ฝ์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.

    ์ฝ˜์†” ์‹œ๋ฎฌ๋ ˆ์ด์…˜ :

    ํŠธ๋žœ์žญ์…˜ A

    ํŠธ๋žœ์žญ์…˜ A

    ํŠธ๋žœ์žญ์…˜ B

    ํŠธ๋žœ์žญ์…˜ B

    ์ฝ˜์†”์—์„œ ์ง์ ‘ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•œ ๊ฒฐ๊ณผ, ๋‘ ํŠธ๋žœ์žญ์…˜์ด ๋™์‹œ์— 'hojun'์ด๋ผ๋Š” ๋™์ผํ•œ ํƒœ๊ทธ๋ช…์— ๋Œ€ํ•ด S-lock์„ ํš๋“ํ•œ ํ›„ ๊ฐ์ž INSERT๋ฅผ ์‹œ๋„ํ•˜๋ฉด์„œ ๋ฐ๋“œ๋ฝ์— ๋น ์ง€๋Š” ๊ฒƒ์„ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค. S-lock์œผ๋กœ๋Š” ์ด ๋ฌธ์ œ๋ฅผ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์—†๋‹ค๋Š”๊ฒƒ์„ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค.

    X-lock ์ ์šฉ ํ…Œ์ŠคํŠธ:

    @Repository
    public interface TagRepository extends JpaRepository<Tag, Long> {
        @Lock(LockModeType.PESSIMISTIC_WRITE) // X Lock
        Optional<Tag> findByTagName(String tagName);
        boolean existsByTagName(String tagName);
    }

    S-lock๊ณผ ๋™์ผํ•˜๊ฒŒ findByTagName ๋ฉ”์„œ๋“œ์— X-lock์„ ์ ์šฉํ•˜๊ณ  ๋™์‹œ์„ฑ ํ…Œ์ŠคํŠธ๋ฅผ ์ˆ˜ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค.

    ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ :

    image.png

    image.png

    • ์˜ˆ์ƒ๊ณผ ๋‹ค๋ฅด๊ฒŒ ์ด์ „๊ณผ ๋˜‘๊ฐ™์ด ๋ฐ๋“œ๋ฝ์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.

    ์ฝ˜์†” ์‹œ๋ฎฌ๋ ˆ์ด์…˜ :

    ํŠธ๋žœ์žญ์…˜ A

    ํŠธ๋žœ์žญ์…˜ A

    ํŠธ๋žœ์žญ์…˜ B

    ํŠธ๋žœ์žญ์…˜ B

    ์ฒ˜์Œ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ–ˆ์„ ๋•Œ ์˜์•„ํ–ˆ๋˜ ์ ์€, ํŠธ๋žœ์žญ์…˜ A์—์„œ X-lock์„ ๊ฑธ์—ˆ์Œ์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  ํŠธ๋žœ์žญ์…˜ B์—์„œ ๊ฐ™์€ ํƒœ๊ทธ๋ฅผ ์กฐํšŒํ•˜๋Š” ๊ฒƒ์ด ๊ฐ€๋Šฅํ–ˆ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  INSERT ๋‹จ๊ณ„์—์„œ๋Š” S-lock๊ณผ ๋™์ผํ•œ ๋ฉ”์ปค๋‹ˆ์ฆ˜์œผ๋กœ ๋ฐ๋“œ๋ฝ์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.

    '์ฟผ๋ฆฌ๋ฌธ์„ ์ž˜๋ชป ์ž‘์„ฑํ–ˆ๋‚˜?' ํ•˜๋Š” ์˜๋ฌธ์ด ๋“ค์–ด ๋‹ค์‹œ ๊ฒ€ํ† ํ•ด๋ณด๋‹ˆ, ๊ณ„์† SELECT๋กœ ์กฐํšŒํ•˜๋˜ ํƒœ๊ทธ๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฐ’์ด์—ˆ์Šต๋‹ˆ๋‹ค.

    ๊ฒ€์ฆ ํ…Œ์ŠคํŠธ: ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ํƒœ๊ทธ์— X-lock์„ ๊ฑธ๊ณ  ์กฐํšŒํ•ด๋ณด๋‹ˆ, ๋‹ค๋ฅธ ํŠธ๋žœ์žญ์…˜์—์„œ๋Š” ์กฐํšŒ๊ฐ€ ๋ธ”๋ก๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ ‡๋‹ค๋ฉด ์กด์žฌํ•˜์ง€ ์•Š๋Š” ํƒœ๊ทธ๋ฅผ INSERTํ•  ๋•Œ๋Š” ์–ด๋–ป๊ฒŒ ๋ฝ์ด ์ž‘๋™ํ–ˆ๋˜ ๊ฑธ๊นŒ์š”?

    ํ˜„์žฌ ๊ฑธ๋ ค์žˆ๋Š” ๋ฝ์˜ ์ข…๋ฅ˜๋ฅผ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด ๋‹ค์Œ ์ฟผ๋ฆฌ๋ฅผ ์‹คํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค

    SELECT object_schema, object_name, lock_type, lock_mode FROM performance_schema.data_locks;
    • S-lock ์ ์šฉ์‹œ

    image.png

    • X-lock ์ ์šฉ์‹œ

    image.png

    S๋ฝ๊ณผ X๋ฝ๋งŒ ๊ฑธ๋ฆฐ๊ฒŒ ์•„๋‹Œ GAP ์ด๋ผ๋Š” Lock์ด ๊ฑธ๋ฆฐ๊ฒƒ์„ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค.

    Gap Lock์˜ ์ž‘๋™ ์›๋ฆฌ :

    ์ธ๋ฑ์Šค ๋ ˆ์ฝ”๋“œ ์‚ฌ์ด์— ์กด์žฌํ•œ Gap๋“ค

    ์ธ๋ฑ์Šค ๋ ˆ์ฝ”๋“œ ์‚ฌ์ด์— ์กด์žฌํ•œ Gap๋“ค

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

๊ฒฐ๋ก : S-lock๊ณผ X-lock ๋ชจ๋‘ Gap Lock ๋ฉ”์ปค๋‹ˆ์ฆ˜์œผ๋กœ ์ธํ•ด, ์ฒ˜์Œ ์ƒ์„ฑ๋˜๋Š” ํƒœ๊ทธ๋“ค์— ๋Œ€ํ•œ ๋™์‹œ INSERT ์‹œ ๋ฐ๋“œ๋ฝ ๋ฌธ์ œ๋ฅผ ๊ทผ๋ณธ์ ์œผ๋กœ ํ•ด๊ฒฐํ•  ์ˆ˜ ์—†์Œ์„ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค.

2. INSERT IGNORE

3. @Retryable์„ ํ™œ์šฉํ•œ ์žฌ์‹œ๋„ ๋ฉ”์ปค๋‹ˆ์ฆ˜

REQUIRES_NEW๋ฅผ ํ†ตํ•œ ์ƒˆ๋กœ์šด ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ ์‹œ๋„:

๋ฝ ๊ธฐ๋ฐ˜ ํ•ด๊ฒฐ์ฑ…์˜ ํ•œ๊ณ„๋ฅผ ํ™•์ธํ•œ ํ›„, ํƒœ๊ทธ ์ €์žฅ ๋ถ€๋ถ„๋งŒ ์ƒˆ๋กœ์šด ํŠธ๋žœ์žญ์…˜์œผ๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ

: @Transactional(propagation = Propagation.REQUIRES_NEW)

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

์ปค๋„ฅ์…˜ ํ’€ ๋ถ€์กฑ ํ˜„์ƒ์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.

  • ๊ธฐ์กด ํŠธ๋žœ์žญ์…˜(ํšŒ์›๊ฐ€์ž…/๋ฌผํ’ˆ๋“ฑ๋ก)์ด ์ปค๋„ฅ์…˜์„ ์ ์œ ํ•œ ์ƒํƒœ
  • ํƒœ๊ทธ ์ €์žฅ์šฉ ์ƒˆ๋กœ์šด ํŠธ๋žœ์žญ์…˜์ด ์ถ”๊ฐ€ ์ปค๋„ฅ์…˜์„ ์š”๊ตฌ
  • ๋™์‹œ ์ ‘์†์ž๊ฐ€ ๋งŽ์„ ๋•Œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ปค๋„ฅ์…˜ ๋ถ€์กฑ์œผ๋กœ ์•„์˜ˆ ์ฒ˜๋ฆฌ๊ฐ€ ๋ถˆ๊ฐ€๋Šฅํ•œ ์ƒํ™ฉ ๋ฐœ์ƒ

@Retryable ์–ด๋…ธํ…Œ์ด์…˜ ์ ์šฉ:

๊ทธ๋ž˜์„œ ํŠธ๋žœ์žญ์…˜์„ ๋ถ„๋ฆฌํ•˜์ง€ ์•Š๊ณ , ๊ธฐ์กด์˜ ๋‹จ์ผ ํŠธ๋žœ์žญ์…˜ ๋‚ด์—์„œ ๋™์‹œ์„ฑ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ์žฌ์‹œ๋„ํ•˜๋Š” ๋ฐฉํ–ฅ์œผ๋กœ ์ ‘๊ทผํ–ˆ์Šต๋‹ˆ๋‹ค. Spring์—์„œ ์ œ๊ณตํ•˜๋Š” @Retryable ์–ด๋…ธํ…Œ์ด์…˜์„ ํ™œ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

	@Retryable(
		retryFor = {CannotAcquireLockException.class, SQLTransientException.class,
		 IllegalStateException.class},
		maxAttempts = 3, // ๊ธฐ๋ณธ๊ฐ’
		backoff = @Backoff(delay = 200) // ms ๋‹จ์œ„
	)
	@Transactional
	public UserRegisterResponse register(UserRegisterRequest userRegisterRequest,
	 UserRole userRole) {
				... // ํšŒ์›๊ฐ€์ž… ๋กœ์ง
		}

@Retryable์˜ ๋™์ž‘ ๋ฐฉ์‹:

  • retryFor: ์ง€์ •๋œ ์˜ˆ์™ธ(๋ฝ, ๋ฐœ์ƒ ์‹œ์—๋งŒ ์žฌ์‹œ๋„ ์ˆ˜ํ–‰
  • maxAttempts: ์ตœ๋Œ€ ์žฌ์‹œ๋„ ํšŸ์ˆ˜ ์„ค์ • (๊ธฐ๋ณธ๊ฐ’ 3ํšŒ)
  • backoff: ์žฌ์‹œ๋„ ๊ฐ„๊ฒฉ ์„ค์ • (200ms ์ง€์—ฐ์œผ๋กœ ์ˆœ๊ฐ„์ ์ธ ๋™์‹œ์„ฑ ์ถฉ๋Œ ํšŒํ”ผ)

image.png

image.png

ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ: ๋™์‹œ์„ฑ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ 200ms ๋”œ๋ ˆ์ด๋ฅผ ๋‘๊ณ  ์ž๋™์œผ๋กœ ์žฌ์‹œ๋„ํ•˜๊ฒŒ ํ•จ์œผ๋กœ์จ, JMeter ํ…Œ์ŠคํŠธ์—์„œ ๋ชจ๋“  ํšŒ์›๊ฐ€์ž…๊ณผ ๋ฌผํ’ˆ๋“ฑ๋ก์ด ์„ฑ๊ณต์ ์œผ๋กœ ์™„๋ฃŒ๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค.

@Retryable ์˜ ์žฅ์  :

  • ๋ณต์žกํ•œ ๋ฉ”์ปค๋‹ˆ์ฆ˜ ์—†์ด ๊ฐ„๋‹จํ•œ ์„ค์ •์œผ๋กœ ๋™์‹œ์„ฑ ๋ฌธ์ œ ํ•ด๊ฒฐ๊ฐ€๋Šฅ
  • ๊ธฐ์กด ํŠธ๋žœ์žญ์…˜ ๊ตฌ์กฐ ์œ ์ง€๋กœ ์ปค๋„ฅ์…˜ ํ’€ ๋ถ€์กฑ ๋ฌธ์ œ ๋ฐฉ์ง€

@Retryable ์˜ ๋‹จ์  :

๊ทผ๋ณธ์  ํ•ด๊ฒฐ์ด ์•„๋‹Œ ์šฐํšŒ ๋ฐฉ์‹

  • ๋™์‹œ์„ฑ ๋ฌธ์ œ์˜ ์›์ธ์„ ์ œ๊ฑฐํ•œ ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ์žฌ์‹œ๋„๋กœ ํšŒํ”ผํ•˜๋Š” ๋ฐฉ์‹
  • ๋™์‹œ ์ ‘์†์ž๊ฐ€ ๋งค์šฐ ๋งŽ์•„์งˆ ๊ฒฝ์šฐ ์žฌ์‹œ๋„ ํšŸ์ˆ˜๊ฐ€ ์ฆ๊ฐ€ํ•˜์—ฌ ์„ฑ๋Šฅ ์ €ํ•˜ ๊ฐ€๋Šฅ์„ฑ

์˜ˆ์ธก ๋ถˆ๊ฐ€๋Šฅํ•œ ์‘๋‹ต ์‹œ๊ฐ„

  • ์žฌ์‹œ๋„๋กœ ์ธํ•ด ์‚ฌ์šฉ์ž ์š”์ฒญ ์ฒ˜๋ฆฌ ์‹œ๊ฐ„์ด ๋ถˆ๊ทœ์น™ํ•ด์ง (200ms ~ 600ms ์ถ”๊ฐ€ ์ง€์—ฐ ๊ฐ€๋Šฅ)
  • ํ˜„์žฌ๋Š” 3๋ฒˆ ๋ฐ˜๋ณต์œผ๋กœ ๋ฌธ์ œ๊ฐ€ ์•ˆ์ƒ๊ฒผ์ง€๋งŒ ์ตœ์•…์˜ ๊ฒฝ์šฐ 3๋ฒˆ ๋ชจ๋‘ ์‹คํŒจํ•˜๋ฉด ์—ฌ์ „ํžˆ ์—๋Ÿฌ ๋ฐœ์ƒ ๊ฐ€๋Šฅ

4. ํšŒ๊ณ 

๊ฐœ์„ ๋ฐฉํ–ฅ :

ํ˜„์žฌ์˜ @Retryable ๋ฐฉ์‹์€ ์ž„์‹œ๋ฐฉํŽธ์ด๋ผ๊ณ  ํŒ๋‹จ๋˜์–ด, ํ–ฅํ›„์—๋Š” MySQL์˜ ๊ตฌ๋ฌธ์„ ํ™œ์šฉํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ ˆ๋ฒจ์—์„œ ๋™์‹œ์„ฑ ๋ฌธ์ œ๋ฅผ ๊ทผ๋ณธ์ ์œผ๋กœ ํ•ด๊ฒฐํ•˜๊ฑฐ๋‚˜, Redis๋ฅผ ํ™œ์šฉํ•œ ํƒœ๊ทธ ์บ์‹ฑ ๋ฐ ๋ถ„์‚ฐ ๋ฝ ๊ตฌํ˜„์„ ํ†ตํ•ด ๋” ์ •๊ตํ•œ ๋™์‹œ์„ฑ ์ œ์–ด๋ฅผ ๋„์ž…ํ•  ์˜ˆ์ •์ž…๋‹ˆ๋‹ค. ๋˜ํ•œ ์ธ๊ธฐ ํƒœ๊ทธ ์‚ฌ์ „์— ์ƒ์„ฑ(ํ˜„์‹ค์ ์œผ๋กœ ๊ฐ€์žฅ ์‰ฌ์šด ๋ฐฉ๋ฒ•)ํ•˜์—ฌ ์ค‘๋ณต์„ ์—†์• ๋Š” ๋ฐฉ๋ฒ•๊ณผ ์žฌ์‹œ๋„ ๋นˆ๋„ ํ…Œ์ŠคํŠธํ•˜์—ฌ ๋™์‹œ์„ฑ ์ถฉ๋Œ ์ž์ฒด๋ฅผ ์˜ˆ๋ฐฉํ•˜๊ณ  ์‹œ์Šคํ…œ ์„ฑ๋Šฅ์„ ์ง€์†์ ์œผ๋กœ ๊ด€์ฐฐํ•  ๊ณ„ํš์ž…๋‹ˆ๋‹ค.

๋А๋‚€์  :

์•ž์œผ๋กœ ์ƒˆ๋กœ์šด ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์„ค๊ณ„ํ•  ๋•Œ๋Š” ๋™์‹œ์„ฑ๊ณผ ํŠธ๋žœ์žญ์…˜์„ ํ•œ ๋ฒˆ ๋” ๊ผผ๊ผผํžˆ ๊ณ ๋ คํ•œ ๋’ค์— ๊ตฌํ˜„ํ•ด์•ผ๊ฒ ๋‹ค๊ณ  ๋А๊ผˆ์Šต๋‹ˆ๋‹ค.

๐Ÿšจย Redis ์ง๋ ฌํ™” ๋ฌธ์ œ

1. ๋ฌธ์ œ ์ƒํ™ฉ

์ฒ˜์Œ์—๋Š” Redis ์บ์‹œ ๋งค๋‹ˆ์ €์— null ๊ฐ’ ์บ์‹ฑ ๋ฐฉ์ง€์™€ TTL ๋งŒ๋ฃŒ ์‹œ๊ฐ„๋งŒ ์„ค์ •ํ•œ ๋’ค, ์ƒˆ์†Œ์‹ ์ „์ฒด ์กฐํšŒ ๋ฉ”์„œ๋“œ์— @Cacheable์„ ์ ์šฉํ•ด ์บ์‹ฑ์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ์ƒˆ์†Œ์‹ ์ „์ฒด ์กฐํšŒ๋ฅผ ํ˜ธ์ถœํ•˜์ž ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.

  • ์ตœ์ดˆ ์ฝ”๋“œ

    // build.gradle
    dependencies {
    	// redis ์บ์‹œ
    	implementation 'org.springframework.boot:spring-boot-starter-data-redis'
    
    	//json ์ง๋ ฌํ™”๋ฅผ ์œ„ํ•œ jackson
    	implementation 'com.fasterxml.jackson.core:jackson-databind'
    	implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
    }
    @Configuration
    @EnableCaching
    public class CacheConfig {
    
    	@Bean
    	public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
    		RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
    			.disableCachingNullValues() // null์€ ์บ์‹œ์— ์ €์žฅํ•˜์ง€ ์•Š์Œ
          .entryTtl(Duration.ofMinutes(10)); // TTL 10๋ถ„
    
    		return RedisCacheManager.builder(redisConnectionFactory)
    			.cacheDefaults(cacheConfig)
    			.build();
    	}
    }
    @Service
    @RequiredArgsConstructor
    public class NewsfeedService {
    
    	private final NewsfeedRepository newsfeedRepository;
    	private final UserService userService;
    	private final ImageService imageService;
    	
    	@Loggable
    	@Transactional(readOnly = true)
    	@Cacheable(
    		value = "listCache",
    		key = "#pageable.pageNumber + '-' + #pageable.pageSize + '-' + #pageable.sort.toString()"
    	)
    	public PageResponseDto<NewsfeedListResponse> getAllNewsfeeds(Pageable pageable) {
    
    		// ์ƒˆ์†Œ์‹ ์ „์ฒด ์กฐํšŒ(์†Œํ”„ํŠธ๋”œ๋ฆฌํŠธ ๋นผ๊ณ )
    		Page<Newsfeed> pagedNewsfeeds = newsfeedRepository.findByIsDeletedFalseWithFetchJoin(pageable);
    
    		// ํŽ˜์ด์ง€์— ๋“ค์–ด๊ฐˆ ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ ์ผ๊ด„ ์กฐํšŒ
    		Map<Long, Image> mainImageMap = imageService.getMainImagesForNewsfeeds(pagedNewsfeeds);
    
    		// NewsfeedListResponse์— ์ ์šฉ (+ ์ด๋ฏธ์ง€ ๋งตํ•‘)
    		Page<NewsfeedListResponse> dtoPage = pagedNewsfeeds.map(newsfeed -> {
    			Image mainImage = mainImageMap.getOrDefault(newsfeed.getId(), null);
    			return NewsfeedListResponse.of(newsfeed, mainImage.getImageUrl());
    		});
    
    		return new PageResponseDto<>(dtoPage);
    	}
    }
2025-08-12 14:52:35.328 15137 ERROR [http-nio-8080-exec-3] o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.data.redis.serializer.SerializationException: Cannot serialize] with root cause
java.lang.IllegalArgumentException: DefaultSerializer requires a Serializable payload but received an object of type [com.example.place.common.dto.PageResponseDto]

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

Redis ์บ์‹ฑ ๊ณผ์ •์—์„œ ๊ฐ์ฒด ์ง๋ ฌํ™” ์„ค์ •์ด ์—†์–ด์„œ ๊ธฐ๋ณธ JdkSerializationRedisSerializer ๋ฐฉ์‹์ด ์‚ฌ์šฉ๋˜์—ˆ๊ณ , ์ด๋กœ ์ธํ•ด DTO๊ฐ€ ์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™”๋˜์ง€ ๋ชปํ•ด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜์˜€์Šต๋‹ˆ๋‹ค.


3. ๋ฌธ์ œ ํ•ด๊ฒฐ

  1. Spring CacheManager ์ง๋ ฌํ™” ๋ฐฉ์‹์„ JSON์œผ๋กœ ๋ณ€๊ฒฝ

    // CacheConfig.cacheManager()
    
    // ์บ์‹œ ์„ค์ •
    RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
      .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) // ํ‚ค๋Š” ๋ฌธ์ž์—ด ์ง๋ ฌํ™”
      .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())) // ๊ฐ’์€ JSON ์ง๋ ฌํ™”
      .disableCachingNullValues() // null์€ ์บ์‹œ์— ์ €์žฅํ•˜์ง€ ์•Š์Œ
      .entryTtl(Duration.ofMinutes(10)); // TTL 10๋ถ„
    • ์ง๋ ฌํ™” ๋ฐฉ์‹์„ ์ง€์ •:
      • ํ‚ค ์ง๋ ฌํ™”๋Š” StringRedisSerializer ์‚ฌ์šฉํ•ด ๋ฌธ์ž์—ด๋กœ ์ง€์ •
      • ๊ฐ’ ์ง๋ ฌํ™” GenericJackson2JsonRedisSerializer ์‚ฌ์šฉํ•ด Jsonํ˜•ํƒœ๋กœ ์ง€์ •
  2. ๊ฐ€๋ณ€ ๋ฆฌ์ŠคํŠธ ๋ณ€ํ™˜

    // newsfeedService.getAllNewsfeeds()
    
    List<NewsfeedListResponse> contentList = pagedNewsfeeds.stream().map(newsfeed -> {
        Image mainImage = mainImageMap.getOrDefault(newsfeed.getId(), null);
        return NewsfeedListResponse.of(newsfeed, mainImage != null ? mainImage.getImageUrl() : null);
    })
    .collect(Collectors.toList()); // ๊ฐ€๋ณ€ ๋ฆฌ์ŠคํŠธ๋กœ ๋ณ€ํ™˜
    
    return new PageResponseDto<>(
        new PageImpl<>(contentList, pageable, pagedNewsfeeds.getTotalElements())
    );
    • Page์˜ content๊ฐ€ ๋ถˆ๋ณ€ ๋ฆฌ์ŠคํŠธ์ผ ๊ฒฝ์šฐ ์ง๋ ฌํ™” ๊ณผ์ •์—์„œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ํŒ๋‹จํ•˜์—ฌ, Collectors.toList()๋ฅผ ์‚ฌ์šฉํ•ด ๊ฐ€๋ณ€ ๋ฆฌ์ŠคํŠธ๋กœ ๋ณ€ํ™˜ ํ›„ ์‘๋‹ต๊ฐ’์œผ๋กœ ์ „๋‹ฌ
  3. LocalDateTime ์ง๋ ฌํ™” ์ฒ˜๋ฆฌ

    // CacheConfig.cacheManager()
    
    // ์ž๋ฐ” ํƒ€์ž„ ๋ชจ๋“ˆ ๋“ฑ๋กํ•˜์—ฌ LocalDateTime -> ISO-8601 ํ˜•ํƒœ๋กœ ์ง๋ ฌํ™”
    ObjectMapper mapper = new ObjectMapper();
    mapper.registerModule(new JavaTimeModule());
    mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
    
    GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(mapper);
    
    // ์บ์‹œ ์„ค์ •
    RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
    	.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) // ํ‚ค๋Š” ๋ฌธ์ž์—ด ์ง๋ ฌํ™”
    	.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer)) // ๊ฐ’์€ JSON ์ง๋ ฌํ™”
    	.disableCachingNullValues() // null์€ ์บ์‹œ์— ์ €์žฅํ•˜์ง€ ์•Š์Œ
    	.entryTtl(Duration.ofMinutes(10)); // TTL 10๋ถ„
    • java.time.LocalDateTime์€ ๊ธฐ๋ณธ ์„ค์ •์—์„œ ์ง๋ ฌํ™”๊ฐ€ ์ง€์›๋˜์ง€ ์•Š์•„ ์—ฌ์ „ํžˆ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒ
    • ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด JavaTimeModule์„ ๋“ฑ๋กํ•˜๊ณ , WRITE_DATES_AS_TIMESTAMPS ์˜ต์…˜์„ ๋น„ํ™œ์„ฑํ™”ํ•ด ISO-8601 ๋ฌธ์ž์—ด๋กœ ์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™”๋˜๋„๋ก ์„ค์ •
  4. ํƒ€์ž… ์ •๋ณด ์œ ์ง€

    // CacheConfig.cacheManager()
    
    // ํƒ€์ž… ์ •๋ณด ํฌํ•จ (์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™” ์‹œ ํด๋ž˜์Šค ์ •๋ณด ์œ ์ง€)
    mapper.activateDefaultTyping(
    	LaissezFaireSubTypeValidator.instance,
    	ObjectMapper.DefaultTyping.NON_FINAL,
    	JsonTypeInfo.As.PROPERTY
    );
    • ์ง๋ ฌํ™” ๊ฐ€๋Šฅ. ๊ทธ๋Ÿฌ๋‚˜ ์—ฌ์ „ํžˆ ์—ญ์ง๋ ฌํ™” ์‹œ ๋Ÿฐํƒ€์ž„์— ์ œ๋„ค๋ฆญ ํƒ€์ž… ์ •๋ณด๋ฅผ ์ œ๋Œ€๋กœ ์•Œ ์ˆ˜ ์—†์–ด, Jackson์ด LinkedHashMap ๊ฐ™์€ ๊ธฐ๋ณธ ๊ตฌ์กฐ๋กœ ๋ณ€ํ™˜ํ•ด๋ฒ„๋ฆฌ๋Š” ๋ฌธ์ œ ๋ฐœ์ƒ
    • ์ด๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ObjectMapper.activateDefaultTyping์„ ์‚ฌ์šฉํ•ด JSON์— ํด๋ž˜์Šค ํƒ€์ž… ์ •๋ณด๋ฅผ ํ•จ๊ป˜ ์ €์žฅํ•˜๋„๋ก ์„ค์ •
    1. DTO ์ƒ์„ฑ์ž์— @JsonCreator/@JsonProperty ์ ์šฉ
    // PageResponseDto
    
    @JsonCreator
    private PageResponseDto(
    	@JsonProperty("content") List<T> content,
    	@JsonProperty("page") int page,
    	@JsonProperty("totalPages") int totalPages) {
    	this.content = content;
    	this.page = page;
    	this.totalPages = totalPages;
    }
    • ๋ถˆ๋ณ€ ๊ฐ์ฒด๋‚˜ final ํ•„๋“œ๋งŒ ์žˆ๋Š” DTO๋Š” Jackson์ด ๊ธฐ๋ณธ ์ƒ์„ฑ์ž ์—†์ด ์—ญ์ง๋ ฌํ™”ํ•  ์ˆ˜ ์—†๋Š” ๋ฌธ์ œ ๋ฐœ์ƒ
    • @JsonCreator์™€ @JsonProperty๋ฅผ ์‚ฌ์šฉํ•ด ์—ญ์ง๋ ฌํ™” ์‹œ ํ•„๋“œ ๋งคํ•‘์ด ๊ฐ€๋Šฅํ•˜๋„๋ก ๋ช…์‹œ
  • ์ตœ์ข… ์ฝ”๋“œ

    // build.gradle
    dependencies {
    	// redis ์บ์‹œ
    	implementation 'org.springframework.boot:spring-boot-starter-data-redis'
    
    	//json ์ง๋ ฌํ™”๋ฅผ ์œ„ํ•œ jackson
    	implementation 'com.fasterxml.jackson.core:jackson-databind'
    	implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
    }
    @Configuration
    @EnableCaching
    public class CacheConfig {
    
    	@Bean
    	public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
    		// ์ž๋ฐ” ํƒ€์ž„ ๋ชจ๋“ˆ ๋“ฑ๋กํ•˜์—ฌ LocalDateTime -> ISO-8601 ํ˜•ํƒœ๋กœ ์ง๋ ฌํ™”
    		ObjectMapper mapper = new ObjectMapper();
    		mapper.registerModule(new JavaTimeModule());
    		mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
    
    		// ํƒ€์ž… ์ •๋ณด ํฌํ•จ (์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™” ์‹œ ํด๋ž˜์Šค ์ •๋ณด ์œ ์ง€)
    		mapper.activateDefaultTyping(
    			LaissezFaireSubTypeValidator.instance,
    			ObjectMapper.DefaultTyping.NON_FINAL,
    			JsonTypeInfo.As.PROPERTY
    		);
    
    		GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(mapper);
    
    		// ์บ์‹œ ์„ค์ •
        RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
            .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) // ํ‚ค๋Š” ๋ฌธ์ž์—ด ์ง๋ ฌํ™”
            .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer)) // ๊ฐ’์€ JSON ์ง๋ ฌํ™”
            .disableCachingNullValues() // null์€ ์บ์‹œ์— ์ €์žฅํ•˜์ง€ ์•Š์Œ
            .entryTtl(Duration.ofMinutes(10)); // TTL 10๋ถ„
    
        return RedisCacheManager.builder(redisConnectionFactory)
            .cacheDefaults(cacheConfig) // ์บ์‹œ ์„ค์ •
            .build();
    	}
    }
    @Service
    @RequiredArgsConstructor
    public class NewsfeedService {
    
    	private final NewsfeedRepository newsfeedRepository;
    	private final UserService userService;
    	private final ImageService imageService;
    	
    	@Loggable
    	@Transactional(readOnly = true)
    	@Cacheable(
    		value = "listCache",
    		key = "#pageable.pageNumber + '-' + #pageable.pageSize + '-' + #pageable.sort.toString()"
    	)
    	public PageResponseDto<NewsfeedListResponse> getAllNewsfeeds(Pageable pageable) {
    
    		// ์ƒˆ์†Œ์‹ ์ „์ฒด ์กฐํšŒ(์†Œํ”„ํŠธ๋”œ๋ฆฌํŠธ ๋นผ๊ณ )
    		Page<Newsfeed> pagedNewsfeeds = newsfeedRepository.findByIsDeletedFalseWithFetchJoin(pageable);
    
    		// ํŽ˜์ด์ง€์— ๋“ค์–ด๊ฐˆ ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ ์ผ๊ด„ ์กฐํšŒ
    		Map<Long, Image> mainImageMap = imageService.getMainImagesForNewsfeeds(pagedNewsfeeds);
    
    		List<NewsfeedListResponse> contentList = pagedNewsfeeds.stream().map(newsfeed -> {
            Image mainImage = mainImageMap.getOrDefault(newsfeed.getId(), null);
            return NewsfeedListResponse.of(newsfeed, mainImage != null ? mainImage.getImageUrl() : null);
        })
        .collect(Collectors.toList()); // ๊ฐ€๋ณ€ ๋ฆฌ์ŠคํŠธ๋กœ ๋ณ€ํ™˜
    
        return new PageResponseDto<>(
            new PageImpl<>(contentList, pageable, pagedNewsfeeds.getTotalElements())
        );
    	}
    }
    • JPA์—์„œ ๋ฐ˜ํ™˜ํ•˜๋Š” Page.getContent()๋Š” ๋ณดํ†ต ๋ถˆ๋ณ€ ๋ฆฌ์ŠคํŠธ๋กœ ๊ฐ์‹ธ์ ธ ์žˆ์–ด์„œ, ์ง์ ‘ ์บ์‹œ์— ๋„ฃ์œผ๋ฉด ์ง๋ ฌํ™” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Œ.
    • PageResponseDto ๋‚ด๋ถ€ content ํ•„๋“œ๋Š” Page.getContent()๋ฅผ ๋ฐ›์•„์„œ ์ดˆ๊ธฐํ™”ํ•˜๋ฏ€๋กœ, ์ „๋‹ฌํ•˜๋Š” Page์˜ ๋ฆฌ์ŠคํŠธ๊ฐ€ ๊ฐ€๋ณ€ ๋ฆฌ์ŠคํŠธ์—ฌ์•ผ ํ•จ.
    • ์„œ๋น„์Šค ๋ ˆ์ด์–ด์—์„œ Page<T>์˜ ๋‚ด์šฉ์„ ๊ฐ€๋ณ€ ๋ฆฌ์ŠคํŠธ๋กœ ์ƒˆ๋กœ ์ˆ˜์ง‘ ํ›„, ๊ทธ ๋ฆฌ์ŠคํŠธ๋ฅผ ์‚ฌ์šฉํ•ด PageImpl์„ ์ƒˆ๋กœ ๋งŒ๋“ค๋ฉด, ๊ทธ ๋‚ด๋ถ€ ๋ฆฌ์ŠคํŠธ๋Š” ๊ฐ€๋ณ€ ๋ฆฌ์ŠคํŠธ๊ฐ€ ๋จ.
    • PageImpl์€ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ณต์‚ฌํ•˜๊ฑฐ๋‚˜ ๋ถˆ๋ณ€์œผ๋กœ ๊ฐ์‹ธ์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์—, ๊ฐœ๋ฐœ์ž๊ฐ€ ๋„˜๊ธด ๊ฐ€๋ณ€ ๋ฆฌ์ŠคํŠธ๋ฅผ ๊ทธ๋Œ€๋กœ ์œ ์ง€ํ•จ.

4. ํšŒ๊ณ 

์ด ๊ณผ์ •์„ ํ†ตํ•ด Redis ์บ์‹œ์—์„œ DTO๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ์ €์žฅยท์กฐํšŒํ•˜๋ ค๋ฉด,

๊ฐ’ ์ง๋ ฌํ™” ์‹œ JSON ๋ณ€ํ™˜ + ํƒ€์ž… ์ •๋ณด ํฌํ•จ + ์—ญ์ง๋ ฌํ™”๋ฅผ ์œ„ํ•œ ์ƒ์„ฑ์ž ์„ค์ •์ด ํ•จ๊ป˜ ๊ณ ๋ ค๋˜์–ด์•ผ ํ•œ๋‹ค๋Š” ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿšจย ์ˆœํ™˜ ์ฐธ์กฐ ๋ฌธ์ œ

1. ๋ฌธ์ œ ์ƒํ™ฉ

  • OrderService์—์„œ ItemService ์˜์กด, ItemService์—์„œ๋„ ์žฌ๊ณ  ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•ด OrderService ์ฐธ์กฐ ํ•„์š”
  • ์–‘๋ฐฉํ–ฅ ์˜์กด์„ฑ์œผ๋กœ ์ธํ•œ ์ˆœํ™˜์ฐธ์กฐ ๋ฐœ์ƒ

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

  • OrderService โ†’ ItemService(์ƒํ’ˆ ์ •๋ณด ์กฐํšŒ)
  • ItemService โ†’ OrderService (์žฌ๊ณ  ๊ด€๋ฆฌ ๋กœ์ง)
  • ๋‘ ์„œ๋น„์Šค ๊ฐ„ ์ƒํ˜ธ ์ฐธ์กฐ๋กœ ์ธํ•œ ์˜์กด์„ฑ ์ˆœํ™˜ ๊ตฌ์กฐ

3. ๋ฌธ์ œ ํ•ด๊ฒฐ

  1. ์žฌ๊ณ  ๊ด€๋ฆฌ ๋กœ์ง์„ ๋ณ„๋„์˜ StockService๋กœ ๋ถ„๋ฆฌ
  2. OrderService โ†’ StockService, ItemService โ†’ StockService(์˜์กด์„ฑ ๊ตฌ์กฐ ๊ฐœ์„ )

4. ํšŒ๊ณ 

์ดˆ๊ธฐ ์„ค๊ณ„๋‹จ๊ณ„์—์„œ ์˜์กด์„ฑ ๊ด€๊ณ„๋ฅผ ์ถฉ๋ถ„ํžˆ ๊ฒ€ํ† ํ•˜์ง€ ๋ชปํ•˜์˜€์Šต๋‹ˆ๋‹ค

ํ–ฅํ›„ ์ƒˆ๋กœ์šด ์„œ๋น„์Šค ์ถ”๊ฐ€ ์‹œ ์˜์กด์„ฑ ๋‹ค์ด์–ด๊ทธ๋žจ ์‚ฌ์ „ ์ž‘์„ฑ์„ ์‹œ๋„ํ•ด ๋ณด๊ณ ์ž ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿšจย ๋ถ„์‚ฐ ๋ฝ ๊ตฌํ˜„ ์‹œ ํŠธ๋žœ์žญ์…˜ ์ „ํŒŒ ์ด์Šˆ

1. ๋ฌธ์ œ ์ƒํ™ฉ

๋ถ„์‚ฐ ๋ฝ๊ณผ ํŠธ๋žœ์žญ์…˜ ๊ฒฝ๊ณ„ ์ถฉ๋Œ ๋ฌธ์ œ ๋ถ„์‚ฐ ๋ฝ์„ ๋„์ž…ํ•˜๋ฉด์„œ ๊ธฐ์กด ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ ๋ฐฉ์‹๊ณผ ์ถฉ๋Œ์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ํŠนํžˆ OrderService์—์„œ StockService๋ฅผ ํ˜ธ์ถœํ•  ๋•Œ ํŠธ๋žœ์žญ์…˜ ๊ฒฝ๊ณ„์™€ ๋ฝ์˜ ์ƒ๋ช…์ฃผ๊ธฐ๊ฐ€ ๋งž์ง€ ์•Š์•„ ์˜ˆ์ƒ๊ณผ ๋‹ค๋ฅธ ๋™์ž‘์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.

๋ฐœ์ƒ ๋ฌธ์ œ

  • ๋ฝ์ด ํ•ด์ œ๋œ ํ›„ ํŠธ๋žœ์žญ์…˜์ด ์ปค๋ฐ‹๋˜์–ด ๋™์‹œ์„ฑ ์ œ์–ด๊ฐ€ ๋ฌด์˜๋ฏธํ•ด์ง
  • ํŠธ๋žœ์žญ์…˜ ๋กค๋ฐฑ ์‹œ ๋ฝ์€ ์ด๋ฏธ ํ•ด์ œ๋˜์–ด ๋‹ค๋ฅธ ์Šค๋ ˆ๋“œ๊ฐ€ ์ž˜๋ชป๋œ ๋ฐ์ดํ„ฐ์— ์ ‘๊ทผ
  • ์™ธ๋ถ€ ํŠธ๋žœ์žญ์…˜๊ณผ ๋‚ด๋ถ€ ํŠธ๋žœ์žญ์…˜์˜ ๊ฒฝ๊ณ„๊ฐ€ ๋ชจํ˜ธํ•˜์—ฌ ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ ๋ฌธ์ œ ๋ฐœ์ƒ

๋ฌธ์ œ ๋ฐœ์ƒ ๊ตฌ๊ฐ„

// ๋ฌธ์ œ ๋ฐœ์ƒ ๊ตฌ๊ฐ„
@Transactional  // ์™ธ๋ถ€ ํŠธ๋žœ์žญ์…˜ ์‹œ์ž‘
public CreateOrderResponseDto createOrder(...) {
    
    stockService.decreaseStock(itemId, quantity); // ๋‚ด๋ถ€์—์„œ ๋ฝ ํš๋“/ํ•ด์ œ
    
    // ์ฃผ๋ฌธ ์ƒ์„ฑ ๋ฐ ์ €์žฅ
    orderRepository.save(order);
    
    // ์™ธ๋ถ€ ํŠธ๋žœ์žญ์…˜ ์ปค๋ฐ‹ (๋ฝ์€ ์ด๋ฏธ ํ•ด์ œ๋œ ์ƒํ™ฉ)
}

๋ฌธ์ œ์˜ ์‹œํ€€์Šค

  1. ์‚ฌ์šฉ์žA: ์™ธ๋ถ€ ํŠธ๋žœ์žญ์…˜ ์‹œ์ž‘ โ†’ ๋ฝ ํš๋“ โ†’ ์žฌ๊ณ  ์ฐจ๊ฐ โ†’ ๋ฝ ํ•ด์ œ
  2. ์‚ฌ์šฉ์žB: ๋ฝ ํš๋“ ๊ฐ€๋Šฅ โ†’ ์žฌ๊ณ  ์ฐจ๊ฐ (์‚ฌ์šฉ์žA ํŠธ๋žœ์žญ์…˜ ๋ฏธ์ปค๋ฐ‹ ์ƒํƒœ)
  3. ์‚ฌ์šฉ์žA: ํŠธ๋žœ์žญ์…˜ ์ปค๋ฐ‹
  4. ์‚ฌ์šฉ์žB: ํŠธ๋žœ์žญ์…˜ ์ปค๋ฐ‹
  5. ๊ฒฐ๊ณผ: ๋™์‹œ์„ฑ ์ œ์–ด ์‹คํŒจ, ์žฌ๊ณ  ์˜ค์ฐจ ๋ฐœ์ƒ

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

ํŠธ๋žœ์žญ์…˜ ์ „ํŒŒ ๋ฐฉ์‹์˜ ๋ฌธ์ œ

  • ๊ธฐ์กด์—๋Š” ๊ธฐ๋ณธ ์ „ํŒŒ ๋ฐฉ์‹์ธ REQUIRED๋ฅผ ์‚ฌ์šฉํ–ˆ๋Š”๋ฐ, ์ด๋Š” ์™ธ๋ถ€ ํŠธ๋žœ์žญ์…˜์— ์ฐธ์—ฌํ•˜๋Š” ๋ฐฉ์‹์ด์—ˆ์Šต๋‹ˆ๋‹ค.

ํŠธ๋žœ์žญ์…˜๊ณผ ๋ฝ์˜ ์ƒ๋ช…์ฃผ๊ธฐ ๋ถˆ์ผ์น˜

  • ๋ฝ์˜ ์ƒ๋ช…์ฃผ๊ธฐ: ๋ฉ”์„œ๋“œ ์‹คํ–‰ ์‹œ๊ฐ„ ๋™์•ˆ๋งŒ ์œ ์ง€
  • ํŠธ๋žœ์žญ์…˜ ์ƒ๋ช…์ฃผ๊ธฐ: ์™ธ๋ถ€ ๋ฉ”์„œ๋“œ ์™„๋ฃŒ๊นŒ์ง€ ์œ ์ง€
  • ๊ฒฐ๊ณผ: ๋ฝ์ด ํ•ด์ œ๋œ ํ›„์—๋„ ํŠธ๋žœ์žญ์…˜์ด ์‚ด์•„์žˆ์–ด ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ ๋ณด์žฅ ๋ถˆ๊ฐ€

3. ๋ฌธ์ œ ํ•ด๊ฒฐ

REQUIRES_NEW ์ „ํŒŒ ์˜ต์…˜ ์ ์šฉ

๋…๋ฆฝ์ ์ธ ํŠธ๋žœ์žญ์…˜์„ ์ƒ์„ฑํ•˜์—ฌ ๋ฝ๊ณผ ํŠธ๋žœ์žญ์…˜์˜ ์ƒ๋ช…์ฃผ๊ธฐ๋ฅผ ์ผ์น˜์‹œ์ผฐ์Šต๋‹ˆ๋‹ค.

// ํ•ด๊ฒฐ๋œ ์ฝ”๋“œ
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void decreaseStock(Long itemId, Long quantity) {
    String lockKey = "Lock:" + itemId;
    RLock lock = redissonClient.getLock(lockKey);
    
    try {
        boolean acquired = lock.tryLock(3000, 10000, TimeUnit.MILLISECONDS);
        if (!acquired) {
            throw new CustomException(ExceptionCode.STOCK_LOCK_FAILED);
        }
        
        try {
            // ๋…๋ฆฝ์ ์ธ ํŠธ๋žœ์žญ์…˜์—์„œ DB ์ž‘์—… ์ˆ˜ํ–‰
            Item item = itemRepository.findByIdWithLock(itemId)
                .orElseThrow(() -> new CustomException(ExceptionCode.NOT_FOUND_ITEM));
            
            item.decreaseStock(quantity);
            // ๋ฉ”์„œ๋“œ ์ข…๋ฃŒ ์‹œ ํŠธ๋žœ์žญ์…˜ ์ฆ‰์‹œ ์ปค๋ฐ‹
            
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock(); // ํŠธ๋žœ์žญ์…˜ ์ปค๋ฐ‹ ํ›„ ๋ฝ ํ•ด์ œ
            }
        }
        
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new CustomException(ExceptionCode.OPERATION_INTERRUPTED);
    }
}

๊ฐœ์„ ์‚ฌํ•ญ

1. ํŠธ๋žœ์žญ์…˜ ๊ฒฉ๋ฆฌ

  • ์žฌ๊ณ  ๊ด€๋ฆฌ ๋กœ์ง์„ ๋…๋ฆฝ์ ์ธ ํŠธ๋žœ์žญ์…˜์œผ๋กœ ๋ถ„๋ฆฌ
  • ๋ฝ ํ•ด์ œ ์ „์— ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ณ€๊ฒฝ์‚ฌํ•ญ ํ™•์‹คํžˆ ์ปค๋ฐ‹
  • ์™ธ๋ถ€ ํŠธ๋žœ์žญ์…˜๊ณผ ๋ฌด๊ด€ํ•˜๊ฒŒ ์žฌ๊ณ  ์ฒ˜๋ฆฌ ์™„๋ฃŒ

2. ์›์ž์„ฑ ๋ณด์žฅ

  • ๋ฝ ํš๋“ โ†’ DB ์ž‘์—… โ†’ ํŠธ๋žœ์žญ์…˜ ์ปค๋ฐ‹ โ†’ ๋ฝ ํ•ด์ œ ์ˆœ์„œ ๋ณด์žฅ
  • ์ค‘๊ฐ„์— ์‹คํŒจ ์‹œ ํŠธ๋žœ์žญ์…˜ ๋กค๋ฐฑ ํ›„ ๋ฝ ํ•ด์ œ
  • ๋‹ค๋ฅธ ์Šค๋ ˆ๋“œ๋Š” ์™„์ „ํžˆ ์ฒ˜๋ฆฌ๋œ ๋ฐ์ดํ„ฐ๋งŒ ์ ‘๊ทผ ๊ฐ€๋Šฅ

4. ํšŒ๊ณ 

์ž˜ํ–ˆ๋˜ ์ 

  1. ๋ฌธ์ œ ๋ถ„์„
  • ๋‹จ์ˆœํžˆ "๋™์‹œ์„ฑ ๋ฌธ์ œ"๋กœ ๋๋‚ด์ง€ ์•Š๊ณ  ํŠธ๋žœ์žญ์…˜ ์ „ํŒŒ ๋ฐฉ์‹๊นŒ์ง€ ๊นŠ์ด ์žˆ๊ฒŒ ๋ถ„์„
  • ๋ฝ๊ณผ ํŠธ๋žœ์žญ์…˜์˜ ์ƒ๋ช…์ฃผ๊ธฐ ์ฐจ์ด๋ฅผ ์ •ํ™•ํžˆ ํŒŒ์•…ํ•˜๊ณ  ํ•ด๊ฒฐ๋ฐฉ์•ˆ ๋„์ถœ
  1. ์•ˆ์ •์„ฑ ๊ณ ๋ ค
  • REQUIRES_NEW๋กœ ์ธํ•œ ์„ฑ๋Šฅ ์˜ค๋ฒ„ํ—ค๋“œ๋ฅผ ์ธ์ง€ํ•˜๋ฉด์„œ๋„ ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ์„ ์šฐ์„ ์‹œํ•จ

์•„์‰ฌ์šด์ 

  1. ์ดˆ๊ธฐ ์„ค๊ณ„ ๋‹จ๊ณ„์—์„œ ์ง€์‹๋ถ€์กฑ
  • ๋ถ„์‚ฐ ๋ฝ ๋„์ž… ์‹œ ํŠธ๋žœ์žญ์…˜ ์ „ํŒŒ ๋ฐฉ์‹์— ๋Œ€ํ•œ ์‚ฌ์ „ ๊ฒ€ํ†  ๋ถ€์กฑ
  • ๋™์‹œ์„ฑ ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ์ถฉ๋ถ„ํžˆ ๊ตฌ์„ฑํ•˜์ง€ ๋ชปํ•ด ๋Šฆ์€ ๋ฐœ๊ฒฌ

๐Ÿ“… ์ผ์ •

๊ตฌ๋ถ„ ๊ธฐ๊ฐ„ ํ™œ๋™ ๋น„๊ณ 
๊ธฐํš 2025.07.16 ~ 07.20 ์•„์ด๋””์–ด ํšŒ์˜ ๋ฐ S.A ์ž‘์„ฑ S.A ํ”ผ๋“œ๋ฐฑ
MVP 2025.07.21 ~ 07.28 ์ตœ์†Œ ๊ธฐ๋Šฅ ๊ฐœ๋ฐœ MVP ์‹œ์—ฐ
์Šคํ”„๋ฆฐํŠธ 1์ฐจ 2025.07.29 ~ 08.05 ์ถ”๊ฐ€ ๊ธฐ๋Šฅ ๊ฐœ๋ฐœ ์Šคํ”„๋ฆฐํŠธ ํšŒ๊ณ  ํ›„ ๋ฐฐํฌ
์Šคํ”„๋ฆฐํŠธ 2์ฐจ 2025.08.06 ~ 08.14 ๊ณ ๋„ํ™” ์Šคํ”„๋ฆฐํŠธ ํšŒ๊ณ  ํ›„ ๋ฐฐํฌ & 5๋ถ„ ๋ธŒ๋ฆฌํ•‘
๋ฐœํ‘œ ์ค€๋น„ 2025.08.16 ~ 08.24 ๋ธŒ๋กœ์…” ๋ฐ ๋ฐœํ‘œ์ž๋ฃŒ ์ œ์ž‘ ์ตœ์ข… ํ”„๋กœ์ ํŠธ ์ œ์ถœ

๐Ÿงญ ํ–ฅํ›„ ๊ฐœ์„  ๊ณ„ํš

์‹ค์ œ ํƒ๋ฐฐ์‚ฌ API ์—ฐ๋™ - API ์ธ์ฆ ์ ˆ์ฐจ ๋ฐ ๋ฐฐ์†ก ์ถ”์  ๊ธฐ๋Šฅ ๊ตฌํ˜„

๊ฒฐ์ œ ์‹œ์Šคํ…œ ์™„์„ฑ - ์‹ค์ œ ๊ฒฐ์ œ API ์—ฐ๋™์œผ๋กœ ์™„์„ฑ๋„ ํ–ฅ์ƒ

ํšŒ์›๊ฐ€์ž… API ์„ฑ๋Šฅ ์ตœ์ ํ™” - ํ˜„์žฌ ๋А๋ฆฐ ์‘๋‹ต ์†๋„ ๊ฐœ์„ 

๊ฒ€์ƒ‰์–ด ๋žญํ‚น ๊ธฐ๋Šฅ ์ตœ์ ํ™” - ๊ฒ€์ƒ‰ ์„ฑ๋Šฅ ๋ฐ ์ •ํ™•๋„ ํ–ฅ์ƒ

์บ์‹ฑ fallback ๋ฉ”์ปค๋‹ˆ์ฆ˜ & Pre-heating ์‹œ์Šคํ…œ ๊ตฌ์ถ• - ์‹œ์Šคํ…œ ์•ˆ์ •์„ฑ ํ™•๋ณด

๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ์•Œ๋ฆผ ์ฒด๊ณ„ ๊ตฌ์ถ• - ์‹ค์‹œ๊ฐ„ ์‹œ์Šคํ…œ ์ƒํƒœ ๋ชจ๋‹ˆํ„ฐ๋ง

์ฑ„ํŒ… ๊ธฐ๋Šฅ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๋ณด๊ฐ• - ๋ฉ”์‹œ์ง€ ์ „์†ก ์‹คํŒจ ์‹œ ์žฌ์‹œ๋„ ๋กœ์ง ๋ฐ Dead Letter Queue ๋„์ž…

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ํ™•์ถฉ - ์šด์˜ ํ™˜๊ฒฝ ๊ฐ€์ •ํ•œ ๋‹ค์–‘ํ•œ ํ…Œ์ŠคํŠธ๋กœ ์•ˆ์ •์„ฑ ๊ฐ•ํ™”


About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors