pagedModel fetchJoin 적용후 redis Cache 적용시 발생하는 오류

2024. 7. 22. 21:13TIL

 문제발생

 

기존 프로젝트에서 pagedModel을 사용하라는 권고 표시가 나와서 pagedModel을 적용을 하였다.

 

진행하고자 하는 프로젝트에서 프로모션 관련한 api 기능은 많은 사람들이 조회할 수 밖에 없도록 설계되었기 때문에 Cache를 적용하여 성능향상에 도움이 될 수 있도록 Redis Cache를 적용하기로 하였다.

@Cacheable(value = ["promotionCache"], key = "#page + '-' + #size + '-' + #sortBy + '-' + #direction")
override fun getPromotionWineList(
    page: Int,
    size: Int,
    sortBy: String,
    direction: String
): PagedModel<WinePromotionModel> {

    val pageable: Pageable = PageRequest.of(page, size, getDirection(direction), sortBy)
    val promotionsPage: Page<WinePromotion> = winePromotionRepository.findPromotionWineList(pageable)
    return pagedResourcesAssembler.toModel(promotionsPage, winePromotionAssembler)
}

 페이지네이션을 고려하여 각 페이지마다 캐시를 적용시키도록 기대하였다.

 

하지만 캐시를 적용하자 직렬화 관련 오류가 발생하였다.

 

🆘 해결 과정

 

직렬화 문제를 해결하기 위해서  redis config를 수정해보았다.

package sparta.nbcamp.wachu.infra.redis

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
//import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import org.springframework.beans.factory.annotation.Value
import org.springframework.cache.CacheManager
import org.springframework.cache.annotation.EnableCaching
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.redis.cache.RedisCacheConfiguration
import org.springframework.data.redis.cache.RedisCacheManager
import org.springframework.data.redis.connection.RedisConnectionFactory
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer
import org.springframework.data.redis.serializer.RedisSerializationContext
import org.springframework.data.redis.serializer.StringRedisSerializer
import java.time.Duration

@EnableCaching
@Configuration
@EnableRedisRepositories
class RedisConfig {

    @Value("\${spring.data.redis.host}")
    private lateinit var redisHost: String

    @Value("\${spring.data.redis.port}")
    private var redisPort: Int =0

    @Value("\${spring.data.redis.password}")
    private lateinit var redisPassword: String

    @Bean
    fun redisConnectionFactory(): RedisConnectionFactory {
        return LettuceConnectionFactory(redisHost, redisPort).apply {
            setPassword(redisPassword)
        }
    }

    @Bean
    fun redisTemplate(connectionFactory: RedisConnectionFactory, objectMapper: ObjectMapper): RedisTemplate<String, Any> {
        val template = RedisTemplate<String, Any>()
        template.setConnectionFactory(connectionFactory)
        template.keySerializer = StringRedisSerializer()
        template.valueSerializer = GenericJackson2JsonRedisSerializer(objectMapper)
        return template
    }


    @Bean
    fun cacheManager(redisConnectionFactory: RedisConnectionFactory, objectMapper: ObjectMapper): CacheManager {
        val redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofHours(1)) 
            .disableCachingNullValues()
            .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(GenericJackson2JsonRedisSerializer(objectMapper)))

        return RedisCacheManager.builder(redisConnectionFactory)
            .cacheDefaults(redisCacheConfiguration)
            .build()
    }
}

 

 

하지만 오류가 여전히 발생하였는데 localDateTime을 직렬화 할 수 없다는 오류 였다.

 

해당 문제를 해결하기 위해서 localDate 형식을 직렬화 할 수 있도록 해당 설정을 추가하였다.

package sparta.nbcamp.wachu.infra.redis

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.hateoas.mediatype.hal.Jackson2HalModule
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder

@Configuration
class JacksonConfig {
    @Bean
    fun objectMapper(): ObjectMapper {
        return Jackson2ObjectMapperBuilder.json()
            .modules(JavaTimeModule(), Jackson2HalModule())  // HAL 모듈 추가
            .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)  // WRITE_DATES_AS_TIMESTAMPS 설정 비활성화
            .build()
    }
}

 

해당 문제를 해결하니 새로운 오류가 또 발생하였는데 Jackson2HalModule 관련한 오류가 발생하였다.

 

. Spring HATEOAS를 사용할 때 PagedModel과 같은 HATEOAS 모델을 올바르게 직렬화하고 역직렬화하려면 추가적인 설정이 필요하다고 한다.

 

또한 swagger로 테스트를 하던 도중 치명적인 오류를 발생했다.

바로 완전히 분리되있는 로그인, 회원가입 기능 등이 오류가 발생한 것이다.

 

찾아본 결과 해당 오류의 발생 원인은 Jackson2HalModule과 관련된 설정이 애플리케이션 전반에 영향을 미치는 방식 때문이었다.

이 모듈은 JSON 직렬화/역직렬화 과정에서 특정 방식으로 데이터를 처리하기 때문에, 애플리케이션의 모든 부분에 영향을 줄 수 있어 완전히 분리되어있는 기능인 회원가입 로그인 기능도 오류가 발생한 것이었다.

@Configuration
class JacksonConfig {

    @Bean
    fun objectMapper(): ObjectMapper {
        return ObjectMapper().apply {
            registerModule(JavaTimeModule())
            registerModule(Jackson2HalModule())
            configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
        }
    }
}

 

ObjectMapper로 인해서 모든 api가 영향을 받는 치명적인 오류가 발생하여 해당 방식이 아닌 다른 방식으로 찾아봐야겠다.