Redis 캐싱 직렬화 실패 해결 과정 (LocalDateTime 대응)

에러 발생과 원인

통계 정보를 캐싱하는 과정에서 다음과 에러가 발생했습니다.

Java 8 date/time type java.time.LocalDateTime not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310"

 

번역기를 돌려보면  "Java 8 날짜/시간 유형 java.time.LocalDateTime은 기본적으로 지원되지 않습니다. 모듈 "com.fasterxml.jackson.datatype:jackson-datatype-jsr310"을 추가하세요." 라고 해석이 된다.

 

또한, 에러 로그를 보면 아래와 같은 내용도 있습니다.

at com.fasterxml.jackson.databind.ObjectMapper.writeValueAsBytes → at org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer.serialize

 

이러한 흐름으로 보아, LocalDateTime을 Redis에 저장하려는 과정에서 Jackson이 해당 타입을 직렬화하지 못해 오류가 발생한 것으로 추측할 수 있습니다.

즉, 캐시에 저장하려던 객체에는 LocalDateTime 타입이 있는데, Jackson이 이걸 변환할 수 있는 설정이 없어서 에러가 난 것이다.

해결

해결 방법은 에러 메시지에서 힌트를 줬듯이, 먼저 build.gradle 파일에 관련 의존성을 추가하고, LocalDateTime 같은 날짜/시간 타입을 Jackson이 인식하고 처리할 수 있도록 Java Time 모듈을 등록해줘야 합니다.

 

하지만 현재 프로젝트에서는 Redis 캐싱 대상이 아직 BoardStats 하나뿐이기 때문에, 굳이 ObjectMapper 설정을 추가하지 않고도 날짜 타입을 안전하게 처리할 수 있는 방법이 있지 않을까? 라는 고민을 하였고, 그 고민을 기반으로 아래와 같이 해결하였습니다.

 

Redis에는 LocalDateTime을 String으로 변환한 DTO 객체인 BoardStatsCacheDto를 저장하고,
서비스에서는 기존 도메인 객체인 BoardStats를 반환하도록 구성했습니다.

    public Map<String, BoardStats> getWithFallback(
            List<String> qnaIds,
            Function<List<String>, List<BoardStats>> fallback
    ) {
        List<String> redisKeys = qnaIds.stream()
                .map(id -> KEY_PREFIX + id)
                .toList();

        List<Object> cachedResults = redisTemplate.opsForValue().multiGet(redisKeys);

        Map<String, BoardStats> resultMap = new HashMap<>();
        List<String> missedQnaIds = new ArrayList<>();

        for (int i = 0; i < qnaIds.size(); i++) {
            Object cached = cachedResults.get(i);
            String qnaId = qnaIds.get(i);
            if (cached instanceof BoardStatsCacheDto statsCacheDto) {
                resultMap.put(qnaId, statsCacheDto.toModel());
            } else {
                missedQnaIds.add(qnaId);
            }
        }

        if (!missedQnaIds.isEmpty()) {
            List<BoardStats> fallbackStats = fallback.apply(missedQnaIds);
            for (BoardStats stats : fallbackStats) {
                String key = KEY_PREFIX + stats.getQnaId();
                BoardStatsCacheDto statsCacheDto = BoardStatsCacheDto.from(stats);
                redisTemplate.opsForValue().set(key, statsCacheDto, CACHE_TTL, TimeUnit.SECONDS);
                resultMap.put(stats.getQnaId(), stats);
            }
        }

        return resultMap;
    }

 

여기서 BoardStatsCacheDto.from(stats)를 통해 BoardStats의 시간 관련 필드를 String으로 변환한 후 Redis에 저장합니다.
반대로 Redis에서 조회한 값은 BoardStatsCacheDto.toModel()을 통해 다시 도메인 객체인 BoardStats로 변환합니다.

 

Redis에는 DTO를 저장하고, 서비스에는 도메인 객체를 반환하는 이유

서비스에 BoardStats를 반환하는 이유는, 이후 통계 필드를 MongoDB에 flush해야 할 때 타입이 정확히 맞아야 하기 때문입니다.

만약 BoardStatsCacheDto를 그대로 반환하게 되면, lastUpdated 같은 필드는 String 타입이라 시간 비교나 날짜 계산을 하려 할 때 LocalDateTime이 아니어서 오류가 발생할 수 있습니다.

또, 서비스에서는 BoardStats 객체를 기준으로 동작하기 때문에 기존 코드 변경 없이 처리할 수 있다는 장점이 있습니다.