๐Ÿšจ ์บ์‹œ๊ฐ€ ์žˆ๋Š”๋ฐ DB ์กฐํšŒ๊ฐ€ 10๋ฒˆ? Cache Stampede๋ฅผ Single Flight๋กœ ํ•ด๊ฒฐํ•œ ๊ณผ์ •

์ž„์„ ๊ตฌยท2026๋…„ 8์›” 5์ผ

์ŠคํŒŒ๋ฅดํƒ€

๋ชฉ๋ก ๋ณด๊ธฐ
19/28


์ธ๊ธฐ ์ˆ™์†Œ ์กฐํšŒ ์„ฑ๋Šฅ์„ ๊ฐœ์„ ํ•˜๊ธฐ ์œ„ํ•ด Redis ์‘๋‹ต ์บ์‹œ๋ฅผ ์ ์šฉํ–ˆ๋‹ค.

์บ์‹œ๊ฐ€ ์กด์žฌํ•˜๋Š” ๋™์•ˆ์—๋Š” ๋™์ผํ•œ ์ธ๊ธฐ ์ˆ™์†Œ ์š”์ฒญ์ด ๋“ค์–ด์™€๋„ Redis์— ์ €์žฅ๋œ ์™„์„ฑ ์‘๋‹ต์„ ๋ฐ”๋กœ ๋ฐ˜ํ™˜ํ•˜๊ธฐ ๋•Œ๋ฌธ์— Redis ๋žญํ‚น ์กฐํšŒ์™€ MySQL ์กฐํšŒ๋ฅผ ์ƒ๋žตํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

๊ทธ๋Ÿฐ๋ฐ ์บ์‹œ๋ฅผ ์ ์šฉํ–ˆ๋‹ค๊ณ  ํ•ด์„œ ๋ชจ๋“  ๋ฌธ์ œ๊ฐ€ ๋๋‚œ ๊ฒƒ์€ ์•„๋‹ˆ์—ˆ๋‹ค.

์บ์‹œ์˜ TTL์ด ๋งŒ๋ฃŒ๋˜๋Š” ์ˆœ๊ฐ„ ๋™์ผํ•œ ์š”์ฒญ์ด ๋™์‹œ์— ๋“ค์–ด์˜ค๋ฉด, ๋ชจ๋“  ์š”์ฒญ์ด Cache Miss๋กœ ํŒ๋‹จํ•˜์—ฌ ๊ฐ™์€ Redis ๋žญํ‚น๊ณผ MySQL ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜๋ณตํ•ด์„œ ์กฐํšŒํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

์ด๋ฒˆ ๊ธ€์—์„œ๋Š” ์ด Cache Stampede ๋ฌธ์ œ๋ฅผ ์ง์ ‘ ์žฌํ˜„ํ•˜๊ณ , ์—ฌ๋Ÿฌ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์„ ๋น„๊ตํ•œ ๋’ค Key ๋‹จ์œ„ Single Flight๋ฅผ ์ ์šฉํ•˜์—ฌ ์›๋ณธ ์กฐํšŒ๋ฅผ 10ํšŒ์—์„œ 1ํšŒ๋กœ ์ค„์ธ ๊ณผ์ •์„ ์ •๋ฆฌํ•œ๋‹ค.


๐Ÿ“Œ ๊ธฐ์กด ์ธ๊ธฐ ์ˆ™์†Œ ์กฐํšŒ ๊ตฌ์กฐ

RoomPick์˜ ์ธ๊ธฐ ์ˆ™์†Œ ์กฐํšŒ๋Š” ๋‹ค์Œ ์ˆœ์„œ๋กœ ๋™์ž‘ํ•œ๋‹ค.

์‚ฌ์šฉ์ž ์š”์ฒญ
โ†’ ์ธ๊ธฐ ์ˆ™์†Œ ์‘๋‹ต ์บ์‹œ ์กฐํšŒ
โ†’ Cache Hit๋ผ๋ฉด ์™„์„ฑ๋œ ์‘๋‹ต ๋ฐ˜ํ™˜
โ†’ Cache Miss๋ผ๋ฉด Redis ์ธ๊ธฐ ๋žญํ‚น ์กฐํšŒ
โ†’ MySQL์—์„œ ACTIVE ์ˆ™์†Œ ์ •๋ณด ์ผ๊ด„ ์กฐํšŒ
โ†’ Redis ๋žญํ‚น ์ˆœ์„œ ๋ณต์›
โ†’ ๋น„๊ณต๊ฐœยท์‚ญ์ œ ์ˆ™์†Œ ํ•„ํ„ฐ๋ง
โ†’ rank ๊ณ„์‚ฐ ๋ฐ ์‘๋‹ต DTO ์ƒ์„ฑ
โ†’ ์™„์„ฑ๋œ ์‘๋‹ต์„ Redis ์บ์‹œ์— ์ €์žฅ

์‘๋‹ต ์บ์‹œ ํ‚ค์—๋Š” ๋‹ค์Œ ๊ฐ’์ด ํฌํ•จ๋œ๋‹ค.

์กฐํšŒ ๊ธฐ๊ฐ„ + ๊ธฐ๊ฐ„ ๊ธฐ์ค€ ๋‚ ์งœ + limit

์˜ˆ๋ฅผ ๋“ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

DAILY + 2026-08-05 + limit 10
WEEKLY + 2026-08-04 + limit 10
DAILY + 2026-08-05 + limit 20

๊ธฐ๊ฐ„์ด๋‚˜ limit์ด ๋‹ค๋ฅด๋ฉด ์„œ๋กœ ๋‹ค๋ฅธ ์บ์‹œ๋กœ ๊ด€๋ฆฌ๋œ๋‹ค.


โฑ๏ธ ์ธ๊ธฐ ์ˆ™์†Œ ์บ์‹œ TTL์„ 60์ดˆ๋กœ ์ •ํ•œ ์ด์œ 

ํ˜„์žฌ ์ธ๊ธฐ ์ˆ™์†Œ ์‘๋‹ต ์บ์‹œ์˜ ๊ธฐ๋ณธ TTL์€ 60์ดˆ๋‹ค.

60์ดˆ๋Š” ์—…๊ณ„์—์„œ ๊ณตํ†ต์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š” ํ‘œ์ค€๊ฐ’์ด ์•„๋‹ˆ๋ผ, RoomPick์˜ ๋ฐ์ดํ„ฐ ํŠน์„ฑ๊ณผ ์„ฑ๋Šฅ ์š”๊ตฌ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ •ํ•œ ์ดˆ๊ธฐ ์ ˆ์ถฉ๊ฐ’์ด๋‹ค.

์ธ๊ธฐ ์ˆ™์†Œ ์ˆœ์œ„๋Š” ์˜ˆ์•ฝ ๊ฐ€๋Šฅ ์—ฌ๋ถ€๋‚˜ ๊ฒฐ์ œ ๊ธˆ์•ก์ฒ˜๋Ÿผ ๊ฑฐ๋ž˜ ์ •ํ•ฉ์„ฑ์„ ๊ฒฐ์ •ํ•˜๋Š” ๋ฐ์ดํ„ฐ๊ฐ€ ์•„๋‹ˆ๋‹ค.

์˜ˆ์•ฝ ๊ฐ€๋Šฅ ์—ฌ๋ถ€๊ฐ€ ์˜ค๋ž˜๋œ ๊ฒฝ์šฐ
โ†’ ์ด๋ฏธ ์˜ˆ์•ฝ๋œ ๊ฐ์‹ค์„ ์˜ˆ์•ฝํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ํŒ๋‹จํ•  ์ˆ˜ ์žˆ์Œ
โ†’ ๊ฑฐ๋ž˜ ์ •ํ•ฉ์„ฑ ๋ฌธ์ œ ๋ฐœ์ƒ

์ธ๊ธฐ ์ˆ™์†Œ ์ˆœ์œ„๊ฐ€ ์•ฝ 60์ดˆ ์˜ค๋ž˜๋œ ๊ฒฝ์šฐ
โ†’ ์ตœ๊ทผ ์กฐํšŒ์ˆ˜๊ฐ€ ์ˆœ์œ„์— ์ž ์‹œ ๋Šฆ๊ฒŒ ๋ฐ˜์˜๋จ
โ†’ ๊ฑฐ๋ž˜ ์ •ํ•ฉ์„ฑ์—๋Š” ์ง์ ‘์ ์ธ ์˜ํ–ฅ์ด ์—†์Œ

๋”ฐ๋ผ์„œ ์ธ๊ธฐ ์ˆ™์†Œ ์ˆœ์œ„์—๋Š” ์•ฝ๊ฐ„์˜ ์ตœ์‹ ์„ฑ ์ง€์—ฐ์„ ํ—ˆ์šฉํ•˜๊ณ , ๊ทธ ๋Œ€์‹  ๋ฐ˜๋ณต๋˜๋Š” Redis ๋žญํ‚น ์กฐํšŒ์™€ MySQL ์กฐํšŒ๋ฅผ ์ค„์ด๋Š” ๋ฐฉํ–ฅ์„ ์„ ํƒํ–ˆ๋‹ค.

TTL์ด ๋„ˆ๋ฌด ์งง๋‹ค๋ฉด

TTL์„ 10์ดˆ๋‚˜ 30์ดˆ๋กœ ์„ค์ •ํ•˜๋ฉด ์ตœ๊ทผ ๋žญํ‚น์„ ๋” ๋น ๋ฅด๊ฒŒ ๋ฐ˜์˜ํ•  ์ˆ˜ ์žˆ๋‹ค.

ํ•˜์ง€๋งŒ ๋‹ค์Œ ๋น„์šฉ์ด ์ฆ๊ฐ€ํ•œ๋‹ค.

์บ์‹œ ๋งŒ๋ฃŒ ํšŸ์ˆ˜ ์ฆ๊ฐ€
โ†’ Cache Miss ์ฆ๊ฐ€
โ†’ Redis ๋žญํ‚น ์กฐํšŒ ์ฆ๊ฐ€
โ†’ MySQL ์กฐํšŒ ์ฆ๊ฐ€
โ†’ DTO ์žฌ์ƒ์„ฑ ์ฆ๊ฐ€
โ†’ Cache Stampede ๋ฐœ์ƒ ๊ธฐํšŒ ์ฆ๊ฐ€

ํ˜„์žฌ ์ธ๊ธฐ ์ˆœ์œ„๋ฅผ ์ˆ˜ ์ดˆ ๋‹จ์œ„๋กœ ๊ฐฑ์‹ ํ•ด์•ผ ํ•œ๋‹ค๋Š” ์ œํ’ˆ ์š”๊ตฌ๋Š” ์—†์—ˆ๋‹ค.

TTL์ด ๋„ˆ๋ฌด ๊ธธ๋‹ค๋ฉด

TTL์„ 3๋ถ„์ด๋‚˜ 5๋ถ„์œผ๋กœ ์„ค์ •ํ•˜๋ฉด Cache Hit ๋น„์œจ์€ ๋†’์•„์งˆ ์ˆ˜ ์žˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ ์บ์‹œ๊ฐ€ ๋งŒ๋“ค์–ด์ง„ ์งํ›„ ๋žญํ‚น์ด ๋ณ€๊ฒฝ๋˜๋ฉด ์‚ฌ์šฉ์ž๋Š” ์ตœ๋Œ€ 5๋ถ„ ์ „์˜ ์ธ๊ธฐ ์ˆœ์œ„๋ฅผ ๋ณด๊ฒŒ ๋œ๋‹ค.

์ธ๊ธฐ ์ˆ™์†Œ๋Š” ์ตœ๊ทผ ์‚ฌ์šฉ์ž์˜ ๊ด€์‹ฌ ํ๋ฆ„์„ ๋ณด์—ฌ์ฃผ๋Š” ๊ธฐ๋Šฅ์ด๋ฏ€๋กœ, ์ดˆ๊ธฐ ์ •์ฑ…์—์„œ๋Š” ์ตœ๋Œ€ ๋ฐ˜์˜ ์ง€์—ฐ์„ ์•ฝ 1๋ถ„์œผ๋กœ ์ œํ•œํ–ˆ๋‹ค.

60์ดˆ๊ฐ€ ์ตœ์ ๊ฐ’์ด๋ผ๊ณ  ๋‹จ์ •ํ•œ ๊ฒƒ์€ ์•„๋‹ˆ๋‹ค.
์‹ค์ œ ํŠธ๋ž˜ํ”ฝ์„ ๊ธฐ์ค€์œผ๋กœ 30์ดˆ, 60์ดˆ, 180์ดˆ, 300์ดˆ ๋“ฑ์„ ๋น„๊ตํ•œ ๋’ค ์กฐ์ •ํ•  ์ˆ˜ ์žˆ๋Š” ์šด์˜ ์„ค์ •์œผ๋กœ ๊ด€๋ฆฌํ•œ๋‹ค.


๐Ÿ’ฅ Cache Stampede๋ž€?

์บ์‹œ๊ฐ€ ์œ ์ง€๋˜๋Š” ๋™์•ˆ์—๋Š” ๊ฐ™์€ ์š”์ฒญ์ด ์—ฌ๋Ÿฌ ๋ฒˆ ๋“ค์–ด์™€๋„ ๋ฌธ์ œ๊ฐ€ ์—†๋‹ค.

์š”์ฒญ 10๊ฑด
โ†’ ๋ชจ๋‘ Cache Hit
โ†’ Redis ๋žญํ‚น ์กฐํšŒ 0ํšŒ
โ†’ MySQL ์กฐํšŒ 0ํšŒ

๋ฌธ์ œ๋Š” ์บ์‹œ๊ฐ€ ๋งŒ๋ฃŒ๋˜๋Š” ์ˆœ๊ฐ„์ด๋‹ค.

์บ์‹œ๊ฐ€ ์‚ฌ๋ผ์ง„ ์งํ›„ ๋™์ผํ•œ ์š”์ฒญ 10๊ฑด์ด ๋™์‹œ์— ๋“ค์–ด์˜ค๋ฉด ๋ชจ๋“  ์š”์ฒญ์ด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํŒ๋‹จํ•  ์ˆ˜ ์žˆ๋‹ค.

์š”์ฒญ 1 โ†’ ์บ์‹œ๊ฐ€ ์—†๋‹ค
์š”์ฒญ 2 โ†’ ์บ์‹œ๊ฐ€ ์—†๋‹ค
์š”์ฒญ 3 โ†’ ์บ์‹œ๊ฐ€ ์—†๋‹ค
...
์š”์ฒญ 10 โ†’ ์บ์‹œ๊ฐ€ ์—†๋‹ค

๊ฐ ์š”์ฒญ์€ ๋‹ค๋ฅธ ์š”์ฒญ์ด ๋™์ผํ•œ ์‘๋‹ต์„ ๋งŒ๋“ค๊ณ  ์žˆ๋‹ค๋Š” ์‚ฌ์‹ค์„ ์•Œ ์ˆ˜ ์—†๋‹ค.

๊ฒฐ๊ตญ ๋ชจ๋“  ์š”์ฒญ์ด ๋™์ผํ•œ ์›๋ณธ ์กฐํšŒ๋ฅผ ์‹คํ–‰ํ•œ๋‹ค.

์š”์ฒญ 1 โ†’ Redis ์กฐํšŒ โ†’ MySQL ์กฐํšŒ โ†’ ์‘๋‹ต ์ƒ์„ฑ
์š”์ฒญ 2 โ†’ Redis ์กฐํšŒ โ†’ MySQL ์กฐํšŒ โ†’ ์‘๋‹ต ์ƒ์„ฑ
์š”์ฒญ 3 โ†’ Redis ์กฐํšŒ โ†’ MySQL ์กฐํšŒ โ†’ ์‘๋‹ต ์ƒ์„ฑ
...
์š”์ฒญ 10 โ†’ Redis ์กฐํšŒ โ†’ MySQL ์กฐํšŒ โ†’ ์‘๋‹ต ์ƒ์„ฑ

์ตœ์ข… ๊ฒฐ๊ณผ๋Š” ๊ฐ™์ง€๋งŒ Redis์™€ DB์—๋Š” ๋ถˆํ•„์š”ํ•œ ์š”์ฒญ์ด ๋ฐ˜๋ณต๋œ๋‹ค.

์ด์ฒ˜๋Ÿผ ์บ์‹œ๊ฐ€ ๋งŒ๋ฃŒ๋œ ์ˆœ๊ฐ„ ๋‹ค์ˆ˜์˜ ์š”์ฒญ์ด ๋™์‹œ์— ์›๋ณธ ๋ฐ์ดํ„ฐ๋กœ ๋ชฐ๋ฆฌ๋Š” ํ˜„์ƒ์„ Cache Stampede, ์ฆ‰ ์บ์‹œ ์‡„๋„ ํ˜„์ƒ์ด๋ผ๊ณ  ํ•œ๋‹ค.


๐Ÿฝ๏ธ ์‹๋‹น ์•ˆ๋‚ดํŒ์œผ๋กœ ์ดํ•ดํ•ด ๋ณด๊ธฐ

์‹๋‹น์— ์˜ค๋Š˜์˜ ์ธ๊ธฐ ๋ฉ”๋‰ด๊ฐ€ ์ ํžŒ ์•ˆ๋‚ดํŒ์ด ์žˆ๋‹ค๊ณ  ์ƒ๊ฐํ•ด ๋ณด์ž.

์•ˆ๋‚ดํŒ์ด ์œ ํšจํ•œ ๋™์•ˆ ์†๋‹˜๋“ค์€ ์•ˆ๋‚ดํŒ๋งŒ ํ™•์ธํ•˜๋ฉด ๋œ๋‹ค.

์†๋‹˜ 10๋ช…
โ†’ ์•ˆ๋‚ดํŒ ํ™•์ธ
โ†’ ์ง์›์—๊ฒŒ ์งˆ๋ฌธํ•˜์ง€ ์•Š์Œ

๊ทธ๋Ÿฐ๋ฐ ์•ˆ๋‚ดํŒ ๊ต์ฒด ์‹œ๊ฐ„์ด ๋˜์–ด ์•ˆ๋‚ดํŒ์ด ๋น„์–ด ์žˆ์„ ๋•Œ ์†๋‹˜ 10๋ช…์ด ๋™์‹œ์— ๋“ค์–ด์˜ค๋ฉด ์–ด๋–ป๊ฒŒ ๋ ๊นŒ?

๊ธฐ์กด ๋ฐฉ์‹์—์„œ๋Š” ์†๋‹˜ 10๋ช…์ด ๋ชจ๋‘ ์ฃผ๋ฐฉ ์ง์›์—๊ฒŒ ๊ฐ™์€ ์งˆ๋ฌธ์„ ํ•œ๋‹ค.

์†๋‹˜ 10๋ช…
โ†’ ์˜ค๋Š˜์˜ ์ธ๊ธฐ ๋ฉ”๋‰ด๊ฐ€ ๋ฌด์—‡์ธ์ง€ ๊ฐ๊ฐ ์งˆ๋ฌธ
โ†’ ์ง์›์ด ๊ฐ™์€ ๋‚ด์šฉ์„ 10๋ฒˆ ํ™•์ธ
โ†’ ๊ฐ™์€ ์•ˆ๋‚ดํŒ์„ 10๋ฒˆ ์ž‘์„ฑ

์šฐ๋ฆฌ๊ฐ€ ์›ํ•˜๋Š” ๋ฐฉ์‹์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

์ฒซ ๋ฒˆ์งธ ์†๋‹˜
โ†’ ์ฃผ๋ฐฉ ์ง์›์—๊ฒŒ ์ธ๊ธฐ ๋ฉ”๋‰ด ์งˆ๋ฌธ
โ†’ ์ƒˆ ์•ˆ๋‚ดํŒ ์ž‘์„ฑ

๋‚˜๋จธ์ง€ ์†๋‹˜ 9๋ช…
โ†’ ์ฒซ ๋ฒˆ์งธ ์†๋‹˜์ด ๋ฐ›์•„์˜ฌ ๊ฒฐ๊ณผ๋ฅผ ๊ธฐ๋‹ค๋ฆผ
โ†’ ์™„์„ฑ๋œ ๊ฐ™์€ ์•ˆ๋‚ดํŒ์„ ํ•จ๊ป˜ ์‚ฌ์šฉ

์š”์ฒญ์„ ๊ฑฐ๋ถ€ํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ์ด๋ฏธ ์ง„ํ–‰ ์ค‘์ธ ๋™์ผ ์ž‘์—…์˜ ๊ฒฐ๊ณผ๋ฅผ ๊ณต์œ ํ•˜๋Š” ๊ฒƒ์ด๋‹ค.


๐Ÿงช ์ถ”์ธกํ•˜์ง€ ์•Š๊ณ  ๋จผ์ € ๋ฌธ์ œ๋ฅผ ์žฌํ˜„ํ–ˆ๋‹ค

์ฒ˜์Œ๋ถ€ํ„ฐ ๋ฝ์„ ์ ์šฉํ•˜์ง€ ์•Š์•˜๋‹ค.

์‹ค์ œ๋กœ ํ˜„์žฌ ์ฝ”๋“œ์—์„œ Cache Stampede๊ฐ€ ๋ฐœ์ƒํ•˜๋Š”์ง€ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋กœ ๋จผ์ € ํ™•์ธํ–ˆ๋‹ค.

ํ…Œ์ŠคํŠธ ์กฐ๊ฑด์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

์บ์‹œ ์ƒํƒœ: Cold cache
์กฐํšŒ ๊ธฐ๊ฐ„: DAILY
limit: 1
๋™์‹œ ์š”์ฒญ: 10๊ฑด

Cold cache๋Š” ํ•ด๋‹น ์š”์ฒญ์˜ ์‘๋‹ต์ด ์บ์‹œ์— ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ƒํƒœ๋ฅผ ์˜๋ฏธํ•œ๋‹ค.

๋™์‹œ์— ์š”์ฒญ์„ ์‹œ์ž‘์‹œํ‚ค๊ธฐ

๋‹จ์ˆœํžˆ ์Šค๋ ˆ๋“œ 10๊ฐœ๋ฅผ ์‹คํ–‰ํ•˜๋Š” ๊ฒƒ๋งŒ์œผ๋กœ๋Š” ์ •ํ™•ํ•œ ์žฌํ˜„์ด ์–ด๋ ค์› ๋‹ค.

์ฒซ ๋ฒˆ์งธ ์š”์ฒญ์ด ๋„ˆ๋ฌด ๋นจ๋ฆฌ ์บ์‹œ๋ฅผ ์ƒ์„ฑํ•˜๋ฉด ๋‚˜๋จธ์ง€ ์š”์ฒญ์€ Cache Hit๊ฐ€ ๋˜์–ด ๋ฌธ์ œ๋ฅผ ์žฌํ˜„ํ•˜์ง€ ๋ชปํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

๊ทธ๋ž˜์„œ CountDownLatch๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค.

ready
โ†’ ๋ชจ๋“  ์š”์ฒญ ์Šค๋ ˆ๋“œ๊ฐ€ ์‹œ์ž‘ ์ค€๋น„๋ฅผ ๋งˆ์ณค๋Š”์ง€ ํ™•์ธ

start
โ†’ ์ค€๋น„๋œ ์š”์ฒญ 10๊ฑด์„ ๋™์‹œ์— ์ถœ๋ฐœ

rankingLookupEntered
โ†’ ์š”์ฒญ์ด ์‹ค์ œ Redis ๋žญํ‚น ์กฐํšŒ์— ์ง„์ž…ํ–ˆ๋Š”์ง€ ํ™•์ธ

releaseRankingLookup
โ†’ ์ฒซ ์š”์ฒญ์ด ๋„ˆ๋ฌด ๋นจ๋ฆฌ ์บ์‹œ๋ฅผ ๋งŒ๋“ค์ง€ ๋ชปํ•˜๋„๋ก ์›๋ณธ ์กฐํšŒ ๋ฐ˜ํ™˜์„ ์ž ์‹œ ์ฐจ๋‹จ

ํ•ต์‹ฌ ํ๋ฆ„์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

CountDownLatch ready =
    new CountDownLatch(THREAD_COUNT);

CountDownLatch start =
    new CountDownLatch(1);

CountDownLatch rankingLookupEntered =
    new CountDownLatch(THREAD_COUNT);

CountDownLatch releaseRankingLookup =
    new CountDownLatch(1);

๋ชจ๋“  ์š”์ฒญ์ด Cache Miss ์ดํ›„ ์›๋ณธ ์กฐํšŒ์— ์ง„์ž…ํ•  ๋•Œ๊นŒ์ง€ Redis ๋žญํ‚น ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•˜์ง€ ์•Š๋„๋ก ๊ตฌ์„ฑํ–ˆ๋‹ค.

์žฌํ˜„ ๊ฒฐ๊ณผ

๋™์ผํ•œ ์บ์‹œ Key๋กœ ์š”์ฒญ 10๊ฑด์„ ๋™์‹œ์— ์‹คํ–‰ํ•œ ๊ฒฐ๊ณผ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์•˜๋‹ค.

Redis ์ธ๊ธฐ ๋žญํ‚น ์กฐํšŒ: 10ํšŒ
MySQL ACTIVE ์ˆ™์†Œ ์กฐํšŒ: 10ํšŒ
๋™์ผ ์‘๋‹ต ์ƒ์„ฑ: 10ํšŒ

์บ์‹œ๊ฐ€ ์ตœ์ข…์ ์œผ๋กœ ์ €์žฅ๋˜๊ธฐ๋Š” ํ–ˆ์ง€๋งŒ, ์บ์‹œ๊ฐ€ ๋งŒ๋“ค์–ด์ง€๊ธฐ ์ „ ๋™์ผํ•œ ๊ณ„์‚ฐ์„ 10๋ฒˆ ๋ฐ˜๋ณตํ•˜๊ณ  ์žˆ์—ˆ๋‹ค.

์ฆ‰, Cache Stampede๊ฐ€ ์ด๋ก ์ ์œผ๋กœ๋งŒ ๊ฐ€๋Šฅํ•œ ๋ฌธ์ œ๊ฐ€ ์•„๋‹ˆ๋ผ ํ˜„์žฌ RoomPick ์ฝ”๋“œ์—์„œ ์‹ค์ œ๋กœ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ์ž„์„ ํ™•์ธํ–ˆ๋‹ค.


๐Ÿ” ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• ๋น„๊ต

๋ฌธ์ œ๋ฅผ ์žฌํ˜„ํ•œ ๋’ค ๋‹ค์Œ ์„ธ ๊ฐ€์ง€ ๋Œ€์•ˆ์„ ๋น„๊ตํ–ˆ๋‹ค.

  1. @Cacheable(sync = true)
  2. Redis Locking Cache Writer ๋˜๋Š” ๋ถ„์‚ฐ ๋ฝ
  3. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‚ด๋ถ€ Key ๋‹จ์œ„ Single Flight

1๏ธโƒฃ @Cacheable(sync = true)

Spring Cache์—๋Š” ๋™์ผํ•œ ์บ์‹œ Key์˜ ๋™์‹œ ์กฐํšŒ๋ฅผ ๋™๊ธฐํ™”ํ•˜๊ธฐ ์œ„ํ•œ ์˜ต์…˜์ด ์žˆ๋‹ค.

@Cacheable(
    cacheNames = "popularAccommodations",
    key = "...",
    sync = true
)

์ฒ˜์Œ์—๋Š” ๊ฐ€์žฅ ๊ฐ„๋‹จํ•œ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์œผ๋กœ ๋ณด์˜€๋‹ค.

ํ•˜์ง€๋งŒ RoomPick์˜ Redis Cache Manager๋Š” ๊ธฐ๋ณธ Non-locking Redis Cache Writer๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์—ˆ๋‹ค.

์ค‘์š”ํ•œ ๊ฒƒ์€ ๋ฌธ์„œ๋งŒ ๋ณด๊ณ  ๋™์ž‘ํ•œ๋‹ค๊ณ  ๊ฐ€์ •ํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์ด์—ˆ๋‹ค.

์‹ค์ œ๋กœ sync = true๋ฅผ ์ ์šฉํ•œ ๋’ค ๋™์ผํ•œ Stampede ํ…Œ์ŠคํŠธ๋ฅผ ๋‹ค์‹œ ์‹คํ–‰ํ–ˆ๋‹ค.

๊ฒฐ๊ณผ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์•˜๋‹ค.

๋™์‹œ ์š”์ฒญ 10๊ฑด
โ†’ Redis ๋žญํ‚น ์กฐํšŒ 10ํšŒ
โ†’ MySQL ์กฐํšŒ 10ํšŒ

ํ˜„์žฌ Redis ์บ์‹œ ๊ตฌ์„ฑ์—์„œ๋Š” sync = true๋งŒ ์ถ”๊ฐ€ํ•ด๋„ ์›๋ณธ ์กฐํšŒ๊ฐ€ ํ•˜๋‚˜๋กœ ํ•ฉ์ณ์ง€์ง€ ์•Š์•˜๋‹ค.

ํšจ๊ณผ๊ฐ€ ์—†๋Š” ์„ค์ •์ด๋ฏ€๋กœ ๋‹ค์‹œ ์ œ๊ฑฐํ–ˆ๊ณ  ์ปค๋ฐ‹์—๋„ ํฌํ•จํ•˜์ง€ ์•Š์•˜๋‹ค.


2๏ธโƒฃ Redis Locking Cache Writer

Redis Cache Writer๋ฅผ Locking ๋ฐฉ์‹์œผ๋กœ ๋ฐ”๊พธ๋Š” ๋ฐฉ๋ฒ•๋„ ๊ฒ€ํ† ํ–ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ ์šฐ๋ฆฌ๊ฐ€ ์›ํ•˜๋Š” ์ž ๊ธˆ ๋ฒ”์œ„๋Š” ๋™์ผํ•œ ์บ์‹œ Key๋‹ค.

DAILY:2026-08-05:10
โ†’ ์ด ํ‚ค์™€ ๋™์ผํ•œ ์š”์ฒญ๋งŒ ๋Œ€๊ธฐ

์บ์‹œ ์ด๋ฆ„ ์ „์ฒด๋ฅผ ์ž ๊ทธ๋Š” ๋ฐฉ์‹์€ ์„œ๋กœ ๊ด€๊ณ„์—†๋Š” ์š”์ฒญ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฌ๊ฒŒ ํ•  ์ˆ˜ ์žˆ๋‹ค.

DAILY + limit 10 ๊ฐฑ์‹  ์ค‘

WEEKLY + limit 10
โ†’ ๋‹ค๋ฅธ ์š”์ฒญ์ด์ง€๋งŒ ๋Œ€๊ธฐํ•  ๊ฐ€๋Šฅ์„ฑ

DAILY + limit 20
โ†’ ๋‹ค๋ฅธ ์š”์ฒญ์ด์ง€๋งŒ ๋Œ€๊ธฐํ•  ๊ฐ€๋Šฅ์„ฑ

Stampede๋ฅผ ๋ง‰๋Š” ๋Œ€์‹  ์บ์‹œ ์ „์ฒด๊ฐ€ ์ƒˆ๋กœ์šด ๋ณ‘๋ชฉ ์ง€์ ์ด ๋  ์ˆ˜ ์žˆ๋‹ค.


3๏ธโƒฃ Redis ๋ถ„์‚ฐ ๋ฝ

Redis ๋ถ„์‚ฐ ๋ฝ์„ ์‚ฌ์šฉํ•˜๋ฉด ์—ฌ๋Ÿฌ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„œ๋ฒ„ ์‚ฌ์ด์—์„œ๋„ ์ค‘๋ณต ๊ฐฑ์‹ ์„ ์ œ์–ดํ•  ์ˆ˜ ์žˆ๋‹ค.

ํ•˜์ง€๋งŒ ๋ถ„์‚ฐ ๋ฝ์—๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ถ”๊ฐ€ ์„ค๊ณ„๊ฐ€ ํ•„์š”ํ•˜๋‹ค.

๋ฝ ํš๋“ ์‹คํŒจ ์ฒ˜๋ฆฌ
๋ฝ ๋Œ€๊ธฐ ์‹œ๊ฐ„
๋ฝ ๋งŒ๋ฃŒ ์‹œ๊ฐ„
์žฌ์‹œ๋„ ์ •์ฑ…
๋ฝ ์†Œ์œ ๊ถŒ ํ™•์ธ
์„œ๋ฒ„ ์žฅ์•  ์‹œ ๋ฝ ํ•ด์ œ
Redis ์žฅ์•  ์ฒ˜๋ฆฌ

ํ˜„์žฌ RoomPick์€ ๋‹จ์ผ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ธ์Šคํ„ด์Šค๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์šด์˜ํ•˜๊ณ  ์žˆ์—ˆ๋‹ค.

์•„์ง ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๋‹ค์ค‘ ์ธ์Šคํ„ด์Šค ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ถ„์‚ฐ ๋ฝ์˜ ๋ณต์žก์„ฑ์„ ๋ฏธ๋ฆฌ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์€ ๊ณผ๋„ํ•˜๋‹ค๊ณ  ํŒ๋‹จํ–ˆ๋‹ค.


โœ… ์ตœ์ข… ์„ ํƒ: Key ๋‹จ์œ„ Single Flight

ํ˜„์žฌ ๊ตฌ์กฐ์—์„œ ๊ฐ€์žฅ ์ ์€ ๋ณต์žก๋„๋กœ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‚ด๋ถ€ Key ๋‹จ์œ„ Single Flight๋ฅผ ์„ ํƒํ–ˆ๋‹ค.

Single Flight๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฐฉ์‹์ด๋‹ค.

๋™์ผํ•œ ์ž‘์—…์ด ์ด๋ฏธ ์ง„ํ–‰ ์ค‘์ด๋ผ๋ฉด ์ƒˆ๋กœ์šด ์ž‘์—…์„ ์‹คํ–‰ํ•˜์ง€ ์•Š๊ณ , ์ง„ํ–‰ ์ค‘์ธ ํ•œ ๋ฒˆ์˜ ์ž‘์—… ๊ฒฐ๊ณผ๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•œ๋‹ค.

์ƒˆ๋กœ์šด Service๋ฅผ ์ถ”๊ฐ€ํ–ˆ๋‹ค.

PopularAccommodationSingleFlightService

์ „์ฒด ํ˜ธ์ถœ ๊ตฌ์กฐ๋Š” ๋‹ค์Œ์ฒ˜๋Ÿผ ๋ณ€๊ฒฝ๋๋‹ค.

๋ณ€๊ฒฝ ์ „

Controller
โ†’ AccommodationFacade
โ†’ PopularAccommodationQueryService
โ†’ Redis ๋žญํ‚น ์กฐํšŒ
โ†’ MySQL ์ˆ™์†Œ ์กฐํšŒ

๋ณ€๊ฒฝ ํ›„

Controller
โ†’ AccommodationFacade
โ†’ PopularAccommodationSingleFlightService
โ†’ PopularAccommodationQueryService
โ†’ Redis ๋žญํ‚น ์กฐํšŒ
โ†’ MySQL ์ˆ™์†Œ ์กฐํšŒ

๊ฐ ๊ณ„์ธต์˜ ์ฑ…์ž„์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

AccommodationFacade
โ†’ ์ธ๊ธฐ ์ˆ™์†Œ ์กฐํšŒ ํ๋ฆ„ ์กฐ์œจ
โ†’ Redis ์žฅ์•  fallback ์ฒ˜๋ฆฌ

PopularAccommodationSingleFlightService
โ†’ ๋™์ผํ•œ ์บ์‹œ Key์˜ ๋™์‹œ ์š”์ฒญ ์ œ์–ด

PopularAccommodationQueryService
โ†’ ์ธ๊ธฐ ์ˆ™์†Œ ๋ฐ์ดํ„ฐ ์กฐํ•ฉ
โ†’ @Cacheable์„ ํ†ตํ•œ ์‘๋‹ต ์บ์‹ฑ

PopularAccommodationRankingService
โ†’ Redis ์ธ๊ธฐ ๋žญํ‚น ์กฐํšŒ

AccommodationService
โ†’ MySQL ACTIVE ์ˆ™์†Œ ์กฐํšŒ

๋™์‹œ์„ฑ ์ œ์–ด ์ฑ…์ž„์„ ๋ณ„๋„ Service๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ QueryService๋‚˜ Repository๊ฐ€ ๊ณผ๋„ํ•œ ์ฑ…์ž„์„ ๊ฐ€์ง€์ง€ ์•Š๋„๋ก ํ–ˆ๋‹ค.


๐Ÿ—‚๏ธ Single Flight์˜ ํ•ต์‹ฌ ์ž๋ฃŒ๊ตฌ์กฐ

๊ตฌํ˜„์—์„œ๋Š” ๋‹ค์Œ ๋‘ ๊ฐ€์ง€๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค.

ConcurrentHashMap
CompletableFuture

์ง„ํ–‰ ์ค‘์ธ ์ž‘์—…์„ ๋‹ค์Œ ํ˜•ํƒœ๋กœ ๊ด€๋ฆฌํ•œ๋‹ค.

private final ConcurrentMap<
    String,
    CompletableFuture<List<PopularAccommodationResponseDto>>
> inFlightRequests = new ConcurrentHashMap<>();

๊ฐœ๋…์ ์œผ๋กœ๋Š” ๋‹ค์Œ ์ •๋ณด๊ฐ€ ๋“ค์–ด ์žˆ๋‹ค.

Key
โ†’ DAILY:2026-08-05:10

Value
โ†’ ํ˜„์žฌ ์ง„ํ–‰ ์ค‘์ธ ์ธ๊ธฐ ์ˆ™์†Œ ์กฐํšŒ ์ž‘์—…

๐Ÿฅ‡ ์ตœ์ดˆ ์š”์ฒญ ์ฒ˜๋ฆฌ

๋™์ผ ํ‚ค๋กœ ๋“ค์–ด์˜จ ์š”์ฒญ ์ค‘ ์ตœ์ดˆ ์š”์ฒญ๋งŒ ์‹ค์ œ ์กฐํšŒ๋ฅผ ์ˆ˜ํ–‰ํ•œ๋‹ค.

CompletableFuture<List<PopularAccommodationResponseDto>>
    newRequest = new CompletableFuture<>();

CompletableFuture<List<PopularAccommodationResponseDto>>
    existingRequest = inFlightRequests.putIfAbsent(
        cacheKey,
        newRequest
    );

putIfAbsent()๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋™์ž‘ํ•œ๋‹ค.

Map์— ํ•ด๋‹น Key๊ฐ€ ์—†๋‹ค
โ†’ ๋‚ด๊ฐ€ ๋งŒ๋“  Future ๋“ฑ๋ก
โ†’ ๋‚ด๊ฐ€ ์ตœ์ดˆ ์š”์ฒญ

Map์— ํ•ด๋‹น Key๊ฐ€ ์ด๋ฏธ ์žˆ๋‹ค
โ†’ ๊ธฐ์กด Future ๋ฐ˜ํ™˜
โ†’ ๋‹ค๋ฅธ ์š”์ฒญ์ด ์ด๋ฏธ ์ž‘์—… ์ค‘

์—ฌ๋Ÿฌ ์š”์ฒญ์ด ๋™์‹œ์— ์ ‘๊ทผํ•ด๋„ ConcurrentHashMap๊ณผ putIfAbsent()๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— ํ•œ ์š”์ฒญ๋งŒ ์ตœ์ดˆ ์š”์ฒญ์ด ๋œ๋‹ค.

์š”์ฒญ A โ†’ ๋“ฑ๋ก ์„ฑ๊ณต โ†’ ์‹ค์ œ ์กฐํšŒ ์‹คํ–‰
์š”์ฒญ B โ†’ ๊ธฐ์กด Future ๋ฐœ๊ฒฌ โ†’ A์˜ ๊ฒฐ๊ณผ ๋Œ€๊ธฐ
์š”์ฒญ C โ†’ ๊ธฐ์กด Future ๋ฐœ๊ฒฌ โ†’ A์˜ ๊ฒฐ๊ณผ ๋Œ€๊ธฐ

โณ ๋‚˜๋จธ์ง€ ์š”์ฒญ ์ฒ˜๋ฆฌ

์ด๋ฏธ ๊ฐ™์€ Key์˜ ์š”์ฒญ์ด ์ง„ํ–‰ ์ค‘์ด๋ผ๋ฉด ์ƒˆ๋กœ์šด ์›๋ณธ ์กฐํšŒ๋ฅผ ์‹œ์ž‘ํ•˜์ง€ ์•Š๋Š”๋‹ค.

if (existingRequest != null) {
    return awaitResult(
        existingRequest
    );
}

์ตœ์ดˆ ์š”์ฒญ์˜ CompletableFuture๊ฐ€ ์™„๋ฃŒ๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฐ ๋’ค ๊ฐ™์€ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

์ตœ์ดˆ ์š”์ฒญ
โ†’ Redis ๋žญํ‚น ์กฐํšŒ
โ†’ MySQL ์กฐํšŒ
โ†’ ์‘๋‹ต ์ƒ์„ฑ
โ†’ Future ์™„๋ฃŒ

๋Œ€๊ธฐ ์š”์ฒญ
โ†’ ๊ธฐ์กด Future ๋Œ€๊ธฐ
โ†’ ์ตœ์ดˆ ์š”์ฒญ์ด ๋งŒ๋“  ์‘๋‹ต ๋ฐ˜ํ™˜

๐ŸŽ‰ ์„ฑ๊ณต ๊ฒฐ๊ณผ ๊ณต์œ 

์ตœ์ดˆ ์š”์ฒญ์ด ์ •์ƒ์ ์œผ๋กœ ์ฒ˜๋ฆฌ๋˜๋ฉด ๊ฒฐ๊ณผ๋กœ Future๋ฅผ ์™„๋ฃŒํ•œ๋‹ค.

currentRequest.complete(
    result
);

๋Œ€๊ธฐ ์ค‘์ธ ๋ชจ๋“  ์š”์ฒญ์€ ๊ฐ™์€ ๊ฒฐ๊ณผ๋ฅผ ์ „๋‹ฌ๋ฐ›๋Š”๋‹ค.

์š”์ฒญ 10๊ฑด
โ†’ ์‹ค์ œ ์›๋ณธ ์กฐํšŒ 1ํšŒ
โ†’ ๊ฒฐ๊ณผ ๊ฐ์ฒด๋ฅผ 10๊ฐœ ์š”์ฒญ์ด ๊ณต์œ 

๐Ÿšจ ์‹คํŒจ ๊ฒฐ๊ณผ๋„ ๊ณต์œ ํ•ด์•ผ ํ•œ๋‹ค

์„ฑ๊ณต ๊ฒฐ๊ณผ๋งŒ ๊ณต์œ ํ•ด์„œ๋Š” ์•ˆ ๋œ๋‹ค.

Redis ์กฐํšŒ์— ์‹คํŒจํ–ˆ์„ ๋•Œ ์ตœ์ดˆ ์š”์ฒญ๋งŒ ์˜ˆ์™ธ๋ฅผ ๋ฐ›๊ณ , ๋Œ€๊ธฐ ์š”์ฒญ๋“ค์ด ๋ฉˆ์ถ”๊ฑฐ๋‚˜ ๋‹ค๋ฅธ ๋™์ž‘์„ ํ•˜๋ฉด ์•ˆ ๋œ๋‹ค.

๋”ฐ๋ผ์„œ ์ตœ์ดˆ ์š”์ฒญ์—์„œ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด Future๋„ ์˜ˆ์™ธ ์ƒํƒœ๋กœ ์™„๋ฃŒํ•œ๋‹ค.

currentRequest.completeExceptionally(
    exception
);

๋Œ€๊ธฐ ์š”์ฒญ์€ CompletionException ๋‚ด๋ถ€์˜ ์›๋ณธ ์˜ˆ์™ธ๋ฅผ ๊บผ๋‚ด ๋‹ค์‹œ ์ „๋‹ฌํ•œ๋‹ค.

if (cause instanceof RuntimeException runtimeException) {
    throw runtimeException;
}

if (cause instanceof Error error) {
    throw error;
}

์ด๋ฅผ ํ†ตํ•ด ์„ฑ๊ณต๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์‹คํŒจ๋„ ๊ฐ™์€ ์ž‘์—…์˜ ๊ฒฐ๊ณผ๋กœ ๊ณต์œ ํ•œ๋‹ค.

์ตœ์ดˆ ์š”์ฒญ ์„ฑ๊ณต
โ†’ ๋ชจ๋“  ๋Œ€๊ธฐ ์š”์ฒญ์ด ๊ฐ™์€ ์„ฑ๊ณต ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜

์ตœ์ดˆ ์š”์ฒญ Redis ์žฅ์• 
โ†’ ๋ชจ๋“  ๋Œ€๊ธฐ ์š”์ฒญ์ด ๊ฐ™์€ Redis ์žฅ์•  ์˜ˆ์™ธ ์ „๋‹ฌ

๐Ÿงน ์™„๋ฃŒ๋œ ์ž‘์—…์€ ๋ฐ˜๋“œ์‹œ Map์—์„œ ์ œ๊ฑฐํ•œ๋‹ค

Single Flight Map์€ ํ˜„์žฌ ์ง„ํ–‰ ์ค‘์ธ ์š”์ฒญ๋งŒ ๊ด€๋ฆฌํ•ด์•ผ ํ•œ๋‹ค.

์™„๋ฃŒ๋œ Future๊ฐ€ ๊ณ„์† ๋‚จ์•„ ์žˆ์œผ๋ฉด ์ดํ›„ ์š”์ฒญ์ด ์˜ค๋ž˜๋œ ๊ฒฐ๊ณผ๋ฅผ ๊ณ„์† ์žฌ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ์ฆ๊ฐ€ํ•  ์ˆ˜ ์žˆ๋‹ค.

๋”ฐ๋ผ์„œ ์„ฑ๊ณต๊ณผ ์‹คํŒจ ์—ฌ๋ถ€์— ์ƒ๊ด€์—†์ด finally์—์„œ ์ œ๊ฑฐํ•œ๋‹ค.

finally {
    inFlightRequests.remove(
        cacheKey,
        currentRequest
    );
}

๋‹จ์ˆœํžˆ ๋‹ค์Œ์ฒ˜๋Ÿผ ์ œ๊ฑฐํ•˜์ง€ ์•Š์•˜๋‹ค.

inFlightRequests.remove(cacheKey);

๋Œ€์‹  ๋‹ค์Œ ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค.

inFlightRequests.remove(
    cacheKey,
    currentRequest
);

์ด๋Š” Map์— ์ €์žฅ๋œ ๊ฐ’์ด ํ˜„์žฌ ์š”์ฒญ์˜ Future์™€ ๊ฐ™์„ ๋•Œ๋งŒ ์ œ๊ฑฐํ•œ๋‹ค๋Š” ์˜๋ฏธ๋‹ค.

ํ˜น์‹œ ์ƒˆ๋กœ์šด ์ž‘์—…์ด ๊ฐ™์€ Key์— ๋“ฑ๋ก๋œ ์ƒํƒœ๋ผ๋ฉด ๊ทธ ์ž‘์—…์„ ์ž˜๋ชป ์ œ๊ฑฐํ•˜์ง€ ์•Š๋„๋ก ๋ฐฉ์–ดํ•  ์ˆ˜ ์žˆ๋‹ค.


๐Ÿ”‘ ์„œ๋กœ ๋‹ค๋ฅธ ์š”์ฒญ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š๋„๋ก Key๋ฅผ ๋ถ„๋ฆฌํ–ˆ๋‹ค

Single Flight ์‹๋ณ„ Key๋Š” ๊ธฐ์กด ์‘๋‹ต ์บ์‹œ Key์™€ ๋™์ผํ•œ ์กฐ๊ฑด์œผ๋กœ ์ƒ์„ฑํ–ˆ๋‹ค.

return popularAccommodationKeyGenerator
    .generateCurrentKey(period)
    + ":"
    + limit;

๋”ฐ๋ผ์„œ ์•„๋ž˜ ์š”์ฒญ๋“ค์€ ๊ฐ™์€ ์ž‘์—…์„ ๊ณต์œ ํ•œ๋‹ค.

DAILY + 2026-08-05 + limit 10
DAILY + 2026-08-05 + limit 10

๋ฐ˜๋ฉด ๋‹ค์Œ ์š”์ฒญ๋“ค์€ ์„œ๋กœ ๋…๋ฆฝ์ ์œผ๋กœ ์‹คํ–‰๋œ๋‹ค.

DAILY + limit 10
DAILY + limit 20
WEEKLY + limit 10

์ฆ‰, ์บ์‹œ ์ „์ฒด์— ๋ฝ์„ ๊ฑฐ๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ๋™์ผํ•œ Key์˜ ์š”์ฒญ๋งŒ ๋ฌถ๋Š”๋‹ค.


๐Ÿ›Ÿ ๊ธฐ์กด Redis ์žฅ์•  fallback ์œ ์ง€

RoomPick์—๋Š” Redis ์ธ๊ธฐ ๋žญํ‚น์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์„ ๋•Œ API ์ „์ฒด ์žฅ์• ๋ฅผ ๋ง‰๊ธฐ ์œ„ํ•œ fallback์ด ์กด์žฌํ•œ๋‹ค.

Redis ์ธ๊ธฐ ๋žญํ‚น ์กฐํšŒ ์‹คํŒจ
โ†’ PopularAccommodationRankingUnavailableException ๋ฐœ์ƒ
โ†’ AccommodationFacade์—์„œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ
โ†’ ์ตœ์‹  ACTIVE ์ˆ™์†Œ๋ฅผ MySQL์—์„œ ์กฐํšŒ
โ†’ ์ž„์‹œ rank ๋ถ€์—ฌ
โ†’ fallback ์‘๋‹ต ๋ฐ˜ํ™˜

Single Flight๊ฐ€ ์˜ˆ์™ธ๋ฅผ ๋‚ด๋ถ€์—์„œ ์‚ผ์ผœ๋ฒ„๋ฆฌ๋ฉด ๊ธฐ์กด fallback ํ๋ฆ„์ด ๊นจ์ง„๋‹ค.

๊ทธ๋ž˜์„œ Single Flight๋Š” ์˜ˆ์™ธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ๋Œ€๊ธฐ ์š”์ฒญ์—๋„ ๋™์ผํ•œ ์˜ˆ์™ธ๋ฅผ ์ „๋‹ฌํ•˜๋Š” ์—ญํ• ๋งŒ ํ•œ๋‹ค.

Single Flight
โ†’ ๋™์ผ ์š”์ฒญ์˜ ์„ฑ๊ณต ๋˜๋Š” ์‹คํŒจ ๊ฒฐ๊ณผ ๊ณต์œ 

Facade
โ†’ Redis ์žฅ์• ์ผ ๋•Œ DB fallback ์‹คํ–‰

์ฑ…์ž„์„ ๋ถ„๋ฆฌํ•˜์—ฌ ๊ธฐ์กด ์žฅ์•  ๋Œ€์‘ ํ๋ฆ„์„ ์œ ์ง€ํ–ˆ๋‹ค.

Redis ์žฅ์• 

Redis ๋žญํ‚น ์กฐํšŒ ์‹คํŒจ
โ†’ ์˜ˆ์™ธ ๊ณต์œ 
โ†’ Facade์—์„œ ์ตœ์‹  ACTIVE ์ˆ™์†Œ fallback

MySQL ์žฅ์• 

MySQL ์กฐํšŒ ์‹คํŒจ
โ†’ Redis ์žฅ์• ๊ฐ€ ์•„๋‹ˆ๋ฏ€๋กœ fallbackํ•˜์ง€ ์•Š์Œ
โ†’ DB ์˜ˆ์™ธ ๊ทธ๋Œ€๋กœ ์ „๋‹ฌ

๋น„์ฆˆ๋‹ˆ์Šค ์˜ˆ์™ธ

์ž˜๋ชป๋œ limit ๋“ฑ ๋น„์ฆˆ๋‹ˆ์Šค ์˜ˆ์™ธ
โ†’ fallback์œผ๋กœ ์ˆจ๊ธฐ์ง€ ์•Š์Œ
โ†’ ์˜ˆ์™ธ ๊ทธ๋Œ€๋กœ ์ „๋‹ฌ

๐Ÿ“Š ์บ์‹œ ๋น„ํ™œ์„ฑํ™” ์‹œ Single Flight๋„ ์šฐํšŒ

RoomPick์—์„œ๋Š” ์บ์‹œ ์ ์šฉ ์ „ํ›„์˜ ์„ฑ๋Šฅ์„ ๋น„๊ตํ•˜๊ธฐ ์œ„ํ•ด ์บ์‹œ๋ฅผ ๋น„ํ™œ์„ฑํ™”ํ•  ์ˆ˜ ์žˆ๋‹ค.

roompick.cache.popular-accommodations-enabled=false

์บ์‹œ๋งŒ ๋น„ํ™œ์„ฑํ™”ํ•˜๊ณ  Single Flight๋Š” ์œ ์ง€ํ•˜๋ฉด ๋™์‹œ ์š”์ฒญ์ด ํ•˜๋‚˜๋กœ ํ•ฉ์ณ์ง„๋‹ค.

๊ทธ๋Ÿฌ๋ฉด ์บ์‹œ ๋ฏธ์ ์šฉ ์ƒํƒœ์˜ ์›๋ž˜ ๋น„์šฉ์„ ์ •ํ™•ํ•˜๊ฒŒ ์ธก์ •ํ•  ์ˆ˜ ์—†๋‹ค.

๋”ฐ๋ผ์„œ ์บ์‹œ๊ฐ€ ๋น„ํ™œ์„ฑํ™”๋œ ๊ฒฝ์šฐ Single Flight๋„ ์šฐํšŒํ–ˆ๋‹ค.

if (!popularAccommodationCacheCondition.isEnabled()) {
    return popularAccommodationQueryService
        .getPopularAccommodations(
            period,
            limit
        );
}

์ •์ฑ…์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

์บ์‹œ ํ™œ์„ฑํ™”
โ†’ Single Flight ์ ์šฉ
โ†’ ๋™์ผ Key์˜ ์›๋ณธ ์กฐํšŒ๋ฅผ ํ•˜๋‚˜๋กœ ๋ฌถ์Œ

์บ์‹œ ๋น„ํ™œ์„ฑํ™”
โ†’ Single Flight ์šฐํšŒ
โ†’ ๋ชจ๋“  ์š”์ฒญ์ด ๊ธฐ์กด ์กฐํšŒ ํ๋ฆ„ ์‹คํ–‰

์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ์˜ ๊ธฐ์ค€ ์กฐ๊ฑด์„ ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•œ ๊ฒฐ์ •์ด๋‹ค.


๐Ÿงช ๊ฐœ์„  ํ›„ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ

๊ธฐ์กด ์žฌํ˜„ ํ…Œ์ŠคํŠธ๋ฅผ Single Flight ๊ฐœ์„  ๊ฒ€์ฆ ํ…Œ์ŠคํŠธ๋กœ ๋ณ€๊ฒฝํ–ˆ๋‹ค.

๋™์ผํ•œ ์กฐ๊ฑด์œผ๋กœ ๋‹ค์‹œ ์‹คํ–‰ํ–ˆ๋‹ค.

Cold cache
๋™์ผ ์บ์‹œ Key
๋™์‹œ ์š”์ฒญ 10๊ฑด

์ตœ์ดˆ ์š”์ฒญ์ด ์›๋ณธ ์กฐํšŒ์— ์ง„์ž…ํ•˜๋ฉด Redis ๋žญํ‚น ์‘๋‹ต์„ ์ž ์‹œ ๋ง‰์•„ ๋‚˜๋จธ์ง€ ์š”์ฒญ๋“ค์ด ๋™์ผํ•œ ์ง„ํ–‰ ์ค‘ ์ž‘์—…์„ ๋ฐœ๊ฒฌํ•  ์ˆ˜ ์žˆ๋„๋ก ํ–ˆ๋‹ค.

CountDownLatch rankingLookupEntered =
    new CountDownLatch(1);

CountDownLatch releaseRankingLookup =
    new CountDownLatch(1);

๋ชจ๋“  ์š”์ฒญ์€ ๋‹ค์Œ Service๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค.

popularAccommodationSingleFlightService
    .getPopularAccommodations(
        DAILY,
        LIMIT
    );

์ตœ์ข… ๊ฒ€์ฆ ์กฐ๊ฑด์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

then(popularAccommodationRankingService)
    .should(times(1))
    .findRankedAccommodationIds(
        DAILY,
        LIMIT,
        0L,
        4L
    );

then(accommodationService)
    .should(times(1))
    .findAllActiveSummaryByIds(
        rankedAccommodationIds
    );

๐Ÿ“ˆ ๊ฐœ์„  ์ „ํ›„ ๊ฒฐ๊ณผ

ํ•ญ๋ชฉ๊ฐœ์„  ์ „๊ฐœ์„  ํ›„
๋™์‹œ ์š”์ฒญ ์ˆ˜10๊ฑด10๊ฑด
Redis ๋žญํ‚น ์กฐํšŒ10ํšŒ1ํšŒ
MySQL ์ˆ™์†Œ ์กฐํšŒ10ํšŒ1ํšŒ
์‘๋‹ต DTO ์ƒ์„ฑ10ํšŒ1ํšŒ
์‚ฌ์šฉ์ž ์š”์ฒญ ๊ฒฐ๊ณผ๋ชจ๋‘ ์„ฑ๊ณต๋ชจ๋‘ ์„ฑ๊ณต
์ตœ์ข… Redis ์บ์‹œ ์ €์žฅ์„ฑ๊ณต์„ฑ๊ณต

์‚ฌ์šฉ์ž๊ฐ€ ๋ฐ›๋Š” ์‘๋‹ต์€ ๋™์ผํ•˜์ง€๋งŒ ๋‚ด๋ถ€์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์ค‘๋ณต ๋น„์šฉ์€ ํฌ๊ฒŒ ๊ฐ์†Œํ–ˆ๋‹ค.

๊ฐœ์„  ์ „

๋™์ผ ์š”์ฒญ 10๊ฑด
โ†’ Redis ์กฐํšŒ 10ํšŒ
โ†’ MySQL ์กฐํšŒ 10ํšŒ
โ†’ ๊ฐ™์€ ์‘๋‹ต 10๋ฒˆ ์ƒ์„ฑ
๊ฐœ์„  ํ›„

๋™์ผ ์š”์ฒญ 10๊ฑด
โ†’ Redis ์กฐํšŒ 1ํšŒ
โ†’ MySQL ์กฐํšŒ 1ํšŒ
โ†’ ์ตœ์ดˆ ์š”์ฒญ ๊ฒฐ๊ณผ๋ฅผ 10๊ฑด์ด ๊ณต์œ 

ํŠนํžˆ MySQL ์กฐํšŒ๊ฐ€ 10ํšŒ์—์„œ 1ํšŒ๋กœ ์ค„์–ด DB Connection๊ณผ ์ฟผ๋ฆฌ ์‹คํ–‰ ๋น„์šฉ์„ ์ค„์ผ ์ˆ˜ ์žˆ์—ˆ๋‹ค.


โœ… ํšŒ๊ท€ ํ…Œ์ŠคํŠธ

Single Flight ์ ์šฉ ํ›„ ๊ธฐ์กด ๊ธฐ๋Šฅ์ด ๊นจ์ง€์ง€ ์•Š์•˜๋Š”์ง€ ๋‹ค์Œ ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ–ˆ๋‹ค.

์ธ๊ธฐ ์ˆ™์†Œ ๊ด€๋ จ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ

./gradlew integrationTest \
  --tests "*PopularAccommodation*"

Stampede ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ

./gradlew integrationTest \
  --tests "*PopularAccommodationCacheStampedeIntegrationTest"

์ „์ฒด ๋‹จ์œ„ ํ…Œ์ŠคํŠธ

./gradlew clean test

Facade๊ฐ€ ๊ธฐ์กด PopularAccommodationQueryService ๋Œ€์‹  PopularAccommodationSingleFlightService๋ฅผ ํ˜ธ์ถœํ•˜๋„๋ก ๋ณ€๊ฒฝ๋๊ธฐ ๋•Œ๋ฌธ์— ๊ธฐ์กด Facade ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์˜ Mock ๋Œ€์ƒ๋„ ํ•จ๊ป˜ ์ˆ˜์ •ํ–ˆ๋‹ค.

๋ชจ๋“  ํ…Œ์ŠคํŠธ๊ฐ€ ์ตœ์ข…์ ์œผ๋กœ ์„ฑ๊ณตํ–ˆ๋‹ค.


๐Ÿ—๏ธ ์ตœ์ข… ๊ตฌ์กฐ

Controller
    โ†“
AccommodationFacade
    โ†“
PopularAccommodationSingleFlightService
    โ†“
PopularAccommodationQueryService
    โ†“
PopularAccommodationRankingService
    โ†“
Redis Repository

๊ทธ๋ฆฌ๊ณ  ์ˆ™์†Œ ๊ณต๊ฐœ ์ •๋ณด๋Š” ๋‹ค์Œ ํ๋ฆ„์œผ๋กœ ์กฐํšŒ๋œ๋‹ค.

PopularAccommodationQueryService
    โ†“
AccommodationService
    โ†“
AccommodationRepository
    โ†“
MySQL

๊ฐ ๊ณ„์ธต์€ ๋‹ค์Œ ์ฑ…์ž„์„ ๊ฐ€์ง„๋‹ค.

๊ณ„์ธต์ฑ…์ž„
Controller์š”์ฒญ ์ˆ˜์‹  ๋ฐ ์‘๋‹ต ๋ฐ˜ํ™˜
Facade์ „์ฒด ๋น„์ฆˆ๋‹ˆ์Šค ํ๋ฆ„ ์กฐ์œจ, Redis ์žฅ์•  fallback
SingleFlightService๋™์ผ ์บ์‹œ Key์˜ ๋™์‹œ ์š”์ฒญ ์ œ์–ด
QueryService๋žญํ‚น๊ณผ ์ˆ™์†Œ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํ•ฉํ•˜๊ณ  ์‘๋‹ต ์บ์‹ฑ
RankingServiceRedis ๋žญํ‚น ์กฐํšŒ
AccommodationServiceACTIVE ์ˆ™์†Œ DB ์กฐํšŒ
RepositoryRedis ๋ฐ MySQL ๋ช…๋ น ์‹คํ–‰

โš ๏ธ ํ˜„์žฌ ๊ตฌํ˜„์˜ ํ•œ๊ณ„

์ด๋ฒˆ Single Flight๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ธ์Šคํ„ด์Šค ๋‚ด๋ถ€์—์„œ๋งŒ ๋™์ž‘ํ•œ๋‹ค.

์„œ๋ฒ„๊ฐ€ ํ•œ ๋Œ€๋ผ๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

์„œ๋ฒ„ A์— ๋™์ผ ์š”์ฒญ 10๊ฑด
โ†’ ์„œ๋ฒ„ A ๋‚ด๋ถ€์—์„œ ์›๋ณธ ์กฐํšŒ 1ํšŒ

์„œ๋ฒ„๊ฐ€ ๋‘ ๋Œ€๋ผ๋ฉด ๊ฐ ์„œ๋ฒ„๊ฐ€ ์ž์‹ ์˜ Map์„ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค.

์„œ๋ฒ„ A์— ์š”์ฒญ 5๊ฑด
โ†’ ์„œ๋ฒ„ A์—์„œ ์›๋ณธ ์กฐํšŒ 1ํšŒ

์„œ๋ฒ„ B์— ์š”์ฒญ 5๊ฑด
โ†’ ์„œ๋ฒ„ B์—์„œ ์›๋ณธ ์กฐํšŒ 1ํšŒ

์ „์ฒด์ ์œผ๋กœ๋Š” ์›๋ณธ ์กฐํšŒ๊ฐ€ 2ํšŒ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค.

ํ•˜์ง€๋งŒ ํ˜„์žฌ RoomPick์€ ๋‹จ์ผ ์ธ์Šคํ„ด์Šค ๊ตฌ์กฐ์ด๋ฏ€๋กœ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‚ด๋ถ€ Single Flight๊ฐ€ ํ˜„์žฌ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•œ ๊ฐ€์žฅ ๋‹จ์ˆœํ•˜๊ณ  ์ ์ ˆํ•œ ๋ฐฉ์‹์ด๋ผ๊ณ  ํŒ๋‹จํ–ˆ๋‹ค.

ํ–ฅํ›„ ๋‹ค์ค‘ ์ธ์Šคํ„ด์Šค ํ™˜๊ฒฝ์—์„œ ์ธ์Šคํ„ด์Šค ๊ฐ„ ์ค‘๋ณต ๊ฐฑ์‹ ์ด ์‹ค์ œ ๋ฌธ์ œ๋กœ ํ™•์ธ๋œ๋‹ค๋ฉด ๋‹ค์Œ ๋ฐฉ๋ฒ•์„ ๊ฒ€ํ† ํ•  ์ˆ˜ ์žˆ๋‹ค.

Redis ๋ถ„์‚ฐ ๋ฝ
๋ถ„์‚ฐ Single Flight
Soft TTL
์บ์‹œ ์„ ์ œ ๊ฐฑ์‹ 
๋ฐฑ๊ทธ๋ผ์šด๋“œ Refresh

์‹ค์ œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜๊ธฐ ์ „์— ๋ถ„์‚ฐ ๋ฝ์˜ ๋ณต์žก์„ฑ์„ ๋ฏธ๋ฆฌ ์ถ”๊ฐ€ํ•˜์ง€ ์•Š๋Š” ๊ฒƒ๋„ ์ค‘์š”ํ•œ ์„ค๊ณ„ ๊ฒฐ์ •์ด๋ผ๊ณ  ์ƒ๊ฐํ•œ๋‹ค.


๐Ÿ’ก ์ด๋ฒˆ ์ž‘์—…์—์„œ ๋ฐฐ์šด ์ 

1. ์บ์‹œ ์ ์šฉ์ด ์„ฑ๋Šฅ ๋ฌธ์ œ์˜ ๋์€ ์•„๋‹ˆ๋‹ค

์บ์‹œ๋Š” ํ‰์ƒ์‹œ ์›๋ณธ ์กฐํšŒ๋ฅผ ์ค„์—ฌ์ฃผ์ง€๋งŒ ์บ์‹œ ๋งŒ๋ฃŒ ์‹œ์ ์—๋Š” ์š”์ฒญ์ด ์›๋ณธ ๋ฐ์ดํ„ฐ๋กœ ํ•œ๊บผ๋ฒˆ์— ๋ชฐ๋ฆด ์ˆ˜ ์žˆ๋‹ค.

Cache Hit ์„ฑ๋Šฅ๋ฟ ์•„๋‹ˆ๋ผ ๋‹ค์Œ ์ƒํ™ฉ๋„ ํ•จ๊ป˜ ๊ฒ€์ฆํ•ด์•ผ ํ•œ๋‹ค.

Cold cache
TTL ๋งŒ๋ฃŒ ์งํ›„
๋™์‹œ ์š”์ฒญ
Redis ์žฅ์• 
์บ์‹œ ๋น„ํ™œ์„ฑํ™”

2. ๋™์‹œ์„ฑ ๋ฌธ์ œ๋Š” ํ…Œ์ŠคํŠธ์—์„œ ์‹ค์ œ๋กœ ์žฌํ˜„ํ•ด์•ผ ํ•œ๋‹ค

โ€œ์•„๋งˆ ์ค‘๋ณต ์กฐํšŒ๊ฐ€ ๋ฐœ์ƒํ•  ๊ฒƒ์ด๋‹คโ€๋ผ๋Š” ์ถ”์ธก๋งŒ์œผ๋กœ ๋ณต์žกํ•œ ๋ฝ์„ ์ถ”๊ฐ€ํ•˜์ง€ ์•Š์•˜๋‹ค.

CountDownLatch์™€ ๋‹ค์ค‘ ์Šค๋ ˆ๋“œ ํ…Œ์ŠคํŠธ๋ฅผ ํ†ตํ•ด ์‹ค์ œ๋กœ Redis์™€ MySQL ์กฐํšŒ๊ฐ€ ๊ฐ๊ฐ 10ํšŒ ๋ฐœ์ƒํ•œ๋‹ค๋Š” ๊ฒƒ์„ ํ™•์ธํ•œ ๋’ค ๊ฐœ์„ ํ–ˆ๋‹ค.

3. ๊ฐ€์žฅ ๋ณต์žกํ•œ ๊ธฐ์ˆ ์ด ํ•ญ์ƒ ๊ฐ€์žฅ ์ข‹์€ ํ•ด๊ฒฐ์ฑ…์€ ์•„๋‹ˆ๋‹ค

Redis ๋ถ„์‚ฐ ๋ฝ์€ ๊ฐ•๋ ฅํ•˜์ง€๋งŒ ํ˜„์žฌ ๋‹จ์ผ ์ธ์Šคํ„ด์Šค ๊ตฌ์กฐ์—์„œ๋Š” ๊ณผ๋„ํ•œ ๋ณต์žก์„ฑ์ด ๋  ์ˆ˜ ์žˆ๋‹ค.

ํ˜„์žฌ ๋ฌธ์ œ์˜ ๋ฒ”์œ„์— ๋งž์ถฐ Key ๋‹จ์œ„ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ Single Flight๋ฅผ ์„ ํƒํ–ˆ๋‹ค.

4. ์„ฑ๊ณต๋ฟ ์•„๋‹ˆ๋ผ ์‹คํŒจ๋„ ๊ณต์œ ํ•ด์•ผ ํ•œ๋‹ค

๋™์ผ ์ž‘์—…์„ ํ•˜๋‚˜๋กœ ํ•ฉ์น  ๋•Œ ์„ฑ๊ณต ๊ฒฐ๊ณผ๋งŒ ๊ณต์œ ํ•˜๋ฉด ์•ˆ ๋œ๋‹ค.

์ตœ์ดˆ ์š”์ฒญ์—์„œ ๋ฐœ์ƒํ•œ ์˜ˆ์™ธ๋„ ๋Œ€๊ธฐ ์š”์ฒญ์— ์ „๋‹ฌํ•ด์•ผ ๊ธฐ์กด ์˜ˆ์™ธ ์ฒ˜๋ฆฌ์™€ fallback ํ๋ฆ„์„ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ๋‹ค.

5. ์„ฑ๋Šฅ ๊ฐœ์„ ์€ ๊ธฐ์กด ์žฅ์•  ์ •์ฑ…์„ ๊นจ๋œจ๋ฆฌ์ง€ ์•Š์•„์•ผ ํ•œ๋‹ค

Redis ์ค‘๋ณต ์กฐํšŒ๋ฅผ ์ค„์ด๋Š” ๊ฒƒ๋งŒํผ ๊ธฐ์กด DB fallback๊ณผ ๋น„์ฆˆ๋‹ˆ์Šค ์˜ˆ์™ธ ์ •์ฑ…์„ ์œ ์ง€ํ•˜๋Š” ๊ฒƒ๋„ ์ค‘์š”ํ–ˆ๋‹ค.

์ตœ์ ํ™” ๋•Œ๋ฌธ์— ์˜ˆ์™ธ ์˜๋ฏธ๊ฐ€ ๋‹ฌ๋ผ์ง€๊ฑฐ๋‚˜ ์ž˜๋ชป๋œ fallback์ด ์‹คํ–‰๋˜์ง€ ์•Š๋„๋ก ์ฑ…์ž„์„ ๋ถ„๋ฆฌํ–ˆ๋‹ค.

6. ์„ค์ •๊ฐ’์˜ ์ด์œ ๋ฅผ ๋ฌธ์„œ๋กœ ๋‚จ๊ฒจ์•ผ ํ•œ๋‹ค

TTL 60์ดˆ๋ผ๋Š” ์ˆซ์ž๋งŒ ๋‚จ์•„ ์žˆ์œผ๋ฉด ์ดํ›„ ํŒ€์›์ด ๋ณ€๊ฒฝ ์—ฌ๋ถ€๋ฅผ ํŒ๋‹จํ•˜๊ธฐ ์–ด๋ ต๋‹ค.

๋‹ค์Œ์„ ๋ฌธ์„œํ™”ํ–ˆ๋‹ค.

์™œ 60์ดˆ์ธ๊ฐ€
์™œ ๋” ์งง์ง€ ์•Š์€๊ฐ€
์™œ ๋” ๊ธธ์ง€ ์•Š์€๊ฐ€
์–ด๋–ค ์ตœ์‹ ์„ฑ ์ง€์—ฐ์„ ํ—ˆ์šฉํ•˜๋Š”๊ฐ€
์–ด๋–ค ๋ณ€๊ฒฝ์€ ์ฆ‰์‹œ ์บ์‹œ๋ฅผ ๋ฌดํšจํ™”ํ•ด์•ผ ํ•˜๋Š”๊ฐ€
ํ˜„์žฌ ๊ฒฐ์ •์˜ ํ•œ๊ณ„๋Š” ๋ฌด์—‡์ธ๊ฐ€
ํ–ฅํ›„ ์–ด๋–ค ๋น„๊ต ์ธก์ •์ด ํ•„์š”ํ•œ๊ฐ€

์ˆซ์ž๋ณด๋‹ค ์ค‘์š”ํ•œ ๊ฒƒ์€ ์ˆซ์ž๋ฅผ ์„ ํƒํ•œ ๊ทผ๊ฑฐ๋ผ๊ณ  ๋А๊ผˆ๋‹ค.


๐ŸŽฏ ๋งˆ๋ฌด๋ฆฌ

์ด๋ฒˆ ์ž‘์—…์—์„œ๋Š” ์ธ๊ธฐ ์ˆ™์†Œ ์‘๋‹ต ์บ์‹œ๊ฐ€ ๋งŒ๋ฃŒ๋œ ์งํ›„ ๋™์ผ ์š”์ฒญ์ด ๋™์‹œ์— ์œ ์ž…๋˜๋ฉด Redis ๋žญํ‚น๊ณผ MySQL ์ˆ™์†Œ ์ •๋ณด๋ฅผ ์ค‘๋ณต ์กฐํšŒํ•˜๋Š” Cache Stampede ๋ฌธ์ œ๋ฅผ ๋ฐœ๊ฒฌํ–ˆ๋‹ค.

๋จผ์ € 10๊ฐœ์˜ ๋™์‹œ ์š”์ฒญ์„ ์‚ฌ์šฉํ•œ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋กœ ์‹ค์ œ ๋ฌธ์ œ๋ฅผ ์žฌํ˜„ํ–ˆ๋‹ค.

๊ฐœ์„  ์ „
โ†’ Redis ๋žญํ‚น ์กฐํšŒ 10ํšŒ
โ†’ MySQL ์กฐํšŒ 10ํšŒ

์ดํ›„ @Cacheable(sync = true), Redis Locking Cache Writer, Redis ๋ถ„์‚ฐ ๋ฝ์„ ๋น„๊ตํ–ˆ๋‹ค.

ํ˜„์žฌ RoomPick์˜ ๋‹จ์ผ ์ธ์Šคํ„ด์Šค ๊ตฌ์กฐ์—์„œ๋Š” ๋™์ผ ์บ์‹œ Key๋งŒ ์ œ์–ดํ•˜๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‚ด๋ถ€ Single Flight๊ฐ€ ๊ฐ€์žฅ ์ ์€ ๋ณต์žก๋„๋กœ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ํŒ๋‹จํ–ˆ๋‹ค.

ConcurrentHashMap๊ณผ CompletableFuture๋ฅผ ์‚ฌ์šฉํ•ด ์ตœ์ดˆ ์š”์ฒญ์˜ ์„ฑ๊ณต ๊ฒฐ๊ณผ ๋˜๋Š” ์˜ˆ์™ธ๋ฅผ ๊ณต์œ ํ•˜๋„๋ก ๊ตฌํ˜„ํ–ˆ๊ณ , ๋™์ผํ•œ ํ…Œ์ŠคํŠธ ์กฐ๊ฑด์—์„œ ๋‹ค์Œ ๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ–ˆ๋‹ค.

๊ฐœ์„  ํ›„
โ†’ Redis ๋žญํ‚น ์กฐํšŒ 1ํšŒ
โ†’ MySQL ์กฐํšŒ 1ํšŒ
โ†’ ๋™์‹œ ์š”์ฒญ 10๊ฑด ๋ชจ๋‘ ๋™์ผํ•œ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜

๋˜ํ•œ ์บ์‹œ ๋น„ํ™œ์„ฑํ™” ์„ฑ๋Šฅ ์ธก์ • ์กฐ๊ฑด๊ณผ Redis ์žฅ์•  ์‹œ DB fallback ํ๋ฆ„๋„ ๊ทธ๋Œ€๋กœ ์œ ์ง€ํ–ˆ๋‹ค.

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


๐Ÿท๏ธ Tags

Spring Boot Redis Cache Cache Stampede Single Flight CompletableFuture ConcurrentHashMap ๋™์‹œ์„ฑ ์„ฑ๋Šฅ ์ตœ์ ํ™” MySQL Testcontainers Spring Cache

profile
๋๊นŒ์ง€ ๊ฐ€๋ฉด ๋‚ด๊ฐ€ ๋‹ค ์ด๊ฒจ

0๊ฐœ์˜ ๋Œ“๊ธ€