https://erjuer.tistory.com/131
에서 Caffeine 캐시를 등록하고 이제 캐시를 활용해보고자 하였다.
실무 개발을 진행하며서 Spring Boot에서 제공하는 어노테이션 @Cacheable("")를 사용해여 DB 조회를 매번 하는 것이 아니라 캐시를 통해서 조회 쿼리를 최소화 하려고 하였으나 hit 되지 않고 자꾸 DB를 조회하는 로직이 수행되었다.
Cache가 적용되는 순서를 살펴보면
- @Cacheable이 설정된 함수 호출시 Cache 내 Key값이 있는지 확인한다
- Key값이 있으면 그 Key값에 대한 데이터를 반환한다. -> 끝
- Kery값이 없으면 Key를 새로 생성한다.
- Simplekeygenerator를 통해 Key를 생성한다,.
- 생성된 Key값으로 데이터를 Cache 영역에 저장한다. -> 끝
Cache가 hit 되지 않는 이유는 간단하다.
Key값이 매핑이 되지 않는다는 것
비동기 함수 즉 suspend 함수를 사용하면서 이런 증상이 발생했는데 그 이유는
suspend 함수를 complie 하면 기존 파라미터외 Continuation<T>가 추가 된다.
CPS style로 바뀐다. (Continuation-passing style) →
suspend 함수는 컴파일 하게 되면 switch case형태의 바이트코드가 생성되며 이는 코루틴의 중단 재개 시점을 알 수 있도록 하는 파라미터가 추가 된다. (단순히 중단과 재개시점을 파악하려는 flag값으로 이해하면 쉽다.)
코드로 확인해보자!
캐시값을 저장하면서 Key 생성하는 부분은 다음과 같다.
Debug를 찍어보면 params가 두개 찍히는 것을 확인 할 수 있다.
첫번째는 그 함수의 파라미터로 설정한 프로퍼티였고 또 다른 하나는 Continuation at... 을 확인 할 수 있으며 이는 컴파일이 추가된 Continuation 프로퍼티라는 것을 확인 할 수 있다.
즉 suspend 함수에 @Cacheable 어노테이션을 걸어 캐시를 저장한다고 해도 Continuation이 매번 새롭게 생성 되면서 key값 또한 새롭게 생성 된 것이다.
최종 키값을 확인해보면 Continuation이 추가된 Key값을 확인 할 수 있다.
그렇다면 해결방법은?
자동으로 Key값을 생성하는 것이 아닌 내가 직접 Key값을 생성하는 것이다. 즉 Continuation을 제외한 내가 설정한 파라미터 값으로만 Key값을 생성할 수 있도록 설정한다.
KeyGernator를 상속받아서 customKeyGenerator를 생성하였다.
params를 넘겨 받을 때 마지막에 위치하는 Continuation 객체를 제외한 나머지를 list형태로 만들어 hashcode를 생성하고 이를 Key값으로 생성하였다.
(물론 method가 suspend 여부에 따라 마지막 파라미터값을 삭제 또한 설정하면 더 좋다. 일단 예시로 간단히 표현하였다.)
그리고 "suspendKeyGenerator" Bean등록을 하고 @Cacheable 파라미터 내 KeyGerator를 지정하면 된다.
// BaseService
@Cacheable(value = ["baseCache"], keyGenerator = "suspendKeyGenerator")
suspend fun getbaseList(form: baseForm): MutableList<Base> =
baseRepository.getBaseList(form).toList()
끝!
인줄 알았으나 해당 값을 적용하고 테스트 진행 시 최초 요청은 DB를 조회하지만
재조회 했을 경우에는 무한 루프 즉 결과 값이 나오지 않고 기약을 알 수 없는 응답 대기하게 된다.
-> 엇.. Cache 영역 안에 있는 데이터를 부를 때 뭔가 오류가 발생한게 아닐까?
그 이유를 이제 key값 생성이 아니라 key값 생성 후 데이터를 저장 그리고 찾을 때의 로직을 살펴보자.
위는 키값을 생성하고 최초로 데이터를 캐시영역안에 저장할 때 실행되는 로직이다.
여기서 name ="COROUTINE_SUSPEND" 라는 것을 확인 할 수 있는데 이는 DB값을 조회한 결과값이 아닌 "COROUTINE_SUSPEND" 값이 저장되는 확인 할 수 있다.
다시 조회 했을 경우 cache hit는 했지만 codeValue값이 COROUTINE_SUSPEND값이라는 것을 확인 할 수 있다.
구체적인 원인은 CacheInterceptor에서 캐시 저장 및 불러올 때 @cacheable이 달린 함수 상태가 비동기 및 suspend 인지 모르고 있기 때문이다.
그 때문에 비동기 리턴 타입을 인식하지 못하고 어플리케이션이 일시 중지 상태가 되는 것이다.
(https://github.com/micronaut-projects/micronaut-cache/issues/183)
이에 따라
나는 Deferred 객체를 활용하여 아예 그 suspend의 함수의 비동기 상태를 객체로 저장하였다.
// baseComponent.kt
@Cacheable(value = ["baseCache"], keyGenerator = "suspendKeyGenerator")
suspend fun getBaseList(form: baseForm): Deferred<Base> = CoroutineScope(Dispatchers.Default).async {
baseRepository.getBaseList(form)
}
// baseService.kt
fun retriveBaseList() ; String {
val caffeineCache = cacheManager.getCache("Contents") as CaffeineCache
val key = keyGenerator.generate(#{클래스명}, #{메소드명}, form)
baseComponent.getBaseList(form).await() // 캐시 데이터 저장 및 불러오기
return "성공!"
}
이라고 하면 끝!
이지만 또 끝이 아니다.
왜냐하면 위의 코드에서는 문제점이 하나 있다.
비동기로 동작하다가 실제적인 데이터는 await 함수가 실행 시값을 가져올 수 있는데 혹시 await 했더니 NPE 또는 에러가 발생한다면..
?
: Deferred 객체를 캐시 데이터로 저장했기 때문에 저장 당시에 해당 데이터가 에러인지는 판단할 수 없으며 (왜냐하면 await해야 그 데이터를 확인할 수 있으니까) 혹시나 실제 데이터 조회 시 에러가 발생한다면 그 에러 자체를 데이터로 저장한다.
예륻 들면 'Key" : #{key}, "value" : Null point Exception 와 같이 저장되어 Exception이 터져버린다.
그러므로 정상적인 데이터 일 때 데이터를 저장 할 수 있도록 로직을 일부 수정해야 한다.
try {
val caffeineCache = cacheManager.getCache("Contents") as CaffeineCache
val key = keyGenerator.generate(#{클래스명}, #{메소드명}, form)
caffeineCache.nativeCache.getIfPresent(key)?.let {
return it as BaseDto
} ?: run {
val base = movieComponent.getBaseList(form).await()
val baseDto = mapper.toBase(base.toList()).toList()
caffeineCache.put(key, baseDto)
return baseDto
}
} catch (e: Exception) {
throw IllegalStateException()
}
진짜 끝.
P.S 아니면 다음과 같은 오프소스를 활용해도 좋을 듯 하다.
https://github.com/konrad-kaminski/spring-kotlin-coroutine/blob/master/spring-kotlin-coroutine/src/main/kotlin/org/springframework/kotlin/coroutine/cache/CoroutineCacheConfiguration.kt#L81
'개발 지식 > Spring Framework' 카테고리의 다른 글
[Spring] Caffeine Cache Config 설정하기 (0) | 2023.02.05 |
---|---|
[Spring] 구글 smtp 서버를 활용한 메일 보내기 (1) | 2023.01.15 |
[Reactive Mongo] Reactive MongoDB QueryDSL 버전 충돌 (0) | 2022.08.22 |
[Test] MongoDB 단위 테스트(Unit Test) 작성해보기 (4) | 2022.06.06 |
[Spring] SpringBoot + WebFlux + Kotlin + ReactiveMongo 프로젝트 생성 (1) | 2022.05.01 |
댓글