캐시의 종류는 많지만 이 글에서는 Redis 를 사용하여 캐시를 적용하고 발견한 이슈들을 정리 한다.
어느 기술이나 잘 사용하면 좋은 방안이 될 수 있지만 잘못 사용하면 독이 될 수 있다
설정
@Bean
fun cacheManager(redisConnectionFactory: RedisConnectionFactory): CacheManager {
val defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
.disableCachingNullValues()
.entryTtl(Duration.ofHours(1L))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer())) // 키를 문자열로 직렬화
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
redisSerializer()
)
)
return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(defaultCacheConfig)
.build()
}
@Bean
fun redisSerializer(): RedisSerializer<Any> {
val objectMapper = ObjectMapper().registerKotlinModule()
.registerModule(JavaTimeModule())
.activateDefaultTyping(
BasicPolymorphicTypeValidator.builder()
.allowIfBaseType(Any::class.java).build(), ObjectMapper.DefaultTyping.EVERYTHING)
return GenericJackson2JsonRedisSerializer(objectMapper)
}
Redis 캐시매니저를 빈에 추가 시켜준다. 설명은 아래 이슈 정리에 적어두었다.
사용
@CachePut(value = ["post"], key = "#req.markerId")
@CachePut(value = ["comment"], key = "#req.markerId")
@CachePut(value = ["userInfo"], key = "#req.markerId")
카테고리 - value / key - 캐시 데이터
키를 기준으로 검색 해서 업데이트를 한다
@Cacheable(value = ["post"], key = "#req.markerId")
키를 기준으로 post 에 있는 캐시를 조회한다
@CacheEvict(value = ["post"])
post 에 있는 모든 캐시를 무효화 한다.
주의 !!
A 가 게시글을 조회(DB)했다 -> B 가 게시글을 작성한다 -> A 가 게시글을 조회(캐시)한다.
이 상황에서는 A 는 캐시된 데이터를 조회 했기 때문에 B 가 작성한 게시물을 가져오지 않는다.
이럴땐 캐시 안에 있는 데이터를 업데이트 해야한다고 생각할 수 있다.
이럴땐
캐시를 무효화 해서, 조회시 캐시데이터를 최신화 해주는 전략이 필요하다.
생성 - @CacheEvict 캐시 무효화
조회 - @Cacheable 캐시 조회
수정 - @CachePut 캐시 업데이트
성능 테스트
캐시 전 조회
디비 조회가 1번 일어나는걸 확인할 수 있다.
이슈모음
Page 역직렬화 이슈
[2024-11-14 14:10:32:11978] WARN 85633 --- [nio-8810-exec-1] .m.m.a.ExceptionHandlerExceptionResolver : Resolved [org.springframework.data.redis.serializer.SerializationException: Could not read JSON:Cannot construct instance of `Dto` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)<EOL> at [Source: (byte[])" ~~~(through reference chain: ["content"]->java.util.ArrayList[0]) ]
Redis Cache 는 기본 생성자가 있어야 한다
1) 캐시처리를 적용할 메소드의 반환 class에 기본 생성자가 있어야 한다
2) Dto에는 @NoArgsConstructor 붙여 주어야 한다
3) !! 코틀린은 불변 데이터를 강조한다 !!
@JsonCreator 를 사용하여 주 생성자를 기본 생성자로 사용 할 수 있다.
@JsonIgnoreProperties(ignoreUnknown = true,value= ["pageable"])
class RestPage<T> @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) constructor(
@JsonProperty("content") content: MutableList<T>,
@JsonProperty("page") page: Int,
@JsonProperty("size") size: Int,
@JsonProperty("totalElements") totalElements: Long
) : PageImpl<T>(content, PageRequest.of(page, size), totalElements) {
constructor(page: Page<T>) : this(
content = page.content,
page = page.number,
size = page.size,
totalElements = page.totalElements
)
}
코틀린 모듈 추가 및 LocalDateTime 역직렬화 이슈
class java.util.LinkedHashMap cannot be cast to class
RedisSerializer 를 커스텀 하여 코틀린 모듈과 LocalDateTime 이 역직렬화 안되는 이슈를 해결 할 수 있다.
@Bean
fun cacheManager(redisConnectionFactory: RedisConnectionFactory): CacheManager {
val defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1L))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer())) // 키를 문자열로 직렬화
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
redisSerializer()
)
)
.disableCachingNullValues()
return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(defaultCacheConfig)
.build()
}
fun redisSerializer(): RedisSerializer<Any> {
val objectMapper = ObjectMapper()
.registerKotlinModule() // 코틀린 지원 추가
.registerModule(JavaTimeModule()) // 날짜 및 시간 객체 지원 추가
.activateDefaultTyping(
BasicPolymorphicTypeValidator.builder()
.allowIfBaseType(Any::class.java).build(), // 다형성 타입 허용 조건 설정
ObjectMapper.DefaultTyping.NON_FINAL // Final이 아닌 클래스에 대해 다형성 타입 활성화
)
return GenericJackson2JsonRedisSerializer(objectMapper)
}
QueryDSL ResponseDTO
class GetBoardListQueryDSLDto(
... , fileNames: String?
) {
.
.
.
.
val fileNames: List<String> = fileNames?.split(",")?.filter { it.isNotBlank() } ?: emptyList()
fun toResponseDto() = GetBoardListResponseDto(
.
.
.
.
fileNames = fileNames
)
}
class GetBoardListResponseDto(
.
.
.
.
var fileNames: List<String>?
)
Projection 을 사용해 에서 받아온 String 데이터를 List형태로 반환하는 Dto 가 있었다.
DB 에서는 String 형태의 List 를 받아오지만 캐시에서는 List 를 받아와서 역직렬화가 안된다.
ResponseDto 를 따로 만들어주어 해결하였다.
'개-발 > Java + Spring + Kotlin' 카테고리의 다른 글
[Spring] ServletRequest 캐싱 (ContentCachingRequestWrapper) (0) | 2024.12.02 |
---|---|
[Spring] 상대방 채팅 읽음 감지 (1) | 2024.11.17 |
[WebSoket] Spring + SocketJs 사용하기 ( 테스트 Html코드 공유 ) (0) | 2024.11.02 |
[Spring Batch] 반복 오류 제어 (0) | 2024.09.01 |
[Java] 정규표현식 regex 패키지 (0) | 2024.07.29 |