JiwonDev

Cache#1 ์Šคํ”„๋ง์˜ ์บ์‹œ ์ถ”์ƒํ™”(@Cacheable)

by JiwonDev

์Šคํ”„๋ง ํ”„๋ ˆ์ž„์›Œํฌ๋Š” ๋‹ค๋ฅธ ์„œ๋น„์Šค์™€์˜ ํ†ตํ•ฉ & ๊ธฐ๋Šฅ ์ถ”์ƒํ™”๋ฅผ ์ œ๊ณตํ•ด์ค€๋‹ค. (RedisTemplate, RestTemplate, MailSender....)

2012๋…„ ์Šคํ”„๋ง 3.1๋ฒ„์ „๋ถ€ํ„ฐ ์บ์‹œ ์ถ”์ƒํ™”(Cache Abstraction)์„ ์ œ๊ณตํ•ด์ฃผ๋ฉฐ, ์ด๋Š” 2014๋…„ ์Šคํ”„๋ง 4.1์—์„œ ์—ฌ๋Ÿฌ๊ฐ€์ง€๊ฐ€ ๊ฐœ์„ ๋˜์—ˆ๋‹ค.

์Šคํ”„๋ง ํ”„๋ ˆ์ž„์›Œํฌ์˜ ๋‹ค๋ฅธ ์„œ๋น„์Šค์™€์˜ ํ†ตํ•ฉ(Integration) ๊ด€๋ จ ๋ฌธ์„œ

 

Integration

As a lightweight container, Spring is often considered an EJB replacement. We do believe that for many, if not most, applications and use cases, Spring, as a container, combined with its rich supporting functionality in the area of transactions, ORM and JD

docs.spring.io

 

๐Ÿ“Œ ์บ์‹œ์— ๋Œ€ํ•œ ์ดํ•ด

2022.08.28 - [๐ŸŒฑ Spring Framework] - Cache#2 ์บ์‹œ์— ๋Œ€ํ•œ ์ดํ•ด

 

Cache#2 ์บ์‹œ์— ๋Œ€ํ•œ ์ดํ•ด

๐Ÿ“Œ Cache Cache๋Š” ์ปดํ“จํ„ฐ ๊ณผํ•™์—์„œ, ๋ฐ์ด๋‚˜ ๊ฐ’์„ ๋ฏธ๋ฆฌ ๋ณต์‚ฌํ•ด๋†“๋Š” ์ž„์‹œ ์ €์žฅ์†Œ๋ฅผ ๊ฐ€๋ฅดํ‚จ๋‹ค. ํ•œ๊ธ€๋กœ๋Š” ๊ณ ์†์™„์ถฉ๊ธฐ์–ต๊ธฐ..์ง€๋งŒ ์•„๋ฌด๋„ ๊ทธ๋ ‡๊ฒŒ ์•ˆ๋ถ€๋ฅธ๋‹ค ์„ฑ๋Šฅํ–ฅ์ƒ์„ ์œ„ํ•ด ๊ฒฐ๊ณผ๋ฅผ ์บ์‹ฑ(์ €์žฅ)ํ•˜์—ฌ ๊ฐ’์„

jiwondev.tistory.com

 

 


๐Ÿ“Œ ์Šคํ”„๋ง์˜ ์บ์‹œ ์ถ”์ƒํ™” (Spring Cache Abstraction)

์–ด๋…ธํ…Œ์ด์…˜ ๊ธฐ๋ฐ˜์œผ๋กœ ์บ์‹œ๋ฅผ ์ถ”์ƒํ™”ํ•˜์—ฌ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด๋Š” Spring Data ์˜ @Transcational ๊ณผ ๊ต‰์žฅํžˆ ์œ ์‚ฌํ•˜๋‹ค.

AOP ๊ธฐ๋ฐ˜์œผ๋กœ ๋ฉ”์„œ๋“œ์˜ ๊ฒฐ๊ณผ๋ฅผ ์บ์‹ฑํ•˜์—ฌ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. (* ์บ์‹ฑ๋œ ๊ฒฝ์šฐ, ๋ฉ”์„œ๋“œ๋ฅผ ์‹คํ–‰ํ•˜์ง€ ์•Š๊ณ  ๋ฐ”๋กœ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.)

@Cacheable("bestSeller") // cacheName: bestSeller, cachekey : BookNo๋กœ, ๋ฉ”์„œ๋“œ ๊ฒฐ๊ณผ๋ฅผ ์บ์‹ฑํ•œ๋‹ค.
public Book getBestSeller(String bookNo) {
   return result
}

 

 

์Šคํ”„๋ง์˜ ์บ์‹œ ์ถ”์ƒํ™”์— ์‚ฌ์šฉ๋˜๋Š” ๊ฐ์ฒด๋“ค์€ org.springframework.cache ํŒจํ‚ค์ง€์—์„œ ์ฐพ์•„๋ณผ ์ˆ˜ ์žˆ๋‹ค.

org.springframework.cache.Cache
org.springframework.cache.CacheManager

์Šคํ”„๋ง 4.1๋ถ€ํ„ฐ๋Š” ์ž๋ฐ” ํ‘œ์ค€์ธ JSR-107 ์–ด๋…ธํ…Œ์ด์…˜๋„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.

 

 


๐Ÿ“Œ ์Šคํ”„๋ง ํ”„๋ ˆ์ž„์›Œํฌ์— ์บ์‹œ ๊ตฌํ˜„์ฒด(์ €์žฅ์†Œ)๋Š” ์—†๋‹ค.

Cache, CacheManager๋Š” ์บ์‹œ ์‚ฌ์šฉ์„ ์ถ”์ƒํ™”ํ•˜๋Š”๊ฑฐ์ง€, ์‹ค์ œ ์บ์‹œ ์ €์žฅ์†Œ๋Š” ํฌํ•จํ•˜์ง€ ์•Š๋Š”๋‹ค.

์บ์‹œ์— ๋Œ€ํ•œ ์„ธ๋ถ€์„ค์ •(TTL, ์ •์ฑ…)์ด๋‚˜ ๋ฉ€ํ‹ฐ ์Šค๋ ˆ๋“œ ๋™๊ธฐํ™”๋“ฑ์€ ์บ์‹œ ์ €์žฅ์†Œ ๊ตฌํ˜„์ฒด๊ฐ€ ์ฒ˜๋ฆฌํ•ด์•ผํ•œ๋‹ค. ์ด๋Š” ๊ณต์‹๋ฌธ์„œ์—๋„ ๊ฐ•์กฐ๋œ ๋‚ด์šฉ์ด๋‹ค.

 

@Cacheable ๊ฐ™์€ ์–ด๋…ธํ…Œ์ด์…˜๋“ค์€ ์Šคํ”„๋ง ๋นˆ์œผ๋กœ ๋“ฑ๋ก๋˜์–ด์žˆ๋Š” CacheManager ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ด์šฉํ•˜์—ฌ ์บ์‹œ๋ฅผ ์ ์šฉ์‹œํ‚จ๋‹ค.
์ด๋ฅผ ์ฝ”๋“œ๋กœ ์ง์ ‘ ๊ตฌํ˜„ํ•ด๋„ ๋˜์ง€๋งŒ, ์Šคํ”„๋ง๋ถ€ํŠธ์—์„œ ์•„๋ž˜์™€ ๊ฐ™์€ ์บ์‹œ ๊ตฌํ˜„์ฒด๋“ค์„ CacheManager๋กœ ์‰ฝ๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ๊ฒŒ ์ œ๊ณตํ•ด์ค€๋‹ค.

// ์Šคํ”„๋ง๋ถ€ํŠธ๋ฅผ ์ด์šฉํ•˜์—ฌ ํ˜„์—…์—์„œ ์ž์ฃผ ์‚ฌ์šฉํ•˜๋Š” ์บ์‹œ ๊ตฌํ˜„์ฒด๋“ค์€ ๋Œ€๋ถ€๋ถ„ ์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

implementation 'org.springframework.boot:spring-boot-starter-cache'// ์Šคํ”„๋ง application ๋‚ด๋ถ€ ์บ์‹œ
implementation 'org.springframework.boot:spring-boot-starter-data-redis'// redis
implementation 'com.github.ben-manes.caffeine:caffeine'// caffenie

ํ•ด๋‹น ์บ์‹œ๊ตฌํ˜„์ฒด๋Š” ์Šคํ”„๋ง ๋ถ€ํŠธ์—์„œ ์„ค์ • ์—ฐ๋™์ด๋‚˜ ์•„์˜ˆ spring-boot-starter-...๋ฅผ ์ถ”๊ฐ€์ ์œผ๋กœ ์ œ๊ณตํ•œ๋‹ค

ํ•ด๋‹น ์บ์‹œ ๊ตฌํ˜„์ฒด๋“ค์€ ๋”ฐ๋กœ ์„ค์ •ํ•ด์ฃผ์ง€ ์•Š์•„๋„, ์„ค์ •ํŒŒ์ผ(yml) ๊ธฐ๋ฐ˜์œผ๋กœ ์บ์‹œ ์„ค์ •์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

์ž์„ธํ•œ๊ฑด ๊ณต์‹๋ฌธ์„œ๋ฅผ ์ฐธ๊ณ ํ•˜์ž.

 

 


๐Ÿ“Œ ์Šคํ”„๋ง์˜ ์บ์‹œ ์ถ”์ƒํ™” ์ ์šฉ๋ฐฉ๋ฒ• (@EnableCaching)

๊ฐ„๋‹จํ•˜๋‹ค. @Configuration ํด๋ž˜์Šค์— @EnableCaching ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ , CacheManager ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๋นˆ์„ ๋“ฑ๋กํ•˜๋ฉด ๋œ๋‹ค.
ํ•œ๋ฒˆ ๋” ์–ธ๊ธ‰ํ•˜๋Š”๋ฐ, ์Šคํ”„๋ง์˜ ์บ์‹œ ์ถ”์ƒํ™” ์–ด๋…ธํ…Œ์ด์…˜๋“ค์€ CacheManager ๋ฅผ ํ†ตํ•ด ์บ์‹œ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.
CacheManager ์˜ ์„ค์ • ๋ฐฉ๋ฒ•์€ ์บ์‹œ ๊ตฌํ˜„์ฒด๋งˆ๋‹ค ๋‹ค๋ฅผ ์ˆ˜ ์žˆ๋‹ค. ์ด๋Š” ๊ธ€ ํ•˜๋‹จ์— ๋‹ค์‹œ ์„ค๋ช…ํ•˜๋‹ˆ, ์ผ๋‹จ์€ ๊ฐ€๋ณ๊ฒŒ ๋ณด๊ณ  ๋„˜์–ด๊ฐ€์ž

  @Configuration
  @EnableCaching
  class RedisCacheConfig {
    
    // ์Šคํ”„๋ง์—์„œ ์บ์‹œ ์ถ”์ƒํ™”๋Š” CacheManager ๊ฐ์ฒด๋ฅผ ํ†ตํ•ด ์ž‘๋™ํ•œ๋‹ค. ํ•ด๋‹น ๋นˆ์„ ๋“ฑ๋กํ•ด์ฃผ์ž
    @Bean
    fun userCacheManager(objectMapper: ObjectMapper, redisProperties: RedisProperties): CacheManager {
      
      // RedisClient ๊ตฌํ˜„์ฒด๋ฅผ Lettuce ๋กœ ๋ณ€๊ฒฝ (์Šคํ”„๋ง-๋ถ€ํŠธ-๋ ˆ๋””์Šค์˜ ๊ธฐ๋ณธ ๊ตฌํ˜„์ฒด : Jedis )
      val redisConnectionFactory = LettuceConnectionFactory(redisProperties.host, redisProperties.port)

      // Redis ์บ์‹œ์„ค์ •
      val redisConfiguration = RedisCacheConfiguration.defaultCacheConfig()
        .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer ()))
        .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer ()))
        .disableCachingNullValues()
        .entryTtl(Duration.ofHours(5L));

      // ์Šคํ”„๋ง์—์„œ ์‚ฌ์šฉํ•  CacheManager ๋นˆ ์ƒ์„ฑ (spring-boot-starter-data-redis)
      return RedisCacheManager.RedisCacheManagerBuilder
        .fromConnectionFactory(redisConnectionFactory)
        .cacheDefaults(redisConfiguration)
        .build()
    }
  }

 

๋ฌผ๋ก  CacheManager ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ง์ ‘ ๊ตฌํ˜„ํ•ด๋„ ๋˜์ง€๋งŒ, ์Šคํ”„๋ง์—์„œ๋Š” ์ด๋ฏธ ์•„๋ž˜์™€ ๊ฐ™์ด ๋‹ค์•™ํ•œ ๊ตฌํ˜„์ฒด๋ฅผ ์ œ๊ณตํ•˜๊ณ  ์žˆ๋‹ค.

  • ConcurrentMapCacheManager : ๊ฐ„๋‹จํ•˜๊ฒŒ ConcurrentHashMap์„ ์‚ฌ์šฉํ•˜๋Š” CocurrentMapCache ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. 
  • SimpleCacheManager : ํ”„๋กœํผํ‹ฐ๋ฅผ ์ด์šฉํ•ด์„œ ์‚ฌ์šฉํ•  ์บ์‹œ๋ฅผ ์ง์ ‘ ๋“ฑ๋กํ•ด์ฃผ๋Š” ๋ฐฉ๋ฒ•(๊ธฐ๋ณธ์ ์œผ๋กœ ์ œ๊ณตํ•˜๋Š” ์บ์‹œ๊ฐ€ ์—†๋‹ค)
     ์ด๋Š” ์Šคํ”„๋ง Cache ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•ด์„œ ์บ์‹œ ํด๋ž˜์Šค๋ฅผ ์ง์ ‘ ๋งŒ๋“œ๋Š” ๊ฒฝ์šฐ ํ…Œ์ŠคํŠธ์—์„œ ์‚ฌ์šฉํ•˜๊ธฐ์— ์ ๋‹นํ•˜๋‹ค.
  • CompositeCacheManager : ํ•˜๋‚˜ ์ด์ƒ์˜ ์บ์‹œ ๋งค๋‹ˆ์ €๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์ง€์›ํ•ด์ฃผ๋Š” ํ˜ผํ•ฉ ์บ์‹œ ๋งค๋‹ˆ์ €๋‹ค.
    ์—ฌ๋Ÿฌ ์บ์‹œ ์ €์žฅ์†Œ๋ฅผ ๋™์‹œ์— ์‚ฌ์šฉํ•ด์•ผ ํ•  ๋•Œ ์“ด๋‹ค. cacheManagers ํ”„๋กœํผํ‹ฐ์— ์ ์šฉํ•  ์บ์‹œ ๋งค๋‹ˆ์ € ๋นˆ์„ ๋ชจ๋‘ ๋“ฑ๋กํ•ด์ฃผ๋ฉด ๋œ๋‹ค.
  • NoOpCacheManager : ์บ์‹œ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค. ์บ์‹œ๊ฐ€ ์ง€์›๋˜์ง€ ์•Š๋Š” ํ™˜๊ฒฝ์—์„œ ๋™์ž‘ํ•  ๋•Œ ์บ์‹œ ๊ด€๋ จ ์„ค์ •์„ ์ œ๊ฑฐํ•˜์ง€ ์•Š์•„๋„ ์—๋Ÿฌ๊ฐ€ ๋‚˜์ง€ ์•Š๊ฒŒ ํ•ด์ค€๋‹ค.

๊ทธ ์™ธ ๋งŽ์ด ์‚ฌ์šฉํ•˜๋Š” ์บ์‹œ์ €์žฅ์†Œ์˜ ๊ตฌํ˜„์ฒด๊ฐ€ ์ด๋ฏธ ์กด์žฌํ•œ๋‹ค.

  • JCacheCacheManager: ์ž๋ฐ” ํ‘œ์ค€ JSR107 ๊ธฐ๋ฐ˜์œผ๋กœ ๋™์ž‘ํ•˜๋Š” ๊ตฌํ˜„์ฒด๋ฅผ ์œ„ํ•œ ์บ์‹œ๋งค๋‹ˆ์ €
  • RedisCacheManager : Redis ์ง€์›ํ•˜๋Š” ์บ์‹œ๋งค๋‹ˆ์ €
  • EhCacheCacheManager : EhCache ์ง€์›ํ•˜๋Š” ์บ์‹œ๋งค๋‹ˆ์ €
  • CaffeineCacheManager: Caffeine ์ง€์›ํ•˜๋Š” ์บ์‹œ๋งค๋‹ˆ์ €
  • ... ์ƒ๋žต

 

 


๐Ÿ“Œ ์บ์‹œ ์‚ฌ์šฉ๋ฐฉ๋ฒ•

์Šคํ”„๋ง ์บ์‹œ๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ฉ”์„œ๋“œ ๋‹จ์œ„์˜ AOP๋กœ ๊ตฌํ˜„๋˜์–ด ์žˆ๋‹ค.

๋ฉ”์„œ๋“œ์— ์•„๋ž˜ ์–ด๋…ธํ…Œ์ด์…˜๋“ค์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ฉ”์„œ๋“œ ๋ฐ˜ํ™˜๊ฐ’์„ ์บ์‹ฑํ•  ์ˆ˜ ์žˆ๋‹ค. ์บ์‹ฑ๋œ ๊ฒฝ์šฐ ๋ฉ”์„œ๋“œ๋Š” ์•„์˜ˆ ์‹คํ–‰๋˜์ง€ ์•Š๋Š”๋‹ค๋Š” ์ ์„ ์œ ์˜ํ•˜์ž.

๐Ÿงจ @Transcational๊ณผ ๋™์ผํ•˜๊ฒŒ, @Cacheable์ด ๊ฑธ๋ ค์žˆ๋Š” ํด๋ž˜์Šค ๋‚ด๋ถ€ ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœ(Self-invocation)ํ•˜๋ฉด AOP๊ฐ€ ๋™์ž‘ํ•˜์ง€ ์•Š๋Š”๋‹ค.

 

SPEL ๋ฌธ๋ฒ• ( ๊ณต์‹๋ฌธ์„œ )

์กฐ๊ฑด์— ๋”ฐ๋ผ ์บ์‹œ๋ฅผ ๋‹ค๋ฅด๊ฒŒ ๋™์ž‘ํ•˜๊ฒŒ ํ•˜๊ฑฐ๋‚˜, ์บ์‹œ key๋ฅผ ์ง€์ •ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด SPEL ๋ฌธ๋ฒ•์„ ์‚ฌ์šฉํ•˜์—ฌ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.
์Šคํ”„๋ง ์บ์‹œ๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ฉ”์„œ๋“œ์˜ ํŒŒ๋ผ๋ฉ”ํƒ€๋ฅผ ์บ์‹œ key ๋กœ ์‚ฌ์šฉํ•œ๋‹ค. ( #๊ฐ’, #๊ฐ์ฒด๋ช….๋ณ€์ˆ˜๋ช…, #๊ฐ์ฒด๋ช….๋ฉ”์„œ๋“œ()) ๋กœ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.

// ํŠน์ • ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ํ‚ค๋กœ ์‚ฌ์šฉ
@Cacheable(value="product", key="#productNo")
Product bestProduct(String productNo, User user, Date dateTime) {
	
}

// ํŒŒ๋ผ๋ฏธํ„ฐ์˜ ํŠน์ • ํ”„๋กœํผํ‹ฐ ๊ฐ’์„ ํ‚ค๋กœ ์‚ฌ์šฉ
@Cacheable(value="product", key="#condition.productNo")
Product bestProduct(SearchCondition condition) {
	
}

// ์‚ฌ์šฉ์ž์˜ ํƒ€์ž…์ด ๊ด€๋ฆฌ์ž์ธ ๊ฒฝ์šฐ์—๋งŒ ์บ์‹œ๋ฅผ ์ ์šฉ (user.type ํ”„๋กœํผํ‹ฐ๊ฐ€ "ADMIN"์ธ ๊ฒฝ์šฐ์—๋งŒ ์บ์‹œ ์ ์šฉ)
@Cacheable(value="user", condition="#user.type == 'ADMIN'")
public User findUser(User user) {
	...
}




๋งŒ์•ฝ ๋ฐ˜ํ™˜๊ฐ’์ด๋‚˜ ๋ฉ”์„œ๋“œ์˜ ์ด๋ฆ„๋“ฑ ๋‹ค๋ฅธ ๊ฐ’์„ ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด Spring Cache์ „์šฉ SPEL ๋ฌธ๋ฒ•์„ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด (๋ฉ”์„œ๋“œ ๋ฐ˜ํ™˜ ๊ฐ์ฒด์˜ id ํ•„๋“œ)๋ฅผ ์บ์‹œ ํ‚ค๋กœ ์‚ฌ์šฉํ•˜๊ณ ์‹ถ๋‹ค๋ฉด, key="#result.id" ๋กœ ์ž‘์„ฑํ•˜๋ฉด ๋œ๋‹ค. ์ž์„ธํ•œ๊ฑด ๊ณต์‹๋ฌธ์„œ๋ฅผ ์ฐธ๊ณ ํ•˜์ž

 

 

@Cacheable

(์กฐ๊ฑด์— ๋”ฐ๋ผ) ๋ฉ”์„œ๋“œ ๊ฒฐ๊ณผ๊ฐ’์„ ์บ์‹œ๋กœ ์ €์žฅํ•œ๋‹ค.
์—ฌ๋Ÿฌ ๊ฐœ์˜ ์บ์‹œ ์ด๋ฆ„์„ ๋ถ™์ผ ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ตœ์†Œ ํ•˜๋‚˜ ์ด์ƒ์˜ cache hit์ด ์กด์žฌํ•œ๋‹ค๋ฉด, ๋ฉ”์„œ๋“œ๋ฅผ ์‹คํ–‰ํ•˜์ง€ ์•Š๊ณ  ํ•ด๋‹น ์บ์‹œ ๊ฐ’์„ ๋ฐ”๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

@Cacheable(cacheNames="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

@Cacheable(cacheNames="books", key="#isbn.rawNumber")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
// unless = "..SPEL.." ์˜ ๊ฒฐ๊ณผ๊ฐ€ true์ด๋ฉด ์บ์‹ฑํ•˜์ง€ ์•Š๋Š”๋‹ค.
@Cacheable(value="spittleCache" unless="#result.message.contains('NoCache')")
Spittle findOne(long id);

// condition = ".." ๊ฒฐ๊ณผ๊ฐ€ true ์ด๋ฉด ์บ์‹ฑํ•œ๋‹ค.
// ๋ฌผ๋ก  unless ์™€ condition์„ ๋‘˜ ๋‹ค ์ ์–ด๋„ ๋œ๋‹ค.
@Cacheable(value="spittleCache" unless="#result.message.contains('NoCache')" condition="#id >= 10")
Spittle findOne(long id);

// ์ฐธ๊ณ ๋กœ ์ฝ”ํ‹€๋ฆฐ์ฒ˜๋Ÿผ ?. null-safety ๋ฌธ๋ฒ•๋„ ์ œ๊ณตํ•ด์ค€๋‹ค ใ…‹ 
@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result?.hardback")
public Optional<Book> findBook(String name)

@Cacheable(value="..") ๋Š” @Cacheable(cacheName="..") ๊ณผ ๊ฐ™์€ ๊ฐ’์ด๋‹ค.

์บ์‹œ๋ฅผ ํƒˆ ์กฐ๊ฑด์€ condition, ์บ์‹œ๋ฅผ ํƒ€์ง€ ์•Š์„ ์กฐ๊ฑด์€ unless ์— ์ ์œผ๋ฉด ๋œ๋‹ค. ํ•„์š”ํ•˜๋‹ค๋ฉด ๋‘˜ ๋‹ค ์ ์–ด๋„ ๋œ๋‹ค.

cacheable ์„ค์ • ๊ฐ’

 

๐Ÿงจ ์ฃผ์˜ - condition์€ ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜๊ธฐ ์ „์—, unless๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•œ ๋’ค์— ํ‰๊ฐ€ํ•œ๋‹ค. (๊ณต์‹๋ฌธ์„œ)

@Cacheable(condition="..")์€ ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜๊ธฐ ์ „์— SPEL์„ ์‹คํ–‰ํ•ด์„œ true์ธ์ง€ ํ™•์ธํ•œ๋‹ค.
์ฆ‰ ๋ฉ”์„œ๋“œ์˜ ํŒŒ๋ผ๋ฉ”ํƒ€๊ฐ€ ์•„๋‹Œ, ๋ฉ”์„œ๋“œ์˜ ๋ฐ˜ํ™˜๊ฐ’ (#result) ๋“ฑ์˜ ์กฐ๊ฑด์€ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋‹ค. ์บ์‹œ ์กฐ๊ฑด์ด ์ •์ƒ์ ์œผ๋กœ ๋™์ž‘ํ•˜์ง€ ์•Š๋Š”๋‹ค.

 

๋ฐ˜ํ™˜๊ฐ’์„ ์บ์‹œ ํ‚ค๋กœ ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ์€ ๊ฒฝ์šฐ, unless ์กฐ๊ฑด๋งŒ์„ ์‚ฌ์šฉํ•ด์•ผํ•œ๋‹ค.

๊ฟ€ํŒ์„ ํ•˜๋‚˜ ์ฃผ์ž๋ฉด CacheableResult ๊ฐ™์€ ์ „์šฉ ๊ฐ์ฒด๋ฅผ ํ•˜๋‚˜ ๋งŒ๋“ค์–ด์„œ ์‚ฌ์šฉํ•˜๋ฉด cache hit ์—ฌ๋ถ€๋„ ์•Œ ์ˆ˜ ์žˆ๊ณ , SPEL ์กฐ๊ฑด๋„ ๊น”๋”ํ•ด์ง„๋‹ค.

// ๋‚ด๊ฐ€ ๋งŒ๋“  Cache์šฉ ๊ฐ์ฒด
class CacheableResult<T>(
  val data: T,
  val isFailed: Boolean = false // ๊ธฐ๋ณธ ๊ฐ’์„ isFailed=false ๋กœ ์„ค์ •
)
/* ๋ฉ”์„œ๋“œ ์‹คํ–‰ ํ›„๋ฅผ ํ‰๊ฐ€ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด unless ๋ฅผ ์‚ฌ์šฉํ•ด์•ผํ•œ๋‹ค. ์ด ๋•Œ ์ด๋Ÿฐ์‹์œผ๋กœ ๊ตฌ์„ฑํ•˜๋ฉด ์ฝ”๋“œ ๊ฐ€๋…์„ฑ์ด ์ข‹์•„์ง„๋‹ค. */
@Cacheable(value = ["myCacheName"], unless = "#result.isFailed()")
fun getData(categoryId: String): CacheableResult<List<CategoryRes>> {

    return CacheableResult(
    	data = data,
        isFailed = true // ์ด๋ ‡๊ฒŒ ์บ์‹œ ์—ฌ๋ถ€๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.
    )
}

 

 

@CacheEvict,  @CachePut

  • @CacheEvict  : (์กฐ๊ฑด์— ๋”ฐ๋ผ) ์บ์‹œ๋ฅผ ์‚ญ์ œํ•œ๋‹ค. ์ฐธ๊ณ ๋กœ Evict์€ ์ซ“์•„๋‚ด๋‹ค, ํ‡ด๊ฑฐํ•˜๋‹ค๋Š” ์˜๋ฏธ์˜ ๋‹จ์–ด์ด๋‹ค.

beforeInvocation, allEntries๋Š” ์‚ญ์ œ(CacheEvict)์—๋งŒ ์กด์žฌํ•˜๋Š” ์„ค์ •์ด๋‹ค.

// bestProduct ์บ์‹œ์˜ ๋‚ด์šฉ์„ ์ œ๊ฑฐํ•œ๋‹ค
@CacheEvict(value="bestProduct")
public void refreshBestProducts() {
	// ...
}

// productNo์™€ ๊ฐ™์€ ํ‚ค๊ฐ’์„ ๊ฐ€์ง„ ์บ์‹œ๋ฅผ ์ œ๊ฑฐํ•œ๋‹ค.
@CacheEvict(value="product", key="#product.productNo")
public void updateProduct(Product product) {
	// ...
}

// ๊ทธ๋ƒฅ ํ†ต์งธ๋กœ ์‚ญ์ œํ•œ๋‹ค.
@CacheEvict(value = "bestSeller", allEntires = true)
public void clearBestSeller() {

}

 

  • @CachePut : ๋ฉ”์„œ๋“œ ์‹คํ–‰์„ ๋ฐฉํ•ดํ•˜์ง€ ์•Š๊ณ , ์บ์‹œ๋ฅผ ์—…๋ฐ์ดํŠธํ•œ๋‹ค. (๋ฉ”์„œ๋“œ๋Š” ์–ธ์ œ๋‚˜ ์‹คํ–‰๋œ๋‹ค.) 
// ํŠน์ • ์บ์‹œ๋ฅผ ์‚ญ์ œํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด @CacheEvict ์„ ์‚ฌ์šฉํ•˜์ž.
@CacheEvict(value = ["product"], key = "#id")
fun updateProduct(long id, dto: UpdateProductDto) {
  val product: Product = repository.findById(id)

  product.update(
    name = dto.name,
    price = dto.price,
    owner = dto.owner
  )
}

// ๋ฉ”์„œ๋“œ ๊ฒฐ๊ณผ๊ฐ€ ์บ์‹œ๊ฐ€ ๋˜์—ˆ์Œ์—๋„ ๋ฉ”์„œ๋“œ๋ฅผ ํ•ญ์ƒ ์‹คํ–‰์‹œํ‚ค๊ณ  ์‹ถ๋‹ค๋ฉด, @CachePut์„ ์‚ฌ์šฉํ•˜์ž
@CachePut(cacheNames = ["exampleStore"], key = "#cacheData.value", condition = "#cacheData.value.length() > 5")
fun updateCacheData(cacheData:CacheData){
  return cacheData
}

 

@CacheConfig, @Caching

  • @CacheConfig : ํด๋ž˜์Šค ๋‹จ์œ„๋กœ ์บ์‹œ์„ค์ •์„ ํ•  ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค. (ํด๋ž˜์Šค ๋‚ด์˜ ๋ชจ๋“  ๋ฉ”์„œ๋“œ์— ์„ค์ •์ด ์ ์šฉ๋œ๋‹ค.)

 

 

  • @Caching : ์—ฌ๋Ÿฌ ๊ฐœ์˜ ์บ์‹ฑ ๋™์ž‘(@Cacheable, @CacheEvict, @CachePut)์„ ํ•˜๋‚˜๋กœ ๋ฌถ์„ ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค.
@CacheConfig(cacheNames= "books", cacheManager="redisCacheManager")
public class CustomerDataService {

    // ๋ฉ”์„œ๋“œ์— ๋”ฐ๋กœ ์ ์ง€์•Š์•„๋„, @CacheConfig์— ์ ์€ ๋‚ด์šฉ์ด ํฌํ•จ๋œ๋‹ค.
    @Cacheable // == @Cacheable(cacheName="books", cacheManager="redisCacheManager")
    public String getAddress(Customer customer) {...}
}
@Caching(evict = { 
  @CacheEvict("addresses"), 
  @CacheEvict(value="directory", key="#customer.name") })
public String getAddress(Customer customer) { /* ... */ }

 

 

๐Ÿ’‍โ™‚๏ธ @Cacheable(key="..")๋Š” ์ƒ๋žตํ•ด๋„ ์‚ฌ์šฉ์ด ๊ฐ€๋Šฅํ•˜๊ธด ํ•ฉ๋‹ˆ๋‹ค.

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

// ๊น”-๋”
@Cacheable("books")
public Book findBook(ISBN isbn) { ... }

 

์ด ๊ฒฝ์šฐ ์Šคํ”„๋ง์˜ SimpleKeyGenerator์— ์˜ํ•ด ์•„๋ž˜์™€ ๊ฐ™์ด Cache Key๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.

SimpleKeyGenreator, SimpleKey ์†Œ์Šค์ฝ”๋“œ

๋”๋ณด๊ธฐ

์ƒ๋‹นํžˆ ์‹ฌํ”Œํ•˜๋‹ค. ์ด๊ฒŒ ์ „๋ถ€์ด๋‹ค

SimpleKey๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด ๊ตฌํ˜„๋˜์–ด์žˆ๋‹ค.

class SimpleKey ์˜ ์ƒ์„ฑ์ž์™€ ๋ฉ”์„œ๋“œ
  • ๋ฉ”์„œ๋“œ ํŒŒ๋ผ๋ฉ”ํƒ€๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ : SimpleKey.empty()
  • ๋ฉ”์„œ๋“œ ํŒŒ๋ผ๋ฉ”ํƒ€๊ฐ€ 1๊ฐœ์ธ ๊ฒฝ์šฐ : ํ•ด๋‹น ํŒŒ๋ผ๋ฉ”ํƒ€ ๊ฐ’(๊ทธ๋Œ€๋กœ ๋ฐ•์Œ, SimpleKey๋กœ ๊ฐ์‹ธ์ง€ ์•Š๋Š”๋‹ค)
  • ๋ฉ”์„œ๋“œ ํŒŒ๋ผ๋ฉ”ํƒ€๊ฐ€ ์—ฌ๋Ÿฌ๊ฐœ์ธ ๊ฒฝ์šฐ : SimpleKey(..ํŒŒ๋ผ๋ฉ”ํƒ€๋“ค..) (* ๋ชจ๋“  ํŒŒ๋ผ๋ฉ”ํƒ€์˜ hashcode ๊ฐ’์„ ์ด์šฉํ•ด ํ•˜๋‚˜์˜ ๋ณตํ•ฉํ‚ค๋กœ ๋งŒ๋“ฆ)

์ถ”์ฒœํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ์•„๋‹ˆ์ง€๋งŒ ์ด๋Š” KeyGenerator ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ง์ ‘ ๊ตฌํ˜„ํ•˜๊ณ  ๋นˆ์œผ๋กœ ๋“ฑ๋กํ•ด์„œ ์ด๋ฅผ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋‹ค.

KeyGenerator ๊ตฌํ˜„ ์ฝ”๋“œ

๋”๋ณด๊ธฐ
  @Bean
  public KeyGenerator keyGenerator() {
    return new CustomKeyGenerator();
  }
public class CustomKeyGenerator implements KeyGenerator {

  @Override
  public Object generate(Object target, Method method, Object... params) {
    StringBuilder keyBuilder = new StringBuilder();
    keyBuilder.append(method.getName());
    keyBuilder.append(SimpleKeyGenerator.generateKey(params));
    return keyBuilder.toString();
  }
}


* ์Šคํ”„๋ง์—์„œ ์บ์‹œ ํ‚ค๋ฅผ ๋น„๊ตํ•  ๋•Œ, hashcode()๋งŒ ์‚ฌ์šฉํ•˜๊ณ  equals()๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
์ž๋ฐ”์˜ hashcode๋Š” 32๋น„ํŠธ ๋ฉ”๋ชจ๋ฆฌ ์ฃผ์†Œ๊ธฐ๋ฐ˜์œผ๋กœ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค. ์ฆ‰ 64๋น„ํŠธ ์šด์˜์ฒด์ œ ์‚ฌ์šฉ ์‹œ ์•„์ฃผ ๋‚ฎ์€ ํ™•๋ฅ ๋กœ ํ•ด์‹œ์ถฉ๋Œ์ด ๋ฐœ์ƒ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ํ•ด๋‹น ์ด์Šˆ(SPR-10237) ๋•Œ๋ฌธ์— ์Šคํ”„๋ง 4.0์—์„œ๋Š” DefaultKeyGenerator ๋ฅผ ์ œ๊ฑฐํ•˜๊ณ  SimpleKeyGenerator ๋กœ ๋ณ€๊ฒฝํ•˜์˜€์Šต๋‹ˆ๋‹ค.

SimpleKey(...)์— ํ• ๋‹น๋œ ํŒŒ๋ผ๋ฉ”ํƒ€๊ฐ€ ์—ฌ๋Ÿฌ๊ฐœ์ธ ๊ฒฝ์šฐ, SimpleKey.readObject ๋ฅผ ์‚ฌ์šฉํ•ด์„œ hashcode๋ฅผ ๋ชจ๋“  ํŒŒ๋ผ๋ฉ”ํƒ€์˜ ๋ณตํ•ฉํ‚ค๋กœ ์žฌ์ •์˜ํ•˜๋„๋ก ๊ตฌํ˜„๋˜์–ด์žˆ๋‹ค.

 

 

๐Ÿ’‍โ™‚๏ธ ์–ด๋…ธํ…Œ์ด์…˜์„ ์ปค์Šคํ…€ํ•ด์„œ ์‚ฌ์šฉํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด๋Š” ์›๋ž˜ ์Šคํ”„๋ง์—์„œ ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋Šฅ์ด๊ธด ํ•ฉ๋‹ˆ๋‹ค.

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Cacheable(cacheNames="books", key="#isbn")
public @interface SlowService {
}
@SlowService // == @Cacheable(cacheNames="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

 

 


๐Ÿ“Œ CacheManager ๊ฐ€ ์—ฌ๋Ÿฌ๊ฐœ ์ผ ๋•Œ (์บ์‹œ ์ €์žฅ์†Œ๋ฅผ ์—ฌ๋Ÿฌ ๊ฐœ๋ฅผ ์“ธ ๋•Œ)

@EnableCaching ์œผ๋กœ ์Šคํ”„๋ง ์บ์‹œ๋ฅผ ํ™œ์„ฑํ™” ์‹œํ‚ค๋ฉด, CacheManager ํƒ€์ž…์˜ ๋นˆ์„ ์กฐํšŒํ•˜์—ฌ ๊ธฐ๋ณธ ๋งค๋‹ˆ์ €๋กœ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค.
์ด ๋•Œ ๋“ฑ๋ก๋œ CacheManager ๋นˆ ํƒ€์ž…์ด ์—ฌ๋Ÿฌ ๊ฐœ๋ผ๋ฉด ์บ์‹œ ํ™œ์„ฑํ™” ๋„์ค‘ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ( NoUniqueBeanDefinitionException )

@Configuration
@EnableCaching
public class CacheConfig extends CachingConfigurerSupport {

    @Override
    @Bean // ex) EHCacheManager
    public CacheManager cacheManager() { ... CacheManager A }

    @Bean // ex) RedisCacheManager
    public CacheManager bCacheManager() { ... CacheManager B }
}

 

 

ํ•ด๊ฒฐ์ฑ…1. @Primary

์ถ”์ฒœํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ์•„๋‹ˆ์ง€๋งŒ, ๋ญ ์Šคํ”„๋ง ์ปจํ…Œ์ด๋„ˆ์˜ @Primary๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํ•ด๊ฒฐ๋˜๊ธด ํ•ฉ๋‹ˆ๋‹ค.

@Configuration
@EnableCaching
public class MultipleCacheManagerConfig {

    @Bean
    @Primary
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager("customers", "orders");
        cacheManager.setCaffeine(Caffeine.newBuilder()
          .initialCapacity(200)
          .maximumSize(500)
          .weakKeys()
          .recordStats());
        return cacheManager;
    }

    @Bean
    public CacheManager alternateCacheManager() {
        return new ConcurrentMapCacheManager("customerOrders", "orderprice");
    }
}

์ด์ œ ๊ธฐ๋ณธ ์บ์‹œ๋งค๋‹ˆ์ €๋Š” @Primary ๊ฐ€ ์‚ฌ์šฉ๋˜๊ณ , ํ•„์š”ํ•  ๋•Œ ์•„๋ž˜์™€ ๊ฐ™์ด ์บ์‹œ๋งค๋‹ˆ์ € ๋นˆ ์ด๋ฆ„์„ ๋ช…์‹œํ•ด์ฃผ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

@Cacheable(cacheNames = "customers") // @Primary CacheManager ์‚ฌ์šฉ๋จ
public Customer getCustomerDetail(Integer customerId) {
    return customerDetailRepository.getCustomerDetail(customerId);
}

// ๋‹ค๋ฅธ ์บ์‹œ๋งค๋‹ˆ์ € ์‚ฌ์šฉ๋จ
@Cacheable(cacheNames = "customerOrders", cacheManager = "alternateCacheManager")
public List<Order> getCustomerOrders(Integer customerId) {
    return customerDetailRepository.getCustomerOrders(customerId);
}

 

 

ํ•ด๊ฒฐ์ฑ…2(์ถ”์ฒœ). CachingConfigurerSupport 

์Šคํ”„๋ง @Configuration์— ํ•ด๋‹น Configurer ๊ฐ์ฒด๋ฅผ ์ƒ์†๋ฐ›์•„์„œ ๊ตฌํ˜„ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. ์ด๋Š” ํ•ด๊ฒฐ์ฑ… 1๊ณผ ๋™์ผํ•˜๊ฒŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.
@Primary ๊ฐ€ ์—†๋Š”๋ฐ๋„ CacheManager ์—ฌ๋Ÿฌ ๊ฐœ ๋“ฑ๋ก์‹œ ์—๋Ÿฌ๊ฐ€ ๋œจ์ง€ ์•Š๋Š” ์ด์œ  -> CacheResolver๊ฐ€ ์ƒ์„ฑ๋˜์—ˆ๊ธฐ ๋•Œ๋ฌธ

๋”๋ณด๊ธฐ

์ด๋Š” @EnableCaching์„ ์‚ฌ์šฉํ–ˆ์„ ๋•Œ, ์Šคํ”„๋ง ์ปจํ…Œ์ด๋„ˆ๋ฅผ ๋””๋ฒ„๊น…ํ•ด๋ณด๋ฉด ์‰ฝ๊ฒŒ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
์Šคํ”„๋ง CacheManager๋ฅผ ๋“ฑ๋กํ•  ๋•Œ, ๋งŒ์•ฝ CacheResolver๊ฐ€ ์กด์žฌํ•œ๋‹ค๋ฉด ๊ธฐ๋ณธ CacheManager๋ฅผ ์กฐํšŒ-๋“ฑ๋กํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

getCacheResolver ๊ฐ€ null์ด ์•„๋‹ˆ๋ผ๋ฉด ์บ์‹œ๋งค๋‹ˆ์ €๋ฅผ ์•„์˜ˆ ์กฐํšŒํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ์—๋Ÿฌ๊ฐ€ ์•ˆํ„ฐ์ ธ์š”

 

CacheResolver ๋Š” ์กฐ๊ฑด์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ์บ์‹œ๋งค๋‹ˆ์ €๋ฅผ ๋“ฑ๋กํ•˜๊ณ  ์‹ถ์„ ๋–„, ์‚ฌ์šฉ์ž๊ฐ€ ํ•ด๋‹น ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ปค์Šคํ…€ํ•ด์„œ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
CachingConfigurerSupport ๋ฅผ ์ƒ์†๋ฐ›์œผ๋ฉด, @Configuration ๊ฐ์ฒด๊ฐ€ ์Šคํ”„๋ง ๋นˆ์œผ๋กœ ๋“ฑ๋ก๋  ๋•Œ Configurer๊ฐ€ ๋™์ž‘ํ•˜๋ฉด์„œ CacheResolver๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

์กฐ๊ธˆ ๋” ์ •ํ™•ํžˆ๋Š” ์•„๋ž˜์™€ ๊ฐ™์€ ๊ณผ์ •์„ ๊ฑฐ์ณ์„œ ์Šคํ”„๋ง ์ธํ„ฐ์…‰ํ„ฐ๋กœ CacheResolver๊ฐ€ ์ƒ์„ฑ๋˜๋Š”๋ฐ, ๊ทธ๋ƒฅ ๋„˜์–ด๊ฐ‘์‹œ๋‹ค ใ…‹ใ…‹

  1. ProxyCachingConfiguration ์„ค์ • ํด๋ž˜์Šค๊ฐ€ ์‹คํ–‰๋˜๋Š” ๊ณผ์ •์ค‘์— ์ƒ์œ„ ํด๋ž˜์Šค์ธ AbstractCachingConfiguration ์— ์žˆ๋Š” setConfigurers ๋ฉ”์†Œ๋“œ๊ฐ€ ์‹คํ–‰์ด ๋œ๋‹ค.
  2. setConfigurers ๋ฉ”์†Œ๋“œ์—์„œ useCachingConfigurer ๋ฉ”์†Œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜๋Š”๋ฐ, ์ด ๋•Œ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ฐ›์€ CachingConfigurer ๊ฐ์ฒด ์—์„œ cacheManager ๋ฅผ ๊ฐ€์ ธ์™€์„œ this.cacheManager ์— ์ฃผ์ž…ํ•œ๋‹ค.
  3. ProxyCachingConfiguration ์„ค์ • ํด๋ž˜์Šค์— ์žˆ๋Š” BeanFactoryCacheOperationSourceAdvisor Bean์ด ๋“ฑ๋ก๋˜๋Š” ๊ณผ์ •์—์„œ CacheInterceptor Bean์ด ์ถ”๊ฐ€๋กœ ๋“ฑ๋ก๋œ๋‹ค.
  4. CacheInterceptor Bean์ด ๋“ฑ๋ก๋˜๋Š” ๊ณผ์ •์—์„œ cacheResolver๊ฐ€ ๋“ฑ๋ก์ด ๋œ๋‹ค.
  5. cacheResolver๊ฐ€ ๋“ฑ๋ก์ด ๋˜์—ˆ๊ธฐ ๋•Œ๋ฌธ์— getCacheResolver ๋ฉ”์†Œ๋“œ๊ฐ€ null์ด ์•„๋‹ˆ๋ฏ€๋กœ Bean์„ ์ฐพ์ง€ ์•Š๊ฒŒ ๋˜๊ณ  ์—๋Ÿฌ๊ฐ€ ๋‚˜์ง€ ์•Š๊ฒŒ ๋˜๋Š” ๊ฒƒ์ด๋‹ค. 

๊ธฐ๋ณธ์œผ๋กœ ์‚ฌ์šฉํ•  ๊ฐ์ฒด๋“ค์„ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@Configuration
@EnableCaching
public class MultipleCacheManagerConfig extends CachingConfigurerSupport {

    // CachingConfigurerSupport.cacheManager()๋ฅผ ์ƒ์†ํ–ˆ์Œ ใ…‹ @Primary ์ƒ๋žต๊ฐ€๋Šฅ
    @Override
    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager("customers", "orders");
        cacheManager.setCaffeine(Caffeine.newBuilder()
          .initialCapacity(200)
          .maximumSize(500)
          .weakKeys()
          .recordStats());
        return cacheManager;
    }

    @Bean
    public CacheManager alternateCacheManager() {
        return new ConcurrentMapCacheManager("customerOrders", "orderprice");
    }
}

 

 

 

ํ•ด๊ฒฐ์ฑ…3. CompositeCacheManager ์‚ฌ์šฉํ•˜๊ธฐ

์Šคํ”„๋ง CacheManager ๊ตฌํ˜„์ฒด์ค‘์—๋Š”, ์•„์˜ˆ ์บ์‹œ๋งค๋‹ˆ์ €๋ฅผ ์—ฌ๋Ÿฌ ๊ฐœ ๋“ฑ๋กํ•  ์ˆ˜ ์žˆ๋Š” ๊ตฌํ˜„์ฒด๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

@Bean
public CacheManager cacheManager(net.sf.ehcache.CacheManager cm, javax.cache.CacheManager jcm) {
  CompositeCacheManager cacheManager = new CompositeCacheManager();
  
  List<CacheManager> managers = new ArrayList<CacheManager>();
  managers.add(new JCacheCacheManager(jcm));
  managers.add(new EhCacheCacheManager(cm))
  managers.add(new RedisCacheManager(redisTemplate()));
  
  cacheManager.setCacheManagers(managers); // ๊ฐœ๋ณ„ ์บ์‹œ ๋งค๋‹ˆ์ € ์ถ”๊ฐ€
  return cacheManager;
}

 

์ด๋Š” ๊ถŒ์žฅํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ์•„๋‹™๋‹ˆ๋‹ค๋งŒ, ์•Œ์•„๋‘๋ฉด ํ†ตํ•ฉํ…Œ์ŠคํŠธ ํ• ๋•Œ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.
์˜ˆ๋ฅผ ๋“ค์–ด ๋ฉ”์„œ๋“œ์—์„œ ์š”์ฒญํ•œ ์บ์‹œ๋งค๋‹ˆ์ €๊ฐ€ ๋“ฑ๋ก๋˜์ง€ ์•Š์•˜๋‹ค๋ฉด NoOpCacheManager๊ฐ€ ์‹คํ–‰๋˜๋„๋ก ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@TestConfiguration
public class TestConfig {
  @Autowired
  private CacheManager jdkCache; // ์˜ˆ์‹œ

  @Autowired
  private CacheManager guavaCache; // ์˜ˆ์‹œ

  @Bean
  @Primary
  public CacheManager cacheManager() {
    CompositeCacheManager manager = new CompositeCacheManager(jdkCache, guavaCache);

    // CompositeCacheManager์— ํฌํ•จ๋œ CacheManager ์™ธ
    // ๋ชจ๋“  ์บ์‹œ ์š”์ฒญ์„ NoOpCache๋กœ ๋ณด๋ƒ„ (์—ฌ๊ธฐ์—์„  jdkCache, guavaCache ์™ธ์˜ ๋ชจ๋“  ์บ์‹œ)
    // NoOpCache๋Š” ์บ์‹œ๊ฐ€ ์—†๋Š” ๊ฒƒ๊ณผ ๋™์ผํ•˜๊ฒŒ ๋™์ž‘
    manager.setFallbackToNoOpCache(true);

    return manager;
  }
}

 

 

ํ•ด๊ฒฐ์ฑ…4(์ถ”์ฒœ). CacheResolver ์‚ฌ์šฉํ•˜๊ธฐ

์Šคํ”„๋ง 4.1 ๋ถ€ํ„ฐ๋Š” @Cacheable์— ์•„๋ฌด๋Ÿฐ ์„ค์ • ๊ฐ’์„ ์ ์ง€ ์•Š๋”๋ผ๋„ ์บ์‹œ๋งค๋‹ˆ์ €๋ฅผ ์•Œ์•„์„œ ์ฐพ์•„์ค๋‹ˆ๋‹ค.
์ด๋Š” cacheManager=".." ๊ฐ’์ด ์—†๋‹ค๋ฉด ๊ธฐ๋ณธ์œผ๋กœ ๋“ฑ๋ก๋œ SimpleCacheResolver๊ฐ€ ๋™์ž‘ํ•˜์—ฌ CacheManager๋ฅผ ์ฐพ์•„์ฃผ๊ธฐ ๋•Œ๋ฌธ์ธ๋ฐ, ์šฐ๋ฆฌ๋Š” ์ด๋ฅผ ์ปค์Šคํ…€ํ•ด์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

์˜ˆ๋ฅผ ๋“ค์–ด ๋ฉ”์„œ๋“œ ์ด๋ฆ„์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ์บ์‹œ๊ฐ€ ๋™์ž‘ํ•˜๋„๋ก ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด, ์•„๋ž˜์™€ ๊ฐ™์ด ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

public class MultipleCacheResolver implements CacheResolver {
    
    private final CacheManager simpleCacheManager;
    private final CacheManager caffeineCacheManager;    
    private static final String ORDER_CACHE = "orders";    
    private static final String ORDER_PRICE_CACHE = "orderprice";
    
    public MultipleCacheResolver(CacheManager simpleCacheManager,CacheManager caffeineCacheManager) {
        this.simpleCacheManager = simpleCacheManager;
        this.caffeineCacheManager=caffeineCacheManager;
        
    }

    // resolveCaches ๋ฉ”์„œ๋“œ๋ฅผ ์žฌ์ •์˜ํ•˜๋ฉด ๋œ๋‹ค.
    @Override
    public Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context) {
        Collection<Cache> caches = new ArrayList<Cache>();
        
        // ํ•ด๋‹น ์ฝ”๋“œ๋Š” context ํŒŒ๋ผ๋ฉ”ํƒ€๋ฅผ ์ด์šฉํ•ด ํ•ด๋‹น ๋ฉ”์„œ๋“œ ์ด๋ฆ„์„ ๋ฐ›์•„์™€์„œ ๋น„๊ตํ•œ๋‹ค.
        if ("getOrderDetail".equals(context.getMethod().getName())) {
            caches.add(caffeineCacheManager.getCache(ORDER_CACHE));
        } else {
            caches.add(simpleCacheManager.getCache(ORDER_PRICE_CACHE));
        }
        return caches;
    }
}

 

๊ทธ๋ฆฌ๊ณ  ์ด๋ฅผ ํ•ด๊ฒฐ์ฑ…3๊ณผ ๋™์ผํ•˜๊ฒŒ ๊ธฐ๋ณธ CacheResolver๋กœ ๋“ฑ๋กํ•ด์ฃผ๋ฉด ๋ฉ๋‹ˆ๋‹ค.
CacheResolver๊ฐ€ ๋“ฑ๋ก ๋œ ๊ฒฝ์šฐ, @Cacheable์— ์„ค์ • ๊ฐ’์ด ์—†์„ ๋•Œ CacheResolver๋ฅผ ๊ฑฐ์ณ์„œ ์บ์‹œ ๋งค๋‹ˆ์ €๋ฅผ ์ฐพ๊ฒŒ๋ฉ๋‹ˆ๋‹ค.

@Configuration
@EnableCaching
public class MultipleCacheManagerConfig extends CachingConfigurerSupport {

    @Override
    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager("customers", "orders");
        cacheManager.setCaffeine(Caffeine.newBuilder()
          .initialCapacity(200)
          .maximumSize(500)
          .weakKeys()
          .recordStats());
        return cacheManager;
    }

    @Bean
    public CacheManager alternateCacheManager() {
        return new ConcurrentMapCacheManager("customerOrders", "orderprice");
    }

    // ์ด์ œ CacheManager๋ฅผ ๋ช…์‹œํ•˜์ง€ ์•Š์œผ๋ฉด ํ•ด๋‹น CacheResolver๊ฐ€ ๋™์ž‘ํ•˜์—ฌ ์ฐพ์•„์ค€๋‹ค.
    @Override
    @Bean
    public CacheResolver cacheResolver() {
        return new MultipleCacheResolver(alternateCacheManager(), cacheManager());
    }
}

 

@Cacheable( .. ์„ค์ • ..) ์—์„œ

1. ์„ค์ •์„ ๋‹ค ์ƒ๋žตํ–ˆ๋Š”๋ฐ ๋“ฑ๋ก๋œ CacheResovler๊ฐ€ ์—†์œผ๋ฉด ๊ธฐ๋ณธ CacheManager๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

2. ์„ค์ •์„ ๋‹ค ์ƒ๋žตํ–ˆ๋Š”๋ฐ ๋“ฑ๋ก๋œ CacheResolver๊ฐ€ ์žˆ๋‹ค๋ฉด, CacheResolver๊ฐ€ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค. 

3. ํŠน์ • CacheManager๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด, @Cacheable(cacheManager="...")๋กœ ๋ช…์‹œํ•ด์ฃผ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

4. ํŠน์ • CacheResolver๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด, @Cacheable(cacheResolver="...")๋กœ ๋ช…์‹œํ•ด์ฃผ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

@Cacheable(cacheNames = "orders", cacheResolver = "cacheResolver")
public Order getOrderDetail(Integer orderId) {
    return orderDetailRepository.getOrderDetail(orderId);
}

@Cacheable(cacheNames = "orderprice", cacheResolver = "cacheResolver")
public double getOrderPrice(Integer orderId) {
    return orderDetailRepository.getOrderPrice(orderId);
}

 

 

์—ฌ๊ธฐ์„œ ๋ฌธ์ œ๋Š” @Cacheable( cacheManager="..", cacheResovler="..") ์ด๋ ‡๊ฒŒ ๋‘˜ ๋‹ค ์ ์—ˆ์„ ๋•Œ ๋ฌด์—‡์ด ๋™์ž‘ํ• ์ง€ ๋ชจ๋ฅธ๋‹ค๋Š”๊ฑด๋ฐ,

์ด ๊ฒฝ์šฐ์—๋Š” ์บ์‹œ ์ดˆ๊ธฐํ™”(@EnableCaching) ์‹œ์ ์— ์Šคํ”„๋ง ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์˜ˆ์™ธ๋ฅผ ๋„์›Œ์ค๋‹ˆ๋‹ค. ๋‘˜ ์ค‘ ํ•˜๋‚˜๋งŒ ์ ์–ด์•ผํ•ฉ๋‹ˆ๋‹ค.

์นœ์ ˆํ•˜๊ฒŒ ํ•ด๋‹น ์–ด๋…ธํ…Œ์ด์…˜์ด ์‚ฌ์šฉ๋œ ์œ„์น˜์™€, ์ด๋ ‡๊ฒŒ ์„ค์ •์„ ๋‘˜ ๋‹ค ์ ์œผ๋ฉด ์•ˆ๋œ๋‹ค๊ณ  ์˜ˆ์™ธ ๋ฉ”์‹œ์ง€๋กœ ์•Œ๋ ค์ค๋‹ˆ๋‹ค.

์Šคํ”„๋ง ๋นˆ ์ƒ์„ฑ์‹œ์ ์— ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉฐ, IllegalStateException ์—์„œ ํ•ด๋‹น ์–ด๋…ธํ…Œ์ด์…˜์ด ์“ฐ์ธ ๋ฉ”์„œ๋“œ ์œ„์น˜๊นŒ์ง€ ์•Œ๋ ค์ค€๋‹ค ใ…Ž

 

 

 


๐Ÿ“Œ  Redis

๋ ˆ๋””์Šค๋Š” Spring-Data-Redis ํ”„๋กœ์ ํŠธ๊ฐ€ ๋ณ„๋„๋กœ ์กด์žฌํ•ด์„œ, ์Šคํ”„๋ง์—์„œ ์„ค์ •์„ ์‰ฝ๊ฒŒ ํ•  ์ˆ˜ ์žˆ๋‹ค.

implementation 'org.springframework.boot:spring-boot-starter-data-redis'

// ์Šคํ”„๋ง5 webflux ๊ธฐ๋ฐ˜์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ ์ถ”๊ฐ€ (์•„์ง spring-data-redis์— ํ†ตํ•ฉ๋˜์ง€ ์•Š์Œ)
implementation 'org.springframework.boot:spring-boot-starter-data-redis-reactive'

 

Spring-Data-Redis์—์„œ๋Š”  Redis Client๋กœ Lettuce์™€ Jedis๋ฅผ ์ œ๊ณตํ•ด์ค€๋‹ค.
2022๋…„ ๊ธฐ์ค€ ์„ค์ •์˜ ํŽธ์˜์„ฑ์ด๋‚˜, ์„ฑ๋Šฅ, ์—…๋ฐ์ดํŠธ ๋ชจ๋“  ๊ฒƒ์ด Lettuce๊ฐ€ ๋” ์ข‹๊ธฐ ๋•Œ๋ฌธ์—, ์ด๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•œ๋‹ค (๊ด€๋ จ ๊ธ€)

 

RedisTemplate

๋ ˆ๋””์Šค๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„  RedisTemplate ๊ฐ์ฒด๋ฅผ ๋นˆ์œผ๋กœ ๋“ฑ๋กํ•ด์ฃผ์–ด์•ผํ•œ๋‹ค.
๊ฐ์ข… ์„ค์ •๊ฐ’๋“ค์€ RedisTemplate์— ์ง์ ‘ ์ฝ”๋“œ๋กœ ๋„ฃ์–ด๋„ ๋˜์ง€๋งŒ, ์•„๋ž˜์™€ ๊ฐ™์€ ์„ค์ •๊ฐ’์œผ๋กœ ๋”ฐ๋กœ ๊ด€๋ฆฌํ•˜๋Š”๊ฒŒ ์ข‹๋‹ค.
Spring Boot Data ๊ด€๋ จ ํ”„๋กœํผํ‹ฐ ์ •๋ฆฌ๊ธ€

// application.yml
spring:
  redis:
    host: localhost
    port: 6379

 

spring.redis.database 0 ์ปค๋„ฅ์…˜ ํŒฉํ† ๋ฆฌ์— ์‚ฌ์šฉ๋˜๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ธ๋ฑ์Šค
spring.redis.host localhost ๋ ˆ๋””์Šค ์„œ๋ฒ„ ํ˜ธ์ŠคํŠธ
spring.redis.password   ๋ ˆ๋””์Šค ์„œ๋ฒ„ ๋กœ๊ทธ์ธ ํŒจ์Šค์›Œ๋“œ
spring.redis.pool.max-active 8 pool์— ํ• ๋‹น๋  ์ˆ˜ ์žˆ๋Š” ์ปค๋„ฅ์…˜ ์ตœ๋Œ€์ˆ˜ (์Œ์ˆ˜๋กœ ํ•˜๋ฉด ๋ฌด์ œํ•œ)
spring.redis.pool.max-idle 8 pool์˜ "idle" ์ปค๋„ฅ์…˜ ์ตœ๋Œ€์ˆ˜ (์Œ์ˆ˜๋กœ ํ•˜๋ฉด ๋ฌด์ œํ•œ)
spring.redis.pool.max-wait -1 pool์ด ๋ฐ”๋‹ฅ๋‚ฌ์„ ๋•Œ ์˜ˆ์™ธ๋ฐœ์ƒ ์ „์— ์ปค๋„ฅ์…˜ ํ• ๋‹น ์ฐจ๋‹จ์˜ ์ตœ๋Œ€ ์‹œ๊ฐ„ (๋‹จ์œ„: ๋ฐ€๋ฆฌ์„ธ์ปจ๋“œ, ์Œ์ˆ˜๋Š” ๋ฌด์ œํ•œ ์ฐจ๋‹จ)
spring.redis.pool.min-idle 0 ํ’€์—์„œ ๊ด€๋ฆฌํ•˜๋Š” idle ์ปค๋„ฅ์…˜์˜ ์ตœ์†Œ ์ˆ˜ ๋Œ€์ƒ (์–‘์ˆ˜์ผ ๋•Œ๋งŒ ์œ ํšจ)
spring.redis.port 6379 ๋ ˆ๋””์Šค ์„œ๋ฒ„ ํฌํŠธ
spring.redis.sentinel.master   ๋ ˆ๋””์Šค ์„œ๋ฒ„ ์ด๋ฆ„
spring.redis.sentinel.nodes   ํ˜ธ์ŠคํŠธ:ํฌํŠธ ์Œ ๋ชฉ๋ก (์ฝค๋งˆ๋กœ ๊ตฌ๋ถ„)
spring.redis.timeout 0 ์ปค๋„ฅ์…˜ ํƒ€์ž„์•„์›ƒ (๋‹จ์œ„: ๋ฐ€๋ฆฌ์„ธ์ปจ๋“œ)

 

RedisProperties ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•˜๋Š”๋ฐ, ์ด๋Š” ๋ณ„๊ฑด ์•„๋‹ˆ๊ณ  application.yml ์„ค์ • ๊ฐ’์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ํŽธ์˜ ๊ฐ์ฒด์ด๋‹ค.

// org.springframework.boot.autoconfigure.data.redis.RedisProperties
@ConfigurationProperties(prefix="spring.redis")
public class RedisProperties {...}

// ๋ฌผ๋ก  RedisProperties ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์•„๋„ ์„ค์ •ํŒŒ์ผ์€ ์ฝ์„ ์ˆ˜ ์žˆ๋‹ค.
@Value("${spring.redis.host}")
private String host;

@Value("${spring.redis.port}")
private int port;

 

์ด ์„ค์ • ๊ฐ’์„ ์ด์šฉํ•˜์—ฌ RedisTemplate ๋นˆ์„ ๊ตฌ์„ฑํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

@Configuration
public class RedisConfig {
    private final RedisProperties redisProperties;

    @Bean // Redis์™€ Connection์„ ์ƒ์„ฑํ•˜๋Š” ๊ฐ์ฒด๋ฅผ ์ •์˜ํ•œ๋‹ค. ์—ฌ๊ธฐ์—์„œ๋Š” Lettuce Redis Client ์‚ฌ์šฉ
    public RedisConnectionFactory redisConnectionFactory(){
        return new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort());
    }

    
    @Bean // ์Šคํ”„๋ง์—์„œ ์ œ๊ณตํ•˜๋Š” RedisTemplate์„ ์ •์˜ํ•œ๋‹ค. ์œ„์—์„œ ์ •์˜ํ•œ ConnectionFactory๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค
    public RedisTemplate<String, Object> redisTemplate(){
        RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        //redisTemplate.setKeySerializer(new StringRedisSerializer());    //key serializer
        //redisTemplate.setValueSerializer(new StringRedisSerializer());  //value serializer
        //redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        //redisTemplate.setHashValueSerializer(new StringRedisSerializer());
        
        // ์œ„์˜ ์„ค์ •๋“ค์„ ํ•œ๋ฒˆ์— ์„ค์ •ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด DefaultSerializer ๋ฅผ ๋“ฑ๋กํ•ด์ฃผ๋ฉด ๋œ๋‹ค
        //redisTemplate.setDefaultSerializer(new StringRedisSerializer());
        return redisTemplate;
    }
}

 

Redis Serializer ์ปค์Šคํ…€

์ฐธ๊ณ ๋กœ Redis๋Š” Serializer๋ฅผ ๋ณ„๋„๋กœ ๋“ฑ๋กํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ, key-value๋ฅผ ๋‘˜ ๋‹ค Byte[] ์œผ๋กœ ์ €์žฅํ•ด๋ฒ„๋ฆฐ๋‹ค.

RedisTemplate์˜ ๊ธฐ๋ณธ๊ฐ’์€ JdkSerializationRedisSerializer ์ด๋‹ค.
- ์ด๋Š” ์ž๋ฐ”์˜ Serializable ๊ฐ์ฒด๋ฅผ ๊ตฌํ˜„ํ•ด์•ผ๋งŒ ์ง๋ ฌํ™”๊ฐ€ ๊ฐ€๋Šฅํ•˜๋‹ค.

- ๊ธฐ๋ณธ๊ฐ’์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ ์ž๋ฐ”๊ฐ€ ์•„๋‹Œ ๋‹ค๋ฅธ ์–ธ์–ด์—์„œ๋Š” Redis ์ €์žฅ์†Œ์˜ ๊ฐ’์„ ์ฝ์„ ์ˆ˜ ์—†๋‹ค
- ์“ธ๋ชจ ์—†๋Š” ์ž๋ฐ”์˜ ๊ธฐ๋ณธ Object class์˜ ์ •๋ณด๊นŒ์ง€ Redis์— ์ €์žฅํ•ด์„œ ๋น„ํšจ์œจ์ ์ด๋‹ค.

๊ทธ๋ž˜์„œ ๋ณดํ†ต ์•„๋ž˜ 3๊ฐ€์ง€ Serializer๋ฅผ ๋งŽ์ด ์‚ฌ์šฉํ•œ๋‹ค.

1. GenericJackson2JsonRedisSerializer

๋ณ„๋„์˜ ํด๋ž˜์Šค ํƒ€์ž…์„ ์ง€์ •ํ•ด์ฃผ์ง€ ์•Š์•„๋„, ๊ฐ์ฒด๋ฅผ Json ์œผ๋กœ ์ง๋ ฌํ™”ํ•ด์ค€๋‹ค.
๋‹ค๋งŒ @class ์— Class ์ •์˜ ๊ฒฝ๋กœ๋ฅผ ๋ฐ•์•„๋ฒ„๋ ค์„œ, ๋ฐ์ดํ„ฐ๋ฅผ ๊บผ๋‚ด์˜ฌ ๋•Œ ๊ฐ™์€ ๋””๋ ‰ํ† ๋ฆฌ์— ํ•ด๋‹น DTO๊ฐ€ ๋ฐ˜๋“œ์‹œ ์กด์žฌํ•ด์•ผํ•˜๋Š” ๋‹จ์ ์ด ์žˆ๋‹ค.

com.ssg.api.item.domain.Item ๊ฒฝ๋กœ๋ฅผ ๋ฐ•์•„๋ฒ„๋ฆฐ๋‹ค... ์ด๋Ÿฐ..

 

2. Jackson2JsonRedisSerializer

ํŠน์ • ํด๋ž˜์Šค ํƒ€์ž…์„ ๋ช…์‹œํ•ด์ค˜์•ผํ•œ๋‹ค. ๊ทธ ๋Œ€์‹  @class ์ •๋ณด๋ฅผ ๋ฐ์ดํ„ฐ์— ํฌํ•จํ•˜์ง€ ์•Š๋Š”๋‹ค.

๋‹ค๋งŒ ํด๋ž˜์Šค ํƒ€์ž…๋ณ„๋กœ Serializer๋ฅผ ๋ณ„๋„๋กœ ์ƒ์„ฑํ•ด์ค˜์•ผํ•˜๋Š”๋ฐ, cacheDto ๊ฐ€์ง€๋Š” API๋“ค์ด ๊ฐ์ž ๊ณ ์œ ํ•œ RedisTemplate์„ ๋”ฐ๋กœ ๋“ค๊ณ ์žˆ์–ด์•ผํ•œ๋‹ค.. ใ… 

redisTemplate.setHashValueSerializer(new Jackson2JsonRedisSerializer(value.getClass()));

 

3. StringRedisSerializer + ObjectMapper
SpringBoot-Redis์— ํฌํ•จ๋œ StringRedisSerializer๋Š” ๊ทธ๋ƒฅ ๋ชจ๋“  key-value๋ฅผ ๋ฌธ์ž์—ด๋กœ ์ €์žฅํ•œ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ๊ฐ’(value)์ค‘์— ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜์ด ๋ถˆ๊ฐ€๋Šฅํ•œ ๊ฐ์ฒด๋ฅผ ์ €์žฅํ•  ๋•, ์•„๋ž˜์™€ ๊ฐ™์ด ObjectMapper๋ฅผ ์ปค์Šคํ…€ํ•˜์—ฌ ์‚ฌ์šฉํ•œ๋‹ค.

@Configuration
class RedisConfig {

  @Bean
  fun redisConnectionFactory(redisProperties: RedisProperties): RedisConnectionFactory {
    return LettuceConnectionFactory(redisProperties.host, redisProperties.port)
  }

  @Bean
  fun redisObjectMapper(): ObjectMapper = ObjectMapper()
    .registerModule(kotlinModule())
    .registerModule(JavaTimeModule())
    .activateDefaultTyping(
      BasicPolymorphicTypeValidator.builder().allowIfBaseType(Any::class.java).build(),
      ObjectMapper.DefaultTyping.EVERYTHING
    )


  @Bean
  fun redisTemplate(redisConnectionFactory: RedisConnectionFactory): RedisTemplate<String, Any?> {
    val redisTemplate = RedisTemplate<String, Any?>()
    redisTemplate.setConnectionFactory(redisConnectionFactory)
    redisTemplate.keySerializer = StringRedisSerializer()
    // GenericJackson2JsonRedisSerializer์— ObjectMapper๋ฅผ ์ปค์Šคํ…€
    redisTemplate.valueSerializer = GenericJackson2JsonRedisSerializer(redisObjectMapper())
    return redisTemplate
  }
}

 

 

Redis ์‚ฌ์šฉํ•˜๊ธฐ 1 - RedisTemplate ์ง์ ‘ ์‚ฌ์šฉ

์ฐธ๊ณ ๋กœ ๋ ˆ๋””์Šค๋Š” ๋‹จ์ˆœ ์ŠคํŠธ๋ง ๋ฟ ์•„๋‹ˆ๋ผ, ์—ฌ๋Ÿฌ๊ฐ€์ง€ ์ž๋ฃŒ๊ตฌ์กฐ๋ฅผ ์ œ๊ณตํ•ด์ฃผ๋Š”๋ฐ ์ด๋Š” redisTemplate์˜ ์•„๋ž˜ ๋ฉ”์„œ๋“œ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

// ๋Œ€์ถฉ ์ด๋ ‡๊ฒŒ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.
object CacheUtil {

  fun setValues(
    redisTemplate: RedisTemplate<String, Any?>,
    prefix: String,
    key: String,
    data: String,
    timeout: Long,
    timeUnit: TimeUnit
  ) {
    redisTemplate.opsForValue().set("$prefix::$key", data, timeout, timeUnit)
  }

  fun getValuesOrNull(redisTemplate: RedisTemplate<String, Any?>, prefix: String, key: String): Any? {
    return redisTemplate.opsForValue()["$prefix::$key"]
  }

  fun deleteValues(redisTemplate: RedisTemplate<String, Any?>, prefix: String, key: String) {
    redisTemplate.delete("$prefix::$key")
  }

  fun getRedisTtl(redisTemplate: RedisTemplate<String, Any?>, prefix: String, key: String): Long? {
    return redisTemplate.getExpire("$prefix::$key")
  }

  fun setRedisTtl(redisTemplate: RedisTemplate<String, Any?>, prefix: String, key: String, timeout: Long) {
    redisTemplate.expire("$prefix::$key", Duration.ofSeconds(timeout))
  }
}

 

 

Redis ์‚ฌ์šฉํ•˜๊ธฐ2 - RedisRepositories (๋น„์ถ”์ฒœ)

๊ถŒ์žฅํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ์•„๋‹ˆ์ง€๋งŒ, Redis๋ฅผ ๋งˆ์น˜ JPA์ฒ˜๋Ÿผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. ์ฐธ๊ณ ๋กœ ๋‚ด๋ถ€์ ์œผ๋กœ RedisTemplate์„ ์‚ฌ์šฉํ•˜๋Š”๊ฑด ๋™์ผํ•˜๋‹ค.

@Configuration
@EnableRedisRepositories // ์ถ”๊ฐ€
public class RedisConfig {
    /*.. ์ดํ•˜ ๋™์ผ .. */
}
@RedisHash(value = "refreshToken")
public class RefreshToken {
    @Id // org.springframework.data.annotation.Id
    private String userId;
    private String refreshToken;

    @TimeToLive
    private long expiredTime;
}
// Spring.Data์˜ CrudRepository๋ฅผ ์ƒ์†๋ฐ›์€ Repository ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ •์˜ํ•œ๋‹ค.
public interface RefreshTokenRedisRepository extends CrudRepository<RefreshToken, String> {
}

// ๋งˆ์น˜ JPA ์ฒ˜๋Ÿผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
@Service
public class AdminUserRedisService {
    private final RefreshTokenRedisRepository refreshTokenRedisRepository;

    @Override
    public RefreshToken save(RefreshToken adminUserToken) {
        return refreshTokenRedisRepository.save(adminUserToken);
    }

    @Override
    public RefreshToken findById(String userId) {
        return refreshTokenRedisRepository.findById(userId).get();
    }
}
@SpringBootTest
class AdminUserRedisServiceTest {

    @Autowired
    private AdminUserRedisService adminUserRedisService;

    @DisplayName("token redis ์ €์žฅ success")
    @Test
    void tokenSave(){
        //given
        RefreshToken token = RefreshToken.builder()
                .userId("test")
                .refreshToken("test_token")
                .expiredTime(60)
                .build();
                
        //when
        RefreshToken refreshToken = adminUserRedisService.save(token);
        
        //then
        RefreshToken findToken = adminUserRedisService.findById(token.getUserId());
        assertEquals(refreshToken.getRefreshToken(), findToken.getRefreshToken());
    }
}

 

 

Redis ์‚ฌ์šฉํ•˜๊ธฐ3 - ์Šคํ”„๋ง ์บ์‹œ ์ถ”์ƒํ™” CacheManager

RedisTemplate์„ ์ƒ์„ฑํ•˜๋Š” ๊ณผ์ •์€ ์œ„์™€ ๋™์ผํ•˜๋‹ค. ๋ณดํ†ต ๊ด€๋ฆฌํ•˜๊ธฐ ํŽธํ•˜๊ฒŒ RedisConfig๋ฅผ ๋ณ„๋„๋กœ ๋ถ„๋ฆฌํ•ด์„œ ์ง€์ •ํ•˜๊ณค ํ•œ๋‹ค.

ํŒ์„ ํ•˜๋‚˜ ์ฃผ์ž๋ฉด, @ConditionalOnProperty ์–ด๋…ธํ…Œ์ด์…˜์„ ์ด์šฉํ•ด yml ์„ค์ •์— redis ์ •๋ณด๊ฐ€ ์žˆ์„๋•Œ๋งŒ ๋“ฑ๋กํ•˜๊ฒŒ ํ•  ์ˆ˜ ์žˆ๋‹ค.
์Šคํ”„๋ง๋ถ€ํŠธ์˜ ์กฐ๊ฑด๋ถ€ Configuration 

@Configuration // spring.cache.type: redis ์ผ ๋•Œ ํ•ด๋‹น ์„ค์ •์„ ๋“ฑ๋กํ•œ๋‹ค, ๋งŒ์•ฝ ์„ค์ •๊ฐ’์ด ์•„์˜ˆ ์—†๋Š” ๊ฒฝ์šฐ ์ƒ์„ฑํ•˜์ง€ ์•Š๋Š”๋‹ค.
@ConditionalOnProperty(name = ["spring.cache.type"], havingValue = "redis", matchIfMissing = false)
class RedisConfig {

  @Bean
  fun redisConnectionFactory(redisProperties: RedisProperties): RedisConnectionFactory {
    return LettuceConnectionFactory(redisProperties.host, redisProperties.port)
  }

  @Bean // ์ฐธ๊ณ ๋กœ ๊ทธ๋ƒฅ String์œผ๋กœ ์ €์žฅํ• ๊ฑฐ๋ฉด, objectMapper๋ฅผ ์ปค์Šคํ…€ํ•  ํ•„์š”๋Š” ์—†๋‹ค ใ…Ž
  fun redisObjectMapper(): ObjectMapper = ObjectMapper()
    .registerModule(kotlinModule())
    .registerModule(JavaTimeModule())
    .activateDefaultTyping(
      BasicPolymorphicTypeValidator.builder().allowIfBaseType(Any::class.java).build(),
      ObjectMapper.DefaultTyping.EVERYTHING
    )


  @Bean
  fun redisTemplate(redisConnectionFactory: RedisConnectionFactory): RedisTemplate<String, Any?> {
    val redisTemplate = RedisTemplate<String, Any?>()
    redisTemplate.setConnectionFactory(redisConnectionFactory)
    redisTemplate.keySerializer = StringRedisSerializer()
    // GenericJackson2JsonRedisSerializer์— ObjectMapper๋ฅผ ์ปค์Šคํ…€
    redisTemplate.valueSerializer = GenericJackson2JsonRedisSerializer(redisObjectMapper())
    return redisTemplate
  }
}

 

 

์ด๋ ‡๊ฒŒ ์ƒ์„ฑํ•œ Redis ๊ฐ์ฒด๋“ค์„ ์ด์šฉํ•˜์—ฌ CacheManager๋ฅผ ๋“ฑ๋กํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

  @Bean  // RedisCacheManager ๋นˆ
  public CacheManager cacheManager(RedisTemplate redisTemplate) { 
      return new RedisCacheManager(redisTemplate); // Redisํ…œํ”Œ๋ฆฟ์˜ ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑ์ž์— ์ „๋‹ฌํ•˜์—ฌ, ์ƒ์„ฑ
}

 

์ฐธ๊ณ ๋กœ CacheManager๋งŒ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ, RedisTemplate๋ฅผ ๊ผญ ๋งŒ๋“ค ํ•„์š”๋Š” ์—†๋‹ค. ์•„๋ž˜์™€ ๊ฐ™์ด ๋นŒ๋”๋กœ RedisTemplate ์˜ ์„ค์ •๊ณผ ๋น„์Šทํ•˜๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค. (RedisCacheConfiguration)

@Configuration
@EnableCaching
class CacheConfig {

    // spring.cache.type: redis ๊ฐ€ ์•„๋‹ˆ๋ผ๋ฉด, ์ƒ์„ฑํ•˜์ง€ ์•Š๋Š”๋‹ค. (์•„์˜ˆ ๊ฐ’์ด ์—†์–ด๋„ ์ƒ์„ฑํ•˜์ง€ ์•Š๋Š”๋‹ค)
    @ConditionalOnProperty(name = ["spring.cache.type"], havingValue = "redis", matchIfMissing = false)
    @Bean
    fun cacheManager(
       redisConnectionFactory: RedisConnectionFactory,
       objectMapper:ObjectMapper
    ): CacheManager {
      
      val redisConfiguration:RedisCacheConfiguration = RedisCacheConfiguration
      // ํ•ด๋‹น ์บ์‹œ๋งค๋‹ˆ์ €๊ฐ€ ์‚ฌ์šฉํ•  ๊ธฐ๋ณธ ์„ค์ •
      .defaultCacheConfig()
        // 1. ์บ์‹œ ์œ ํšจ๊ธฐ๊ฐ„(Time to live) ์„ค์ •
        .entryTtl(Duration.ofSeconds(3600))
        
        // 2. ์บ์‹œ ์ด๋ฆ„์— prefix:: ๋ฅผ ์ž๋™์œผ๋กœ ๋ถ™์—ฌ์คŒ.
        .computePrefixWith(CacheKeyPrefix.simple())
        
        // 3. key ์ง๋ ฌํ™”์— StringRedisSerializer ์‚ฌ์šฉ
        .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer()))
        
        // 4. value ์ง๋ ฌํ™”์— GenericJackson2JsonRedisSerializer(์ปค์Šคํ…€ํ•œ ObjectMapper) ์‚ฌ์šฉ
        .serializeValuesWith(
          RedisSerializationContext.SerializationPair.fromSerializer(
            GenericJackson2JsonRedisSerializer(objectMapper)
          )
        )

      // ์•ž์—์„œ ์ƒ์„ฑํ•œ redisConnectionFactory ์™€ ์—ฌ๊ธฐ์„œ ๋งŒ๋“  redisConfiguration์„ ๋“ฑ๋กํ•จ 
      return RedisCacheManager.RedisCacheManagerBuilder
        .fromConnectionFactory(redisConnectionFactory)
        .cacheDefaults(redisConfiguration)
        .build()
    }
}

 

 

๐Ÿ“Œ Caffeine

Caffeine์€ ์‚ฌ์šฉ๋ฒ•์ด ๋‹จ์ˆœํ•œ ๋กœ์ปฌ ์บ์‹œ(๋ฉ”๋ชจ๋ฆฌ ์บ์‹œ)์˜ ํ•˜๋‚˜๋กœ, ์ž๋ฐ”์˜ ConcurrentMap ๋ณด๋‹ค ์†๋„๊ฐ€ ๋น ๋ฅธ๊ฑธ๋กœ ์œ ๋ช…ํ•˜๋‹ค.

์ฐธ๊ณ ๋กœ EHCache๋„ ์ž๋ฐ”์—์„œ ๋งŽ์ด ์‚ฌ์šฉํ•˜๋Š” ๋กœ์ปฌ ์บ์‹œ์ค‘ ํ•˜๋‚˜์ด๋‹ค. ์„œ๋ฒ„ ๋ถ„์‚ฐ์บ์‹œ, ๋น„๋™๊ธฐ, ๋””์Šคํฌ์ €์žฅ๋“ฑ ๊ธฐ๋Šฅ์€ ๋งŽ์œผ๋‚˜ ์„ฑ๋Šฅ์€ ์นดํŽ˜์ธ์ด ์šฐ์„ธํ•˜๋‹ค.( https://www.ehcache.org/documentation/3.1/clustered-cache.html )


๋ ˆ๋””์Šค ์„ค์ •์„ ๋ดค์œผ๋ฉด ๋Š๊ผˆ๊ฒ ์ง€๋งŒ ์บ์‹œ ๊ตฌํ˜„์ฒด ์„ค์ •์ด ๊ท€์ฐฎ์€๊ฑฐ์ง€ CacheManager ์„ค์ •์€ ๋ณ„๊ฑฐ์—†๋‹ค ใ…Ž

// ์Šคํ”„๋ง ๋กœ์ปฌ cache๋ฅผ ์œ„ํ•œ ์„ค์ •
implementation 'org.springframework.boot:spring-boot-starter-cache'
// caffeine
implementation 'com.github.ben-manes.caffeine:caffeine'

 

@EnableCaching
@Configuration
public class CacheConfig {

    @Bean // ์นดํŽ˜์ธ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•œ๋‹ค. ๋ฌผ๋ก  ๊ผญ ๋นˆ์œผ๋กœ ๋“ฑ๋กํ•  ํ•„์š”๋Š” ์—†๋‹ค.
    public Caffeine caffeineConfig() {
        return Caffeine
                .newBuilder()
                 .recordStats() // ์บ์‹œ ํ†ต๊ณ„ ๊ธฐ๋ก
                 .expireAfterWrite(60, TimeUnit.MINUTES) // .expireAfterWrite(java.time.Duration.ofMinutes(60L))
                 .maximumSize(5)
    }

    @Bean
    public CacheManager cacheManager(Caffeine caffeine) {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        cacheManager.setCaffeine(caffeine);
        return cacheManager;
    }
}

โœ” initialCapacity: ๋‚ด๋ถ€ ํ•ด์‹œ ํ…Œ์ด๋ธ”์˜ ์ตœ์†Œํ•œ์˜ ํฌ๊ธฐ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

โœ” maximumSize: ์บ์‹œ์— ํฌํ•จํ•  ์ˆ˜ ์žˆ๋Š” ์ตœ๋Œ€ ์—”ํŠธ๋ฆฌ ์ˆ˜๋ฅผ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.

โœ” maximumWeight: ์บ์‹œ์— ํฌํ•จํ•  ์ˆ˜ ์žˆ๋Š” ์—”ํŠธ๋ฆฌ์˜ ์ตœ๋Œ€ ํฌ๊ธฐ๋ฅผ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค. (* maximumSize์™€ ํ•จ๊ป˜ ์ง€์ •ํ•  ์ˆ˜ ์—†๋‹ค, ๋‘˜ ์ค‘ ํ•˜๋‚˜๋งŒ)

โœ” expireAfterAccess: (์บ์‹œ๊ฐ€ ์ƒ์„ฑ๋œ ํ›„ or ๋Œ€์ฒด๋˜๊ฑฐ๋‚˜ ๋งˆ์ง€๋ง‰์œผ๋กœ ์ฝ์€ ํ›„) ํŠน์ • ๊ธฐ๊ฐ„์ด ๊ฒฝ๊ณผํ•˜๋ฉด ์บ์‹œ์—์„œ ์ž๋™์œผ๋กœ ์ œ๊ฑฐ

โœ” expireAfterWrite: (ํ•ญ๋ชฉ์ด ์ƒ์„ฑ๋œ ํ›„ or ๋ฐ”๋€ ํ›„) ํŠน์ • ๊ธฐ๊ฐ„์ด ์ง€๋‚˜๋ฉด ๊ฐ ํ•ญ๋ชฉ์ด ์บ์‹œ์—์„œ ์ž๋™์œผ๋กœ ์ œ๊ฑฐ

โœ” refreshAfterWrite: ์บ์‹œ๊ฐ€ ์ƒ์„ฑ๋˜๊ฑฐ๋‚˜ ๋งˆ์ง€๋ง‰์œผ๋กœ ์—…๋ฐ์ดํŠธ๋œ ํ›„ ์ง€์ •๋œ ์‹œ๊ฐ„ ๊ฐ„๊ฒฉ์œผ๋กœ ์บ์‹œ๋ฅผ ์ƒˆ๋กœ๊ณ ์นจ(๊ฐฑ์‹ ) ํ•ฉ๋‹ˆ๋‹ค.

โœ” weakKeys: ํ‚ค๋ฅผ weak reference๋กœ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค. (GC์—์„œ ํšŒ์ˆ˜๋จ)

โœ” weakValues: Value๋ฅผ weak reference๋กœ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค. (GC์—์„œ ํšŒ์ˆ˜๋จ)

โœ” softValues: Value๋ฅผ soft reference๋กœ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค. (๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ๊ฐ€๋“ ์ฐผ์„ ๋•Œ GC์—์„œ ํšŒ์ˆ˜๋จ)

โœ” recordStats: ์บ์‹œ์— ๋Œ€ํ•œ Statics๋ฅผ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค.

 

์ฐธ๊ณ ๋กœ recordStats()๋Š” ์•„๋ž˜์™€ ๊ฐ™์€ ์บ์‹œ ํ†ต๊ณ„ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•œ๋‹ค. ์ด๋Š” Cache.stats() ๋ฉ”์„œ๋“œ๋กœ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

// com.github.benmanes.caffeine.cache.Cache ์ธํ„ฐํŽ˜์ด์Šค
Cache.stats() // CacheStats ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
public final class CacheStats {

    // ... 
    private final long hitCount;
    private final long missCount;
    private final long loadSuccessCount;
    private final long loadFailureCount;
    private final long totalLoadTime;
    private final long evictionCount;
    private final long evictionWeight;
    // ... 
    public double hitRate() {/* ... */}
    public long evictionCount() {/* ... */}
    public double averageLoadPenalty() {/* ... */}
    // ... 
}

 

์บ์‹œ ์„ค์ • ๊ฟ€ํŒ1 - ENUM ์ด์šฉํ•˜๊ธฐ

๋งค๋ฒˆ ์„ค์ •๊ฐ’๋“ค์„ Configuration์—์„œ ์ˆซ์ž๋กœ ๋ฐ•๋Š” ๊ฑด, ์œ ์ง€๋ณด์ˆ˜๋‚˜ ๊ด€๋ฆฌ ์ธก๋ฉด์—์„œ ์ข‹์ง€ ๋ชปํ•˜๋‹ค.
์•„๋ž˜์™€ ๊ฐ™์ด ์„ค์ •๊ฐ’๋“ค์„ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” CacheType ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค๊ณ , ์ด๋ฅผ Enum์œผ๋กœ ๋“ค๊ณ ์žˆ์ž.

@Getter
public enum CacheType {

  ARTISTS("artists", 5 * 60, 10000), // cachename: artists
  ARTIST_INFO("artistInfo", 24 * 60 * 60, 10000); // cachename: artistInfo

  CacheType(String cacheName, int expiredAfterWrite, int maximumSize) {
    this.cacheName = cacheName;
    this.expiredAfterWrite = expiredAfterWrite;
    this.maximumSize = maximumSize;
  }

  private String cacheName;
  private int expiredAfterWrite;
  private int maximumSize;

}

 

 

์บ์‹œ ์„ค์ • ๊ฟ€ํŒ2- ์„ค์ •(yml) ์ฝ์–ด์„œ ์ด์šฉํ•˜๊ธฐ

ํ•œ๊ฑธ์Œ ๋” ๋‚˜์•„๊ฐ€์„œ, ์„ค์ •๊ฐ’๋“ค์„ ์ž๋ฐ” ๊ฐ์ฒด๊ฐ€ ์•„๋‹Œ ์Šคํ”„๋ง ํ”„๋กœํผํ‹ฐ๋กœ ์ผ๊ด„์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜์—ฌ ์ฝ์–ด์˜ค๋ฉด ๋” ๊น”๋”ํ•˜๋‹ค.

my:
  project:
    cache-manager:
      type: REDIS
      caches:
        - cacheName: accessToken
          expireAfterWrite: 1800
          
        - cacheName: loginOtp
          expireAfterWrite: 300
          
      type: CAFFEINE
      caches:
        - cacheName: test
          expireAfterWrite: 180
          maximumSize: 5
          
        - cacheName: channels
          expireAfterWrite: 180
          maximumSize: 1
          
        - cacheName: categoryContents
          expireAfterWrite: 1800
          maximumSize: 30
          
        - cacheName: newContentIds
          expireAfterWrite: 1800
          maximumSize: 1

 

์ดํ›„ ์ด ์„ค์ •์„ ์‰ฝ๊ฒŒ ์ฝ์„ ์ˆ˜ ์žˆ๊ฒŒ ์Šคํ”„๋ง ํ”„๋กœํผํ‹ฐ ๊ฐ์ฒด๋ฅผ ๋”ฐ๋กœ ๋งŒ๋“ ๋‹ค.

// yml ํ”„๋กœํผํ‹ฐ๋ฅผ ๊ฐ์ฒด๋กœ ์ฝ์–ด์˜ค๋Š” ์–ด๋…ธํƒœ์ด์…˜
@ConstructorBinding
@ConfigurationProperties(prefix = "my.project.cache-manager") // ํ”„๋กœํผํ‹ฐ ๊ฐ’ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
data class MyCacheProperties(
  val type: String,
  val caches: List<ArBookCache>? = null
) {
  data class MyCache(
    val cacheName: String,
    val expireAfterWrite: Long,
    val maximumSize: Long? = null
  )
}

 

์•„๋ž˜์™€ ๊ฐ™์ด ํ”„๋กœํผํ‹ฐ๋ฅผ ์ด์šฉํ•˜์—ฌ ์‹ค์ œ ์„ค์ • ๊ฐ’์„ ์ฝ”๋“œ๊ฐ€ ๋ชฐ๋ผ๋„, ๋“ฑ๋กํ•  ์ˆ˜ ์žˆ๊ฒŒ ์ถ”์ƒํ™”ํ•œ๋‹ค.

@Configuration
@EnableCaching // ์ฃผ์˜ - ConfigurationProperties๋ฅผ ์‚ฌ์šฉํ• ๋•, Enable ์–ด๋…ธํ…Œ์ด์…˜์„ ์ถ”๊ฐ€ํ•ด์•ผ ์ƒ์„ฑ๋œ๋‹ค.
@EnableConfigurationProperties(MyCacheProperties::class)
class CachingConfig() {
    
    @Bean
    fun cacheManager(myCacheProperties: MyCacheProperties): CacheManager{
        val cacheProperties:List<MyCacheProperties.MyCache> = myCacheProperties.caches
         ?: return NoOpCacheManager() // yml ์„ค์ •๊ฐ’์ด ์—†๋Š” ๊ฒฝ์šฐ, ์บ์‹œ๋ฅผ ์ ์šฉํ•˜์ง€ ์•Š๋„๋ก ๋งŒ๋“ฆ
         
        if(cacheProperties.type == "CAFFEINE"){
           val caches:List<CaffeineCache> = cacheProperties.map{
             CaffeineCache( it.cacheName, // it == MyCacheProperties.MyCache
                            Caffeine.newBuilder()
                            .expireAfterWrite(it.expireAfterWrite.seconds.toJavaDuration())
                            .maximumSize(property.maximumSize ?: 1)
                            .build()
                            )
        }
        
        /*.. ์ดํ•˜ CacheManager ์ƒ์„ฑ๋ถ€๋ถ„ ์ƒ๋žต ..*/

    }
}

์œ„ ์ฝ”๋“œ๋Š” ์˜ˆ์ œ์ผ ๋ฟ์ด๊ณ , ์œ„์—์„œ ์‚ด์ง ์–ธ๊ธ‰ํ•œ @CondtionalOnProperty ๊ฐ™์€์Šคํ”„๋ง๋ถ€ํŠธ์˜ ์กฐ๊ฑด๋ถ€ ๋นˆ ๋“ฑ๋ก ๊ณผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋ฉด, yml ์„ค์ •๋งŒ์œผ๋กœ ๋™์ž‘์„ ๋ณ€๊ฒฝํ•˜๋„๋ก ์ถ”์ƒํ™” ํ•  ์ˆ˜ ์žˆ๋‹ค.

 

๋งŒ๋“ค๊ธฐ ๋‚˜๋ฆ„์ด๋‹ค. ๋งŒ์•ฝ ์บ์‹œ๋งค๋‹ˆ์ €๊ฐ€ ์—ฌ๋Ÿฌ๊ฐœ๋ผ๋ฉด ์œ„์—์„œ ์–ธ๊ธ‰ํ•œ CacheConfigurerSupport, CacheResolver๋“ฑ์„ ์‚ฌ์šฉํ•˜์—ฌ ์ ์ ˆํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•˜์ž

'๐ŸŒฑ Spring Framework' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

Cache#2 ์บ์‹œ์— ๋Œ€ํ•œ ์ดํ•ด  (1) 2022.08.28

๋ธ”๋กœ๊ทธ์˜ ์ •๋ณด

JiwonDev

JiwonDev

ํ™œ๋™ํ•˜๊ธฐ