이전 게시물에서 직렬화/역직렬화 과정에서 문제가 생겨 redis에서의 조회가 안되는 상황이 벌어졌다.

이유가 뭘까?

앞서 value값을 직렬화하는데 GenericJackson2JsonRedisSerializer클래스를 사용했고, 이 클래스는 객체의 클래스 지정 없이 모든 Class Type을 JSON 형태로 저장할 수 있다고 했었다.

하지만 날짜 타입에 대해서는 default로 지원이 안되는 것으로 파악됐다.

ObjectMapper를 커스텀하여 GenericJackson2JsonRedisSerializer 클래스와 같은 Serializer에게 전달하면 된다는 것은 찾았지만 관련 자료들마다 방법이 다른데다가 적용이 안되는 문제가 있어 해결하는데 쉽지 않았다.

이를 위한 시도로 3가지의 과정을 거쳤고, 결국 성공했다.

하나씩 살펴보도록 하자.

추가적으로, 우리가 캐싱을 통해 조회할 부분은 아래와 같이 상품 단건 조회 부분이다.

@Transactional(readOnly = true)
@Cacheable(value = "products", key ="#id")
public ProductInfo getProductInfo(Long id) {
    return productRepository.findById(id)
            .orElseThrow(() -> new ShoeKreamException(ErrorCode.PRODUCT_NOT_FOUND))
            .toProductInfo();
}

0. jackson 의존성 추가


🚫 오류 메세지에 따라 의존성을 추가해줬으나 실패

implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.14.2'


1. MapperConfig 작성


ObjectMapper 커스텀을 위해 MapperConfig 클래스를 만들었다.

그리고 커스텀 ObjectMapper Bean을 생성하여 그 안에 날짜 타입 변환을 위한 과정을 추가해보았다.

우선, JavaTimeModule을 불러와서 addSerializer()addDeserializer()를 통해 직렬화와 역직렬화를 위한 커스텀 클래스(LocalDateTimeSerializer,LocalDateTimeDeserializer)를 넣어준다.

다음으로, objectMapper.registerModules(javaTimeModule, new Jdk8Module()) 통해 방금 전에 작성한 javaTimeModule을 등록하여 Jackson이 LocalDate, LocalDateTime등을 매핑할 수 있도록 해준다.

마지막으로, objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)을 통해선 Data를 TimeStamp 형식으로 직렬화하지 못하게 하고 반환해주면 된다.

@Configuration
public class MapperConfig {

    public static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    @Bean
    public ObjectMapper serializingObjectMapper() {
        ObjectMapper objectMapper = new ObjectMapper();
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer());
        javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer());
        objectMapper.registerModules(javaTimeModule);
        objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
        return objectMapper;
    }

    public class LocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {

        @Override
        public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
            gen.writeString(value.format(FORMATTER));
        }
    }

    public class LocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {

        @Override
        public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
            return LocalDateTime.parse(p.getValueAsString(), FORMATTER);
        }
    }
}


그리고, CacheConfig에서 ObjectMapper를 주입해주면 된다.

이전 게시글과 비교했을 때, 의존성 주입 부분과 GenericJackson2JsonRedisSerializer클래스 파라미터로 objectMapper를 주입하는 것이 추가되었다.

@Configuration
public class CacheConfig {

    @Autowired
    ObjectMapper objectMapper;

    private RedisCacheConfiguration defaultCacheConfiguration() {
    return RedisCacheConfiguration
            .defaultCacheConfig()
            .disableCachingNullValues()
            .serializeKeysWith(fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(fromSerializer(new GenericJackson2JsonRedisSerializer(objectMapper)))
            .entryTtl(Duration.ofDays(1L));
    }
}


🚫 이랬더니 웬걸 날짜 타입은 해결되고, 이번엔 다른 에러가 발생했다. ArrayList를 직렬화할 수 없다고 한다…

image


2. CacheConfig 내에서 Custom ObjectMapper Bean 생성


다음 방법으로, MapperConfig없이 CacheConfig내에서 간단하게 작성하는 방식을 시도해보았다.

@Bean
public ObjectMapper objectMapper() {

    return new ObjectMapper()
            .findAndRegisterModules()
            .enable(SerializationFeature.INDENT_OUTPUT)
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
            .registerModule(new JavaTimeModule());
}

하나씩 살펴보면,

.findAndRegisterModules()에서 ObjectMapper 클래스의 메서드로 JDK ServiceLoader에 의해 기본적으로 제공되는 모듈들을 찾아 넣어준다.

.enable(SerializationFeature.INDENT_OUTPUT)에선 JSON 형태로 저장하거나 출력할 때 인덴트를 맞춰서 formatting 해주도록 한다.

.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)에선 앞서 사용한 것처럼 Date를 TimeStamp 형식으로 직렬화하지 못하게 하고,

.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)에선 역직렬화하는 대상에 모르는 속성(필드) 이 있더라도 역직렬화를 수행하라는 의미에서 속성값에 false를 넣어준다.

마지막으로 .registerModules(new JavaTimeModule())에서 JavaTimeModule을 넣어주어 LocalDateTime 직렬화/역직렬화가 가능하도록 해주고 반환해주면 된다.

작성이 끝나면 GenericJackson2JsonRedisSerializer클래스 파라미터로 objectMapper()를 주입하는 것만 추가해주면 된다.

@Configuration
public class CacheConfig {

    private RedisCacheConfiguration defaultCacheConfiguration() {
    return RedisCacheConfiguration
            .defaultCacheConfig()
            .disableCachingNullValues()
            .serializeKeysWith(fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(fromSerializer(new GenericJackson2JsonRedisSerializer(objectMapper())))
            .entryTtl(Duration.ofDays(1L));
    }


🚫 여전히 ArrayList를 직렬화 할 수 없다고 한다…

image


3. Bean 생성 없이 CacheConfig 내에서 Custom ObjectMapper 메서드 작성


그래서 이번엔, Bean 생성 없이 Custom ObjectMapper 메서드를 작성해봤다.

방법은 크게 다르지 않다.

ObjectMapper 클래스에 JavaTimeModule을 등록하고, GenericJackson2JsonRedisSerializer 클래스 파라미터로 해당 ObjectMapper를 넘기는 작업이다.

@Configuration
public class CacheConfig {

    private RedisCacheConfiguration defaultCacheConfiguration() {
    return RedisCacheConfiguration
            .defaultCacheConfig()
            .disableCachingNullValues()
            .serializeKeysWith(fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(fromSerializer(new GenericJackson2JsonRedisSerializer(objectMapper())))
            .entryTtl(Duration.ofDays(1L));
    }

    private ObjectMapper objectMapper() {

        PolymorphicTypeValidator typeValidator = BasicPolymorphicTypeValidator.builder()
                .allowIfSubType(Object.class)
                .build();

        return new ObjectMapper()
                .enable(SerializationFeature.INDENT_OUTPUT)
                .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
                .registerModule(new JavaTimeModule())
                .activateDefaultTyping(typeValidator, ObjectMapper.DefaultTyping.NON_FINAL);
    }


✅ 드디어 성공했다…

image


id가 3인 상품을 최초에 조회하면 아래와 같이 쿼리문이 나오면서 redis에 우리가 설정한 캐싱 value/key 값으로 저장이 된다.

image

image


그리고 그 다음번 조회 부터는 @Cacheable 어노테이션에서 만약 value/key와 일치하는 데이터가 있다면 redis에서 바로 조회해온다.

조회속도를 비교해보면 1048ms -> 126ms -> 8ms로 대폭 감소했음을 알 수 있다.

image

image

image


4. 마무리


메서드 내용은 같고 Bean을 만들어서 넣어주는 것이 왜 안되는지는 아직 명확하게 파악하지 못했다.

Bean 생성주기나 이런 것들을 조금 더 공부해봐야 될 것 같고, ObjectMapper 클래스에 대해서도 조금 더 살펴봐야 할 것 같다.

작성한 방법은 3가지이지만 실제로는 무수히 많은 시도를 했었다.

하지만 단순히 Bean 생성 여부에 따라 결과가 달라지니 이유를 파악해야 할 의미가 생겼다.

후에 있을 게시글에서 정리해보도록 하겠다.

References

Leave a comment