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 |