TroubleShooting] Redis Cache 사용 중 LocalDate, LocalDateTime 직렬화/역직렬화 오류

JUNHYUK CHANG·2024년 3월 7일
1

TIL

목록 보기
27/33

직렬화(Serialization)와 역직렬화(Deserialization)는

객체를 데이터로 변환하고, 다시 데이터를 객체로 변환하는 과정을 의미한다. 주로 네트워크 통신이나 파일 저장 등에서 객체를 전송하거나 저장할 때 사용된다.

직렬화(Serialization):

직렬화는 객체를 바이트 스트림(데이터를 바이트(byte)의 연속으로 나타낸 것)으로 변환하는 과정.
코틀린에서는 java.io.Serializable 인터페이스를 구현하여 직렬화할 수 있다.
직렬화를 위해서는 직렬화 가능한 클래스에 Serializable 인터페이스를 구현해야 하지만, 스프링 부트에서는 직렬화된 객체를 웹 요청 또는 응답으로 전송할 때 자동으로 직렬화를 수행한다.

스프링 부트에서는 일반적으로 웹 요청이나 응답에서 객체를 직렬화한다. 예를 들어, RESTful API에서 JSON 형식의 데이터를 전송할 때 해당 객체가 직렬화된다.

역직렬화(Deserialization):

역직렬화는 직렬화된 객체를 다시 원래의 객체로 변환하는 과정이다.
스프링 부트에서는 클라이언트가 서버에게 전송한 웹 요청의 본문(JSON 형식의 데이터)을 해당 객체로 역직렬화하여 컨트롤러 메서드의 매개변수로 전달한다.

이러한 과정을 통해 객체의 데이터를 네트워크를 통해 전송하거나 파일에 저장할 수 있다.

그리고 오늘의 핵심 메세지.

일반적으로 엔티티 클래스를 직렬화하는 것은 권장되지 않는다. 엔티티 클래스는 데이터베이스와의 상호작용을 담당하도록 하며, 직렬화와는 직접적인 관련이 없도록 하는 것이다.

대신 DTO(Data Transfer Object)나 VO(Value Object) 클래스를 사용하여 데이터 전송이나 특정 목적에 필요한 데이터를 포장하고, 이러한 객체를 직렬화하여 전송하는 것이 더 적합하다. 이렇게 함으로써 엔티티 클래스와 DTO/VO 클래스를 분리하여 엔티티 클래스를 변경하지 않고도 직렬화 및 데이터 전송에 대한 유연성을 확보할 수 있다.


위에서 설명하였듯 스프링부트에서는 직렬화/역직렬화를 자동으로 수행해주기 때문에 크게 신경쓰지 않아도 될 때가 많지만 예외가 있었으니, 바로 LocalDateTime 과 LocalDate 타입을 처리할때 이다. 이 두 타입은 자바 8부터 등장한 날짜와 시간을 나타내는 타입으로서, 직렬화를 위한 추가 작업이 필요하다.

그래서 Jackson 라이브러리 같은 직렬화 대안 라이브러리를 사용하여 객체를 직렬화할 수 있다. Jackson은 기본적으로 LocalDateTime과 LocalDate를 지원하지 않지만, 직렬화 및 역직렬화를 커스터마이징할 수 있는 기능을 제공한다. 예를 들어, Jackson에서는 커스텀 시리얼라이저 및 디시리얼라이저를 작성하여 LocalDateTime과 LocalDate를 원하는 형식으로 직렬화하거나 역직렬화할 수 있다.

org.springframework.data.redis.serializer.SerializationException: Could not write JSON: Java 8 date/time type java.time.LocalDate not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling

이런 메세지를 마주하였다면 아래의 해결방법을 시도해보시라.


0. 의존성 설치

implementation ("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
Java 8의 Date-Time API인 JSR-310(Java Specification Request)을 지원.
JSR-310을 사용하여 LocalDate, LocalDateTime 등의 새로운 날짜 및 시간 API를 JSON 형식으로 직렬화하고 역직렬화 가능.

implementation ("com.fasterxml.jackson.core:jackson-databind")
JSON 데이터를 Java 객체로 변환하거나 Java 객체를 JSON 데이터로 변환하는 기능을 제공.
JSON 데이터와 Java 객체 간의 변환을 위해 ObjectMapper 클래스를 사용.

1. RedisConfig 정의

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

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

   @Bean
    fun redisConnectionFactory(redissonClient: RedissonClient): RedisConnectionFactory {
        return RedissonConnectionFactory(redissonClient)
    }

   @Bean
    fun objectMapper(): ObjectMapper {
        val mapper = ObjectMapper()
        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) // timestamp 형식 안따르도록 설정
        mapper.registerModules(JavaTimeModule(), Jdk8Module()) // LocalDateTime 매핑을 위해 모듈 활성화 
        mapper.registerModule(ParameterNamesModule()); //생성자의 매개변수 이름을 사용하여 JSON 속성과 매핑
        return mapper
    }


    // Redis 캐시 메니저를 생성하는 메서드. 레디스 연결 팩토리 를 인자로 받아 캐시 매니저를 생성함
    @Bean
    fun cacheManager(cf: RedisConnectionFactory, objectMapper: ObjectMapper): CacheManager {

        // Redis 캐싱 구성을 생성하는 부분
        val redisCacheConfiguration =
            RedisCacheConfiguration.defaultCacheConfig() // 기본 캐시 구성 사용.
                .serializeKeysWith(  // 키를 직렬화 - StringRedisSerializer 사용
                    RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer()))
                .serializeValuesWith( // 값을 직렬화 - GenericJackson2JsonRedisSerializer 사용
                    RedisSerializationContext.SerializationPair.fromSerializer(
                        GenericJackson2JsonRedisSerializer(objectMapper)
                    ))
                .entryTtl(Duration.ofSeconds(30L)) // 캐시 만료시간  3분으로 설정

        // "tickets" 라는 이름의 캐시에 대해 적용
        val ticketsCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(5))
            .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(
                GenericJackson2JsonRedisSerializer(objectMapper)
            ))


        return RedisCacheManager // RedisCacheManager를 생성하기 위한 빌더 패턴을 사용
            .builder(cf) // RedisConnectionFactory 를 받아 CacheManager를 빌드
            .cacheDefaults(redisCacheConfiguration) // 캐시 매니저의 기본 캐시 구성 설정. 위에서 만든 RedisCache 구성 사용
            .withCacheConfiguration("tickets", ticketsCacheConfig) // "tickets" 라는 이름의 캐시에 대해 적용
            .build() // 최종적으로 빌드하여 반환. 캐싱을 관리하고 사용할 수 있도록 함.
    }
}

2. 날짜 형식 처리가 필요한 곳에 어노테이션 설정

    @JsonSerialize(using = LocalDateTimeSerializer::class)
    @JsonDeserialize(using = LocalDateTimeDeserializer::class)
    @CreationTimestamp
    @Column(nullable = false, updatable = false)
    val createdAt: LocalDateTime? = null

Jackson 라이브러리에서 제공되는 @JsonSerialize , @JsonDeserialize 어노테이션을 통해 직렬화/역직렬화 시 사용될 클래스를 직접 지정해줄 수 있다.
LocalDate 타입의 경우 LocalDateSerializer 를 사용하면 된다.

만약 이 방법으로 해결되지 않는다면 @JsonProperty("필드명") 어노테이션을 추가로 붙여주어 key 를 명확히 맵핑해주면 해결되는 경우도 있다.

0개의 댓글