๐Ÿ’ป ๊ฐœ๋ฐœ ์ผ์ง€/SpringBoot

[SpringBoot/Kotlin] Caffeine Cache ์‚ฌ์šฉํ•˜๊ธฐ (LocalCache)

์ ์ด 2023. 6. 19. 19:15
๋ฐ˜์‘ํ˜•

Caffeine Cache ๋ž€?

๋กœ์ปฌ์บ์‹œ๋Š” ํ•ด๋‹น ๊ธฐ๊ธฐ์—์„œ๋งŒ ์‚ฌ์šฉ๋˜๋Š” ์บ์‹œ์ด๋‹ค. ์†๋„๊ฐ€ ๋น ๋ฅด์ง€๋งŒ ๋ถ„์‚ฐ ์‹œ์Šคํ…œ์ผ ๊ฒฝ์šฐ ๋ฐ์ดํ„ฐ ์ •ํ•ฉ์„ฑ์ด ๊นจ์งˆ ์ˆ˜ ์žˆ๋‹ค.

๋น„์ฆˆ๋‹ˆ์Šค ์š”๊ตฌ์‚ฌํ•ญ์— ๋”ฐ๋ผ ์–ด๋А์ •๋„๊นŒ์ง€ ์ •ํ•ฉ์„ฑ์„ ๋งž์ถฐ์•ผํ• ์ง€, ์–ผ๋งˆ๋‚˜ ์†๋„๊ฐ€ ์ค‘์š”ํ• ์ง€์— ๋”ฐ๋ผ ์บ์‹œ๋ฅผ ์„ ํƒํ•œ๋‹ค.

 

Caffeine Cache๋Š” java ์บ์‹ฑ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ค‘ ๋†’์€ ์„ฑ๋Šฅ์„ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ์บ์‹œ์ด๋‹ค.

Caffeine์€ Window TinyLfu eviction ์ •์ฑ…์„ ๊ฐ€์ง€๊ณ  ์žˆ๋Š”๋ฐ, ์ด๋Š” ์ตœ์ ์˜ ์ ์ค‘๋ฅ ์„ ์ œ๊ณตํ•œ๋‹ค.


Setting

build.gradle.kts
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-cache")
    implementation("com.github.ben-manes.caffeine:caffeine")
}

Cache Config ์ž‘์„ฑ

@Configuration
@EnableCaching
class CacheConfig { }
  • CacheConfig ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๊ณ , ์บ์‹œ๋กœ ์‚ฌ์šฉํ•  Bean๋“ค์„ ์ •์˜ํ•ด๋†“๋Š”๋‹ค.

Cache Enum ์ •์˜

enum class CacheType (
    private val cacheName: String,
    private val expireAfterWrite: Long,
    private val timeUnit: TimeUnit
) {
    USER_INFO_CACHE(
        USER_INFO_CACHE_NAME,     // ์บ์‹œ ์ด๋ฆ„: UserInfo
        10,
        TimeUnit.SECONDS,         // ๋งŒ๋ฃŒ ์‹œ๊ฐ„: 10s 
    );
    
    fun getCacheName() = cacheName
    fun getExpireAfterWrite() = expireAfterWrite 
    fun getTimeUnit() = timeUnit

    companion object {
        const val USER_INFO: String = "UserInfo"
    }
}
  • Enum ํด๋ž˜์Šค ๋ฐ ๊ฐ ํ•„๋“œ์— ๋Œ€ํ•ด Getter ์ƒ์„ฑ

Cache Manager Bean ๋“ฑ๋ก

@Bean
fun cacheManager(): CacheManager {
    val cacheManager = SimpleCacheManager()
    val caches = CacheType.values().map {        
        CaffeineCache(
            it.getCacheName(),
            Caffeine.newBuilder()
                .recordStats()
                .expireAfterWrite(it.getExpireAfterWrite(), it.getTimeUnit())
                .build()
        )
    }

    cacheManager.setCaches(caches)
    return cacheManager
}
  • CacheType์— ์ •์˜๋œ ์บ์‹œ๋“ค์„ CaffeineCache ๊ฐ์ฒด๋กœ ์ƒ์„ฑํ•˜์—ฌ, CacheManager์— ๋“ฑ๋ก

Cache KeyGenerator

๋ณ„๋„์˜ keyGenerator๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, ๊ธฐ๋ณธ์ ์œผ๋กœ ์ œ๊ณต๋˜๋Š” ์•„๋ž˜์˜ SimpleKeyGenerator ์‚ฌ์šฉ
public static Object generateKey(Object... params) {
    if (params.length == 0) {
        return SimpleKey.EMPTY;
    }
    if (params.length == 1) {
        Object param = params[0];
        if (param != null && !param.getClass().isArray()) {
            return param;
        }
    }
    return new SimpleKey(params);
}
  • ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ์—†๋‹ค๋ฉด, SimpleKey.EMPTY๊ฐ€ key๋กœ ์‚ฌ์šฉ
  • ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ 1๊ฐœ๋ผ๋ฉด, ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ key๋กœ ์‚ฌ์šฉ
  • ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ 2๊ฐœ ์ด์ƒ์ด๋ผ๋ฉด, SimpleKey ๊ฐ์ฒด๋กœ ์‚ฌ์šฉ

CustomKeyGenerator

@Bean("UserInfoCacheKeyGenerator")
fun userInfoCacheKeyGenerator(): KeyGenerator {
    return KeyGenerator { _, method, params ->
        if (params.size > 2) {
            "${method.name}:${params[2]}"
        } else {
            "${method.name}:${UUID.randomUUID()}"
        }
    }
}
  • ๋ฉ”์†Œ๋“œ๋ช…๊ณผ ํŠน์ • parameter๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ Key ์ƒ์„ฑ

CacheConfig.kt ์ „์ฒด

@Configuration
@EnableCaching
class CacheConfig { 
    @Bean
    fun cacheManager(): CacheManager {
        val cacheManager = SimpleCacheManager()
        val caches = CacheType.values().map {        
            CaffeineCache(
                it.getCacheName(),
                Caffeine.newBuilder()
                    .recordStats()
                    .expireAfterWrite(it.getExpireAfterWrite(), it.getTimeUnit())
                    .build()
            )
        }
    
        cacheManager.setCaches(caches)
        return cacheManager
    }
    
    @Bean("UserInfoCacheKeyGenerator")
    fun userInfoCacheKeyGenerator(): KeyGenerator {
        return KeyGenerator { _, method, params ->
            if (params.size > 2) {
                "${method.name}:${params[2]}"
            } else {
                "${method.name}:${UUID.randomUUID()}"
            }
        }
    }
    
    enum class CacheType (
        private val cacheName: String,
        private val expireAfterWrite: Long,
        private val timeUnit: TimeUnit
    ) {
        USER_INFO_CACHE(
            USER_INFO_CACHE_NAME,     // ์บ์‹œ ์ด๋ฆ„: UserInfo
            10,
            TimeUnit.SECONDS,         // ๋งŒ๋ฃŒ ์‹œ๊ฐ„: 10s 
        );
        
        fun getCacheName() = cacheName
        fun getExpireAfterWrite() = expireAfterWrite 
        fun getTimeUnit() = timeUnit
    
        companion object {
            const val USER_INFO: String = "UserInfo"
        }
    }
}
 

Cache ์‚ฌ์šฉ

@Cacheable(cacheNames = [CacheType.USER_INFO], keyGenerator = "UserInfoCacheKeyGenerator")
fun getUserInfo(userId: String) {...}
  • defaultKeyGenerator ์‚ฌ์šฉ ์‹œ, ์ƒ๋žต ๊ฐ€๋Šฅ

TestCode

class UserInfoCacheTests {
    @SpykBean
    lateinit var cacheManager: CacheManager
    lateinit var userInfoCache: Cache<Any, Any>

    @BeforeEach
    fun beforeEach() {
        userInfoCache = (cacheManager.getCache(CacheConfig.CacheType.USER_INFO) as CaffeineCache).nativeCache
    }

    @Test
    fun `USER_INFO ์บ์‹œ๊ฐ€ ๋™์ž‘ํ•œ๋‹ค`() {
        getUserInfo("requestId", "userId")
        getUserInfo("requestId", "userId")

        assertEquals(1, userInfoCache.asMap().keys.size)
        eventCache.asMap().map {
            assertEquals("getUserInfo|userId", it.key)
            assertNotNull(it.value)
        }

        assertEquals(1, userInfoCache.stats().hitCount())
        assertEquals(1, userInfoCache.stats().missCount())
    }
}

https://www.baeldung.com/java-caching-caffeine

https://wave1994.tistory.com/182

๋ฐ˜์‘ํ˜•