[Spring] Redis 캐시에서 객체를 꺼낼 때 타입 오류 발생

2025. 5. 22. 20:51·프로젝트/트러블슈팅

intro

  • 프로젝트를 진행하며 발생한 문제 상황과 해결 과정들을 상세히 기록하고 추후에 같은 문제가 발생 했을때 빠르게 문제 해결하기 위해 트러블 슈팅을 정리할려고 한다.
  • 기록하는 습관을 기르기 위해 프로젝트 기간동안 꾸준히 작성할 것 이다.

프로젝트를 진행하면서 Redis 캐시에 객체를 저장하고 다시 조회하는 과정에서 ClassCastException이 발생했다.

처음엔 단순한 타입 문제로 보였지만, 내부 원인을 파악하고 직렬화/역직렬화 개념을 명확히 이해하면서 해결할 수 있었다. 

 

⚠️ 1. 문제 상황 발생

 

Redis를 통해 자주 조회되는 상품 정보를 캐싱하고, 캐시에 있으면 DB 조회 없이 바로 반환하는 로직을 구현했다. 그런데 아래 코드에서 문제가 발생했다.

public ProductResponseDto getProduct2(Long productId) {
    String cacheKey = "product:" + productId;

    ProductRedisRequestDto cachedObject = (ProductRedisRequestDto) redisTemplate.opsForValue().get(cacheKey);

    if (cachedObject != null) {
        increaseViewCount(productId);
        return new ProductResponseDto(cachedObject, getViewCount(productId));
    }

    ...
}
 

 

redisTemplate.opsForValue().get(cacheKey) 부분에서 ClassCastException 발생했다.
Object 타입으로 꺼낸 후 ProductRedisRequestDto로 캐스팅 할려했지만 실패!!

 

🔍 2. 원인 분석

❌ RedisTemplate 설정 문제

RedisConfig에서 RedisTemplate의 직렬화 설정을 확인했다.

GenericJackson2JsonRedisSerializer를 사용해 값을 JSON으로 직렬화하고 있었다.

 

// 직렬화 설정
GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(objectMapper);
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
template.setHashValueSerializer(serializer);

 

❌ Redis 데이터 손상

Redis에 저장된 데이터가 손상되었거나 예상과 다른 형식이 아닌지 확인해보았다.

Redis CLI로 GET product:1을 실행해 데이터를 확인한 결과, JSON 형식의 문자열이 정상적으로 저장되어 있다.

 

 

⭕ 역직렬화된 객체의 타입 문제

GenericJackson2JsonRedisSerializer는 Redis에서 데이터를 가져올 때 JSON을 LinkedHashMap으로 역직렬화한다.

따라서 redisTemplate.opsForValue().get(cacheKey)는 ProductRedisRequestDto가 아닌 LinkedHashMap을 반환한다.

이를 직접 ProductRedisRequestDto로 캐스팅하려 했기 때문에 ClassCastException이 발생한 것이다.

 

 

💡 왜 LinkedHashMap으로 역직렬화되지?
GenericJackson2JsonRedisSerializer는 JSON 데이터를 Java 객체로 변환할 때, 타입 정보를 명시적으로 유지하지 않으면 기본적으로 LinkedHashMap으로 변환합니다. 이는 Jackson의 기본 동작이다.

📦 RedisTemplate의 직렬화 동작

RedisConfig.java 전체코드

더보기
package com.example.plusteamproject.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

    @Value("${spring.data.redis.host}")
    private String host;

    @Value("${spring.data.redis.port}")
    private int port;


    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        StringRedisSerializer stringSerializer = new StringRedisSerializer();
        GenericJackson2JsonRedisSerializer jsonSerializer = createJsonSerializer();

        template.setKeySerializer(stringSerializer);
        template.setHashKeySerializer(stringSerializer);
        template.setValueSerializer(jsonSerializer);
        template.setHashValueSerializer(jsonSerializer);

        return template;
    }

    private GenericJackson2JsonRedisSerializer createJsonSerializer() {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.registerModule(new JavaTimeModule());
        return new GenericJackson2JsonRedisSerializer(objectMapper);
    }


    //spring boot 2.x 버전 이상은 자동으로 주입됨
//    @Bean
//    public RedisConnectionFactory redisConnectionFactory() {
//        return new LettuceConnectionFactory(host, port);
//    }
//
//    @Bean
//    public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
//        RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
//        redisTemplate.setConnectionFactory(redisConnectionFactory);
//        redisTemplate.setDefaultSerializer(new StringRedisSerializer());
//        return redisTemplate;}


}
 
 
template.setValueSerializer(new GenericJackson2JsonRedisSerializer(objectMapper));

 

라는 코드를 살펴보자면,

  • 저장 시: 자바 객체 → JSON 문자열로 변환
  • 조회 시: JSON 문자열 → LinkedHashMap 으로 역직렬화됨

 

즉, .get()으로 가져온 데이터는 실제로는 LinkedHashMap 객체다.
그래서 ProductRedisRequestDto로 바로 캐스팅하면 ClassCastException이 나는 것이다.

 

📝 3. 해결 방안

✅ ObjectMapper를 이용한 안전한 변환

 

  • Jackson의 ObjectMapper.convertValue()를 사용하여 LinkedHashMap을 원하는 DTO로 변환했다.
Object cachedObject = redisTemplate.opsForValue().get(cacheKey);

if (cachedObject != null) {
// LinkedHashMap을 ProductRedisRequestDto로 변환
ProductRedisRequestDto cachedDto = 
          objectMapper.convertValue(cachedObject, ProductRedisRequestDto.class);

 

다른방법도 존재하긴한다 더 쉬운방법이긴한데 소규모 프로젝트를 진행할때 사용해야할 방법이다.

바로 

RedisTemplate<String, ProductRedisRequestDto> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);

 

RedisTemplate 를 생성할때 Object 대신 Dto를 바로 넣어주면 저장시에도 Dto 로 가지고 있기때문에 굳이 Object 로 꺼내지 않고 바로 사용할 수 있다! 

 

그렇지만!

✅왜 RedisTemplate<String, Object>로 설정했는가?

  • RedisTemplate<String, ProductRedisRequestDto>로 특정 타입을 설정하면 캐시의 범용성이 떨어진다.
  • 다양한 DTO를 Redis에 저장하려면 Object로 설정하는 것이 확장성 측면에서 유리하다.
  • 대신 역직렬화 시 정확한 타입으로 수동 변환이 필요하다 → convertValue() 사용

 

 📌 5. 결과 확인

  • ClassCastException이 사라졌고 Redis에 저장한 데이터를 정상적으로 ProductRedisRequestDto로 복원되었다는 뜻이다.

 

 

✒️회고

💡 정리

직렬화 자바 객체를 JSON 등으로 변환해 저장하는 과정
역직렬화 저장된 JSON을 다시 자바 객체로 복원하는 과정
RedisTemplate 기본적으로 Object 타입을 반환하므로, 직접 타입 캐스팅하면 위험
해결 방법 ObjectMapper.convertValue()로 안전하게 DTO로 변환

 

- 특히 RedisTemplate<String, Object>와 같이 범용적인 설정을 사용할 경우, 역직렬화에 주의를 기울여야 겠다.

- 처음엔 단순한 타입 오류로 보였지만, 그 이면엔 직렬화 방식과 동작 원리에 대한 이해가 부족했다.

- 이번 경험을 통해 JSON 직렬화 방식과 Jackson의 convertValue() 활용법을 익힐 수 있었다.

'프로젝트 > 트러블슈팅' 카테고리의 다른 글

[트러블슈팅] Redisson 분산락과 트랜잭션 적용 시점 충돌(AOP 를 활용한 해결)  (1) 2025.06.18
[Spring]단위 테스트 Mockito willReturn(Optional<T>) 오류 해결  (1) 2025.05.19
[QueryDSL] Hibernate SemanticException과 fetchJoin() 트러블슈팅  (0) 2025.05.13
[Spring]무한 스크롤 + Enum 정렬 트러블슈팅  (2) 2025.04.23
[Spring/Security] 403 Forbidden? 권한 문제가 아니라 CSRF 이 원인이였다  (0) 2025.04.21
'프로젝트/트러블슈팅' 카테고리의 다른 글
  • [트러블슈팅] Redisson 분산락과 트랜잭션 적용 시점 충돌(AOP 를 활용한 해결)
  • [Spring]단위 테스트 Mockito willReturn(Optional<T>) 오류 해결
  • [QueryDSL] Hibernate SemanticException과 fetchJoin() 트러블슈팅
  • [Spring]무한 스크롤 + Enum 정렬 트러블슈팅
코딩로봇
코딩로봇
금융 IT 개발자
  • 코딩로봇
    쟈니의 일지
    코딩로봇
  • 전체
    오늘
    어제
    • 분류 전체보기 (151) N
      • JavaScript (8)
      • SQL (11)
      • 코딩테스트 (30)
        • Java (15)
        • SQL (13)
      • Java (10)
      • 프로젝트 (30) N
        • 트러블슈팅 (10)
        • 프로젝트 회고 (18) N
      • git,Github (2)
      • TIL (38)
      • Spring (20)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    스파르타 코딩 #부트캠프 #첫ot
    java #arraylist #list #배열
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
코딩로봇
[Spring] Redis 캐시에서 객체를 꺼낼 때 타입 오류 발생
상단으로

티스토리툴바