πŸ’» 개발/πŸ€ Spring

Redis 기반의 캐싱 및 λŒ€κΈ°μ—΄ 관리λ₯Ό ν†΅ν•œ μ½˜μ„œνŠΈ μ˜ˆμ•½ μ„œλΉ„μŠ€ μ„±λŠ₯ κ°œμ„ 

EastShine_ 2024. 11. 7. 21:33

 

λ“€μ–΄κ°€λ©°

이번 μ‹œκ°„μ—λŠ” μ½˜μ„œνŠΈ μ˜ˆμ•½ μ„œλΉ„μŠ€μ˜ μ„±λŠ₯을 κ°œμ„ ν•˜κΈ° μœ„ν•΄ ν˜„μž¬ μ‹œλ‚˜λ¦¬μ˜€μ˜ 쑰회 API 쀑 캐싱을 μ μš©ν•  뢀뢄에 λŒ€ν•΄ κ³ λ―Όν•΄ 보고, κΈ°μ‘΄ RDBμ—μ„œ μž‘λ™λ˜κ³  있던 λŒ€κΈ°μ—΄ λ‘œμ§μ„ Redis둜 μ΄κ΄€ν•˜λŠ” 과정에 λŒ€ν•΄ μ‚΄νŽ΄λ³΄κ² μŠ΅λ‹ˆλ‹€. 또 캐싱 μ „ν›„ ν…ŒμŠ€νŠΈ κ²°κ³Ό 비ꡐλ₯Ό 톡해 μ–Όλ§ˆλ‚˜ μ„±λŠ₯이 κ°œμ„ λ˜λŠ”μ§€λ„ 체크해 보도둝 ν•˜κ² μŠ΅λ‹ˆλ‹€.

 

 

 

 

μΊμ‹œ 적용 κΈ°μ€€

μš°μ„  μΊμ‹œλ₯Ό μ μš©ν•  λ•Œ κ³ λ €ν•΄μ•Ό ν•  기쀀은 μ•„λž˜μ™€ κ°™μŠ΅λ‹ˆλ‹€.

 

1. 쑰회 λΉ„μš©μ΄ 높은지

λ°μ΄ν„°λ² μ΄μŠ€μ—μ„œ 데이터λ₯Ό μ‘°νšŒν•˜λŠ” λΉ„μš©μ΄ 큰 경우 캐싱을 톡해 μ„±λŠ₯을 κ°œμ„ ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

 

2. μ–Όλ§ˆλ‚˜ 자주 μ‘°νšŒλ˜λŠ”μ§€

반볡적인 μš”μ²­μ΄ λ§Žμ€ 경우 μΊμ‹œλ₯Ό ν™œμš©ν•˜μ—¬ νš¨μœ¨μ„±μ„ 높일 수 μžˆμŠ΅λ‹ˆλ‹€.

 

3. 데이터 정합성에 μ΄μŠˆκ°€ μ—†λŠ”μ§€

μΊμ‹œλ₯Ό μ‚¬μš©ν•  경우, μΊμ‹œλœ 데이터와 원본 데이터 κ°„μ˜ 정합성을 μœ μ§€ν•  수 μžˆλŠ”μ§€ 확인해야 ν•©λ‹ˆλ‹€. μΊμ‹œλ₯Ό 적절히 κ°±μ‹ ν•  수 μžˆλ‹€λ©΄ 캐싱을 μ μš©ν•˜λŠ” 것이 μ’‹μŠ΅λ‹ˆλ‹€.

 

μΊμ‹œ μ μš©μ„ κ³ λ €ν•  λ•ŒλŠ” μΊμ‹œ 히트율이 κ°€μž₯ μ€‘μš”ν•œ μš”μ†Œμž…λ‹ˆλ‹€. 높은 νžˆνŠΈμœ¨μ„ μœ μ§€ν•˜λŠ” 것이 캐싱 효과λ₯Ό κ·ΉλŒ€ν™”ν•˜λŠ” 데 ν•„μš”ν•©λ‹ˆλ‹€. 이λ₯Ό 톡해 λ°μ΄ν„°λ² μ΄μŠ€ λΆ€ν•˜λ₯Ό 쀄이고, μ‹œμŠ€ν…œμ˜ 응닡 속도λ₯Ό 크게 κ°œμ„ ν•  μˆ˜κ°€ μžˆμŠ΅λ‹ˆλ‹€.

 

 

 

 

쑰회 API 별 뢄석

ν˜„μž¬ μ½˜μ„œνŠΈ μ˜ˆμ•½ μ„œλΉ„μŠ€μ—λŠ” λ‹€μ–‘ν•œ 쑰회 APIκ°€ μ‘΄μž¬ν•©λ‹ˆλ‹€. 각 API에 λŒ€ν•œ 뢄석과 μΊμ‹œ 적용 μ—¬λΆ€λ₯Ό νŒλ‹¨ν•΄λ³΄κ² μŠ΅λ‹ˆλ‹€.

 

1. μ½˜μ„œνŠΈ 정보 및 λ‚ μ§œ λͺ©λ‘ 쑰회 API βœ…

νŠΉμ • μ½˜μ„œνŠΈμ˜ 정보와 μ˜ˆμ•½ κ°€λŠ₯ν•œ λ‚ μ§œλ₯Ό μ‘°νšŒν•˜λŠ” APIμž…λ‹ˆλ‹€.

  - μ‘°νšŒ λΉ„μš© : 데이터 양이 크고 λ³΅μž‘ν•˜κΈ° λ•Œλ¬Έμ— 쑰회 λΉ„μš©μ΄ ν½λ‹ˆλ‹€.

  - μ‘°νšŒ λΉˆλ„ : μ‚¬μš©μžλ“€μ΄ μ½˜μ„œνŠΈλ₯Ό 검색할 λ•Œ 자주 μš”μ²­λ˜λŠ” APIμž…λ‹ˆλ‹€.

  - λ°μ΄ν„° μ •ν•©μ„± : λ‚ μ§œ 정보가 자주 λ³€κ²½λ˜μ§€ μ•Šμ•„ 캐싱 적용이 μ λ‹Ήν•©λ‹ˆλ‹€. λ‹€λ§Œ, λ‚ μ§œλ³„ μ˜ˆμ•½ κ°€λŠ₯ μ—¬λΆ€λŠ” μ‹€μ‹œκ°„μœΌλ‘œ λ³€κ²½λ˜λ―€λ‘œ μ˜ˆμ•½ κ°€λŠ₯ μ’Œμ„ 수λ₯Ό λ™κΈ°ν™”ν•˜λŠ” μŠ€μΌ€μ€„λ§ 주기에 맞좰 정합성을 맞좜 수 μžˆμŠ΅λ‹ˆλ‹€.

 

2. λŒ€κΈ°μ—΄ μƒνƒœ 쑰회 API ❌

μ‚¬μš©μžκ°€ λŒ€κΈ°μ—΄μ—μ„œ λͺ‡ λ²ˆμ§Έμ— μœ„μΉ˜ν•΄ μžˆλŠ”μ§€λ₯Ό ν™•μΈν•˜λŠ” APIμž…λ‹ˆλ‹€.

  - μ‘°νšŒ λΉ„μš© : λŒ€κΈ°μ—΄ μˆœμœ„ 정보 쑰회 μžμ²΄λŠ” κ°€λ²Όμš°λ‚˜, μ‹€μ‹œκ°„ 데이터가 ν•„μš”ν•©λ‹ˆλ‹€.

  - μ‘°νšŒ λΉˆλ„ : λŒ€κΈ° 쀑인 μ‚¬μš©μžκ°€ 자주 μ‘°νšŒν•˜μ§€λ§Œ, 항상 μ΅œμ‹  정보가 ν•„μš”ν•©λ‹ˆλ‹€.

  - λ°μ΄ν„° μ •ν•©μ„± : μ‹€μ‹œκ°„ 정보가 ν•„μš”ν•˜μ—¬ 캐싱을 μ μš©ν•  경우 μ •ν•©μ„± λ¬Έμ œκ°€ λ°œμƒν•  수 μžˆμŠ΅λ‹ˆλ‹€.

 

3. μ’Œμ„ μƒνƒœ 쑰회 API ❌

νŠΉμ • μ½˜μ„œνŠΈ λ‚ μ§œμ˜ μ’Œμ„ 정보λ₯Ό μ‘°νšŒν•˜λŠ” APIμž…λ‹ˆλ‹€.

  - μ‘°νšŒ λΉ„μš© : μ’Œμ„ μƒνƒœλŠ” λ³΅μž‘ν•œ μ •λ³΄μ΄λ―€λ‘œ 쑰회 λΉ„μš©μ΄ 비ꡐ적 ν½λ‹ˆλ‹€.

  - μ‘°νšŒ λΉˆλ„ : μ’Œμ„ μƒνƒœλŠ” μ˜ˆμ•½μ΄ 진행될 λ•Œ 자주 μ‘°νšŒλ©λ‹ˆλ‹€.

  - λ°μ΄ν„° μ •ν•©μ„± : μ˜ˆμ•½μ΄ 자주 λ³€κ²½λ˜κΈ° λ•Œλ¬Έμ— μ •ν•©μ„± μœ μ§€κ°€ μ–΄λ €μšΈ 수 μžˆμŠ΅λ‹ˆλ‹€.

 

4. μž”μ•‘ 쑰회 API ❌

νŠΉμ • μ‚¬μš©μžμ˜ ν˜„μž¬ μž”μ•‘μ„ μ‘°νšŒν•˜λŠ” APIμž…λ‹ˆλ‹€.

  - μ‘°νšŒ λΉ„μš© : μž”μ•‘ 쑰회 μžμ²΄λŠ” κ°„λ‹¨ν•˜μ—¬ λΉ„μš©μ΄ 크지 μ•ŠμŠ΅λ‹ˆλ‹€.

  - μ‘°νšŒ λΉˆλ„ : μ‚¬μš©μžκ°€ μΆ©μ „ λ˜λŠ” 결제 μ „ν›„λ‘œ μžμ‹ μ˜ μž”μ•‘μ„ 자주 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.

  - λ°μ΄ν„° μ •ν•©μ„± : μž”μ•‘ μ •λ³΄λŠ” μ‹€μ‹œκ°„μ„±μ΄ μš”κ΅¬λ  수 μžˆμ§€λ§Œ, μΆ©μ „/결제 μ‹œ μΊμ‹œλ₯Ό 적절히 κ°±μ‹ ν•œλ‹€λ©΄ 캐싱을 μ μš©ν•  μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€.

 

 

이 API 쀑, μ½˜μ„œνŠΈ 정보 및 λ‚ μ§œ λͺ©λ‘ μ‘°νšŒλŠ” 데이터 양이 크고 κ°±μ‹  μ£ΌκΈ°κ°€ 비ꡐ적 느리며 λ™μΌν•œ μš”μ²­μ΄ 반볡될 κ°€λŠ₯성이 λ†’κΈ° λ•Œλ¬Έμ— 캐싱 적용의 ν•„μš”μ„±μ΄ 크닀고 μƒκ°λ©λ‹ˆλ‹€. 반면 λŒ€κΈ°μ—΄ μƒνƒœ 쑰회, μ’Œμ„ μƒνƒœ μ‘°νšŒλŠ” μ‹€μ‹œκ°„μ„±μ„ μš”κ΅¬ν•˜κ³  μž”μ•‘ μ‘°νšŒλŠ” λΉ„μš©μ΄ 비ꡐ적 크지 μ•ŠκΈ° λ•Œλ¬Έμ— μΊμ‹œ 적용 λŒ€μƒμ— λ„£μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.

λ‹€λ§Œ, λŒ€κΈ°μ—΄ μƒνƒœ μ‘°νšŒλŠ” Redis둜 λŒ€κΈ°μ—΄ 관리λ₯Ό ν•˜κΈ° λ•Œλ¬Έμ— 캐싱이 μ•„λ‹ˆμ–΄λ„ λΉ λ₯Έ 쑰회 λ°©μ‹μœΌλ‘œ κ°œμ„ ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

 

 

 

 

μΊμ‹œ μ „ν›„ μ„±λŠ₯ 비ꡐ

μΊμ‹œ 적용 μ „ν›„μ˜ μ„±λŠ₯을 λΉ„κ΅ν•˜κΈ° μœ„ν•΄ Grafana와 k6λ₯Ό ν™œμš©ν•˜μ—¬ λΆ€ν•˜ ν…ŒμŠ€νŠΈλ₯Ό μ§„ν–‰ν–ˆμŠ΅λ‹ˆλ‹€.

 

ν…ŒμŠ€νŠΈ κ²°κ³Ό, μΊμ‹œ 적용 μ „μ—λŠ” 평균 1.56, μ΅œλŒ€ 10의 ν™œμ„± 컀λ„₯μ…˜μ΄ μžˆμ—ˆμ§€λ§Œ, μΊμ‹œ 적용 ν›„μ—λŠ” ν™œμ„± 컀λ„₯μ…˜μ΄ 0으둜 쀄어듀어 λ°μ΄ν„°λ² μ΄μŠ€μ— λŒ€ν•œ 직접적인 μš”μ²­μ΄ 거의 μ—†μ—ˆμŠ΅λ‹ˆλ‹€. λ˜ν•œ, λ™μ‹œ μš”μ²­μ΄ λ§Žμ„ λ•Œ ν’€ μ‚¬μ΄μ¦ˆμ˜ ν•œκ³„μ— 도달해 λŒ€κΈ° 컀λ„₯μ…˜μ΄ λ°œμƒν–ˆλ˜ 것과 비ꡐ해, μΊμ‹œ 적용 ν›„μ—λŠ” λŒ€κΈ° 컀λ„₯μ…˜μ΄ μ—†μ—ˆμŠ΅λ‹ˆλ‹€.

μΊμ‹œ 적용 ν›„, 동일 μ‹œκ°„ λ‚΄ μš”μ²­ μ²˜λ¦¬λŸ‰μ΄ μ†Œν­ μ¦κ°€ν–ˆμœΌλ©°, μ„œλ²„μ˜ 단건 μš”μ²­ 처리 μ‹œκ°„λ„ μ§§μ•„μ§„ 것을 ν™•μΈν–ˆμŠ΅λ‹ˆλ‹€. 

 

μ•„λž˜λŠ” μ£Όμš” κ²°κ³Όλ₯Ό μš”μ•½ν•œ ν‘œμ™€ Grafana κ²°κ³Ό μΊ‘μ³μž…λ‹ˆλ‹€.

 

ν•­λͺ© μΊμ‹œ 적용 μ „ μΊμ‹œ 적용 ν›„
HikariCP ν™œμ„± μ»€λ„₯μ…˜ (평균) 1.56 0.0
HikariCP ν™œμ„± μ»€λ„₯μ…˜ (μ΅œλŒ€) 10.0 0.0
HikariCP λŒ€κΈ° μ»€λ„₯μ…˜ (평균) 0.222 0.0
HikariCP λŒ€κΈ° μ»€λ„₯μ…˜ (μ΅œλŒ€) 2.0 0.0
API μš”μ²­ 수 (평균) 183.0 198.0
API μš”μ²­ 수 (μ΅œλŒ€) 453.0 486.0
HTTP μš”μ²­ 지속 μ‹œκ°„ (평균) 14.1 13.08
HTTP μš”μ²­ 지속 μ‹œκ°„ (μ΅œλŒ€) 264.95 182.45

 

 

ν…ŒμŠ€νŠΈ κ²°κ³Ό ( μΊμ‹œ 적용 μ „ )

 

 

ν…ŒμŠ€νŠΈ κ²°κ³Ό ( μΊμ‹œ 적용 ν›„ )

 

이λ₯Ό 톡해 캐싱이 λ°μ΄ν„°λ² μ΄μŠ€ λΆ€ν•˜λ₯Ό 쀄이고 응닡 μ‹œκ°„μ„ λ‹¨μΆ•μ‹œν‚€λŠ” 데 κΈ°μ—¬ν•˜λŠ” 것을 확인할 수 μžˆμ—ˆμŠ΅λ‹ˆλ‹€.

 

 

 

 

 

λŒ€κΈ°μ—΄ λ‘œμ§μ„ RDBμ—μ„œ Redis둜 이관

기쑴의 λŒ€κΈ°μ—΄ λ‘œμ§μ€ RDBλ₯Ό μ‚¬μš©ν•˜μ—¬ κ΄€λ¦¬ν•˜κ³  μžˆμ—ˆμŠ΅λ‹ˆλ‹€. ν•˜μ§€λ§Œ 이 방식은 μ‹€μ‹œκ°„ μ„±λŠ₯을 μš”κ΅¬ν•˜λŠ” λŒ€κΈ°μ—΄ μ‹œμŠ€ν…œμ— μ ν•©ν•˜μ§€ μ•Šμ•˜μœΌλ©°, λŒ€μš©λŸ‰ νŠΈλž˜ν”½ 처리 μ‹œ λ°μ΄ν„°λ² μ΄μŠ€μ— 높은 λΆ€ν•˜λ₯Ό μœ λ°œν•  수 μžˆμŠ΅λ‹ˆλ‹€. 이에 따라 λŒ€κΈ°μ—΄ λ‘œμ§μ„ Redis둜 μ΄κ΄€ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

 

Redis둜 μ΄κ΄€ν•˜λ©΄μ„œ ActiveToken(μ˜ˆμ•½ κ°€λŠ₯ ν™œμ„± 토큰)κ³Ό WaitingToken(λŒ€κΈ° 토큰)을 λΆ„λ¦¬ν•˜μ—¬ κ΄€λ¦¬ν•˜λŠ” λ°©ν–₯을 μ„€μ •ν•˜μ˜€μŠ΅λ‹ˆλ‹€. 각 λŒ€κΈ°/ν™œμ„± 토큰은 μ½˜μ„œνŠΈ μŠ€μΌ€μ€„ IDλ₯Ό Key둜 κ°€μ§€λŠ” ZSet(Sorted Set)으둜 κ΄€λ¦¬ν•˜μ˜€μŠ΅λ‹ˆλ‹€. 

이λ₯Ό 톡해, WaitingTokenμ—μ„œλŠ” ZRANK λͺ…λ Ήμ–΄λ₯Ό μ‚¬μš©ν•˜μ—¬ O(log(N)) 의 μ‹œκ°„λ³΅μž‘λ„λ‘œ μˆœμœ„λ₯Ό κ°€μ Έμ˜¬ 수 μžˆμŠ΅λ‹ˆλ‹€. λ˜ν•œ ActiveTokenμ—μ„œλŠ” Score에 만료 μ‹œκ°„μ„ κΈ°λ‘ν•˜μ—¬ ZREMRANGEBYSCORE λͺ…λ Ήμ–΄λ‘œ 만료된 토큰을 λΉ λ₯΄κ²Œ μ‚­μ œν•  수 μžˆμŠ΅λ‹ˆλ‹€.

WaitingTokenκ³Ό ActiveToken

 

또 각 토큰을 λ³„λ„μ˜ Hash ꡬ쑰둜 μ €μž₯ν•˜μ—¬, 토큰을 key둜 μ‚¬μš©ν•˜κ³  valueμ—λŠ” μŠ€μΌ€μ€„ ID, μƒνƒœκ°’, 만료일자λ₯Ό μ €μž₯ν•˜λ„λ‘ κ΅¬μ„±ν–ˆμŠ΅λ‹ˆλ‹€. 이λ₯Ό 톡해 νŠΉμ • 토큰에 λŒ€ν•œ 쑰회 μ‹œ 데이터 μ ‘κ·Ό 속도λ₯Ό 크게 ν–₯μƒμ‹œν‚¬ μˆ˜ μžˆμ—ˆμŠ΅λ‹ˆλ‹€.

토큰 별 정보λ₯Ό λ‹΄κ³ μžˆλŠ” Hash

 

 

μ½”λ“œ λ³€κ²½μ μœΌλ‘œλŠ” κΈ°μ‘΄ JPAλ₯Ό μ‚¬μš©ν•˜λŠ” λ‘œμ§μ„ RedisTemplateλ₯Ό μ‚¬μš©ν•œ 둜직으둜 λ³€κ²½ν•˜μ˜€μœΌλ©°, 만료 μ‹œκ°„ 관리와 μƒνƒœ λ³€κ²½ μ—­μ‹œ Redis둜 μ²˜λ¦¬ν•  수 μžˆλ„λ‘ λ¦¬νŒ©ν„°λ§ ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

 

μ•„λž˜λŠ” 토큰 λ°œκΈ‰ μ‹œ RedisTemplate을 μ‚¬μš©ν•˜μ—¬ Redis에 μ €μž₯ν•˜λ„λ‘ λ³€κ²½ν•œ μ½”λ“œ μ˜ˆμ‹œμž…λ‹ˆλ‹€.

@Repository
class WaitingQueueRedisRepository(
    private val redisTemplate: RedisTemplate<String, Any>,
) : WaitingQueueRepository {
    companion object {
        const val WAITING_TOKEN_PREFIX = "WaitingToken"
        const val ACTIVE_TOKEN_PREFIX = "ActiveToken"
        const val TOKEN_INFO_PREFIX = "TokenInfo"
    }

    override fun addWaitingQueue(waitingQueue: WaitingQueue): WaitingQueue {
        // 1. WaitingToken μ €μž₯
        redisTemplate.opsForZSet().add(
            "$WAITING_TOKEN_PREFIX:${waitingQueue.scheduleId}",
            waitingQueue.token,
            System.currentTimeMillis().toDouble(),
        )
        redisTemplate.expire("$WAITING_TOKEN_PREFIX:${waitingQueue.scheduleId}", Duration.ofHours(1))

        // 2. TokenInfo μ €μž₯
        val key = "$TOKEN_INFO_PREFIX:${waitingQueue.token}"
        val fields =
            mapOf(
                "scheduleId" to waitingQueue.scheduleId.toString(),
                "status" to waitingQueue.status.name,
            )
        redisTemplate.opsForHash<String, String>().putAll(key, fields)
        redisTemplate.expire(key, Duration.ofHours(1))
        return waitingQueue
    }
    
    /* ... */
}

 

 

 

 

마치며

μ½˜μ„œνŠΈ μ˜ˆμ•½ μ„œλΉ„μŠ€μ˜ μ„±λŠ₯을 κ°œμ„ ν•˜κΈ° μœ„ν•΄ 쑰회 API에 캐싱을 μ μš©ν•˜κ³ , λŒ€κΈ°μ—΄ λ‘œμ§μ„ Redis둜 μ΄κ΄€ν•˜λŠ” 과정을 정리해 λ³΄μ•˜μŠ΅λ‹ˆλ‹€. 캐싱을 톡해 반볡적인 쑰회 μš”μ²­μ˜ μ„±λŠ₯을 κ°œμ„ ν•˜κ³ , Redisλ₯Ό ν™œμš©ν•˜μ—¬ λŒ€κΈ°μ—΄ 둜직의 μ‹€μ‹œκ°„ μ„±λŠ₯을 ν–₯μƒμ‹œν‚¬ 수 μžˆμ—ˆμŠ΅λ‹ˆλ‹€. 이번 과정을 톡해 캐싱을 μ μš©ν•˜λŠ” κΈ°μ€€κ³Ό Redisλ₯Ό ν™œμš©ν•œ μ„±λŠ₯ μ΅œμ ν™” 기법을 ν† λŒ€λ‘œ 좔후에 싀무에 μ μš©ν•΄ λ³Ό 수 μžˆμ„ 것 κ°™μŠ΅λ‹ˆλ‹€.