Post

(Spring Boot 3) Mono 캐싱하기 (with caffeine)

Spring 6.1.0 부터, 반환타입 Mono에도 @Cacheable 사용이 가능하다.

코루틴 suspend func에 사용하는 것도 6.1.0 부터 가능하다.

설정 예시

CacheGroup

1
2
3
4
5
6
7
8
9
10
11
enum class CacheGroup(  
    val cacheName: String,  
    val ttl: Duration,  
) {  
    VAS(Names.VAS, Duration.ofMinutes(10)),  
    ;  
  
    object Names {  
        const val VAS = "VAS"  
    }  
}

CacheConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@EnableCaching  
@Configuration  
class CacheConfig {  
  
    @Bean  
    fun caffeineCacheManager(): CaffeineCacheManager =  
        CaffeineCacheManager().apply {  
            getCaffeineCacheList().forEach {  
                registerCustomCache(it.name, it.asyncCache)  
            }  
            setAsyncCacheMode(true)  // AsyncCacheMode 설정해줘야 Mono에 동작.
        }  

    /**
     * Caffeine 객체를 직접 생성 할 때 원하는 대로 커스터마이징이 가능하다. 
     * - eviction listener 등록
     * - expire 시 자동 refresh 설정 등
     */
    private fun getCaffeineCacheList(): List<CaffeineCache> =  
        CacheGroup.entries.map { cacheGroup ->  
            CaffeineCache(  
                cacheGroup.cacheName,  
                Caffeine.newBuilder().recordStats()  
                    .expireAfterWrite(cacheGroup.ttl)  
                    .buildAsync<Any, Any>(),  
                    // Async로 생성해줘야 Mono에 동작. 일반 동기식 함수에 붙여도 동작한다.  
                true,  
            )  
        }  
}

SimpleCacheManager를 사용하는 예제가 많이 보이는데, 카페인을 쓰는데 카페인 캐시 매니저를 쓰는게 더 알맞지 않나 싶다.
게다가 async 설정은 CaffeineCacheManager를 써야만 가능하다.

사용처

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Service  
class MyService {  
    @Cacheable(CacheGroup.Names.VAS)  
    fun aaa(): Mono<String> {  
        logger.info("### async A 호출 테스트")  
        return Mono.just("aaa")  // .cache()는 호출하지 않는다.
    }
    
    @Cacheable(CacheGroup.Names.VAS)
    fun bbb(): String {  // buildAsync 했지만 동기식 함수에 붙여도 동작한다.
        logger.info("### sync B 호출 테스트")  
        return "bbb"
    }
}

주의 사항

  • cacheName + 메서드 파라미터가 같으면 캐시 결과를 공유한다.
    • 이런 경우 리턴 타입이 다른 두 메서드에 적용하면, 이후에 호출되는 메서드는 캐시 결과를 가져오다가 리턴 타입이 안맞아 ClassCastException 발생한다.
    • 메서드 파라미터 다른 것으로 구분하지 말고, 아예 cacheName을 다르게 주는 것이 안전하다.
  • 위 예제에서 aaa() / bbb()cacheName + 메서드 파라미터가 같기 때문에 캐시를 공유한다.
    • aaa()를 먼저 호출한 경우, bbb() => "aaa"를 반환
    • bbb()를 먼저 호출한 경우, aaa() => "bbb"를 반환
    • 하나는 반환타입이 Mono<String>이고 하나는 String 임에도 결과를 공유한다. (캐시 결과를 내놓을 때 callee 메서드의 반환 타입은 전혀 신경쓰지 않기 때문)
This post is licensed under CC BY 4.0 by the author.