
Redis 의 데이터 저장 방식
- Redis 는 모든 데이터를 바이트 배열로 저장한다.
- 키와 값 모두 바이트 배열로 저장되며, 이는 Redis 의 데이터 모델이 이진 안전(Binary Safe) 하기 때문이다.
- 즉 Redis 는 문자열 뿐만 아니라, 이진 데이터(예: 이미지, 암호화된 데이터 등)도 키와 값으로 저장할 수 있다.
경우에 따른 UTF-8 인코딩
- Redis 는 키와 값의 데이터를 인코딩하지 않지만, CLI 및 클라이언트 라이브러리에서 일반적으로 UTF-8 문자열로 데이터를 처리한다 -> CLI 에서 키와 값을 UTF-8 문자열로 볼 수 있는 이유
- CLI 에서 키나 값을 조회할 때, 기본적으로 UTF-8 문자열로 디코딩하여 표시된다.
Redis 에서는 키(Key) 와 값(Value) 모두 직렬화/역직렬화 하는 과정이 필요하다.
- 키(Key) 는 사람이 읽을 수 있는 문자열 형태가 일반적이므로, StringRedisSerializer(문자열->UTF-8 바이트 배열)로 처리한다.
- 값(Value)은 객체나 복잡한 데이터를 저장하기 때문에, GenericJackson2JsonRedisSerializer(객체->JSON 문자열 -> UTF-8 바이트 배열) 와 같은 JSON 직렬화기가 필요하다.
Redis
공통점
최종적으로 바이트 배열로 변환
- 두 직렬화기 모두 데이터를 바이트 배열로 변환하여 Redis 에 저장한다.
- Redis 는 모든 데이터를 바이트 배열로 저장하므로, 이 과정은 필수적이다.
StringRedisSerial
주요 역할
Redis 에 저장되는 키(Key)를 UTF-8 문자열로 직렬화/역직렬화
1. Redis 키를 UTF-8 문자열로 직렬화
( 키(String) -> UTF-8 문자열 -> 바이트 배열 )
- 사용자가 정의한 키(예: myCache::userId)를 UTF-8 인코딩
- UTF-8 로 인코딩된 문자열을 바이트 배열로 변환
- 바이트 배열이 Redis 서버에 저장됨
2. Redis 에 저장된 바이트 배열 키를 UTF-8 문자열로 역직렬화
( 바이트 배열 -> UTF-8 문자열 -> 키(String) )
- Redis 에 저장된 바이트 배열 데이터를 읽어서
- 바이트 배열을 UTF--8 문자열로 디코딩하여 키를 복원
GenericJackson2JsonRedisSerializer
주요 역할
Redis 에 저장되는 값(Value)를 JSON 형태로 직렬화/역직렬화
기본 동작
GenericJackson2JsonRedisSerializer 은 Spring Data Redis 에서 제공하는 직렬화기로, 데이터를 JSON 형태로 저장하며 다음과 같은 기능을 제공한다.
1. 타입 정보 포함
- 기본적으로 Object 하위 타입의 객체를 직렬화할 때, 타입 정보를 JSON 에 추가하여 저장한다.
- 이를 통해 Jackson 직렬화/역직렬화 과정에서 다형성(Polymorphism)을 지원한다.
2. 기본 타입 지원
- 직렬화된 데이터가 String, Integer, Double 과 같은 단순 타입일 경우, Jackson 은 별도의 타입 정보 없이 JSON 데이터로 저장한다.
- 이러한 경우 역직렬화 시, 타입 정보가 없어도 Jackson 이 자동으로 처리한다.
3. 제네릭 타입 처리
- List<String> 과 같은 제네릭 타입의 경우, 타입 정보가 함께 저장되지 않지만, 타입 정보 없이도 JSON 구조만으로 Jackson 이 정확히 처리할 수 있다.
- 그러나 List<Object> (예: ["523", 523, {"id": "Alice"}] )와 같이 다형성이 필요한 제네릭 타입에서는 타입 정보가 없으면 데이터 복원이 제대로 이루어지지 않을 수 있다.
타입 정보 누락 문제와 해결
문제
GenericJackson2JsonRedisSerializer 은 List<Object> 과 같은 다형성이 필요한 제네릭 타입에 대해 구체적인 타입 정보를 저장하지 않으므로 역직렬화 시, 정확한 타입으로 복원되지 않을 수 있다.
예를 들어 JSON 데이터가 ["523", 523, {"id": "Alice"}] 와 같이 저장되면, 역직렬화 시 List<Object> 로 처리하려고 시도하기 때문에 타입 변환(ClassCastException) 문제가 발생할 수 있다.
따라서 역직렬화 시 원래 타입으로 복원하기 위해서는 저장하는 값들에 대한 타입 정보를 포함시켜야 한다.
타입 정보 포함은 다음과 같이 역직렬화 시 타입 정보를 포함하지 않아도 되는 기본타입과 단순 컬렉션 등을 포함하여, 사용자 정의 클래스에 대해 타입 정보를 포함하도록 설정해줘야 한다.
그러기 위해서는 ObjectMapper.DefaultTyping.EVERYTHING 을 설정하여 Redis 에 저장되는 모든 객체에 타입 정보를 포함시키도록 한다.
또한, 이렇게 Redis 에 타입 정보를 포함시키더라도, 추가적으로 역직렬화 시 허용할 타입을 지정해줘야 한다. 기본적으로 Redis 에서는 사용자 정의 클래스에 대해 역직렬화를 허용하지 않는다. 이를 해결하기 위해 .allowIfSubType("com.example") 을 설정하여 사용자 정의 클래스 타입으로 역직렬화가 가능하도록 해줘야 한다.
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.activateDefaultTyping(
BasicPolymorphicTypeValidator.builder()
.allowIfSubType("java.lang") // 기본 타입(String, Integer 등) 허용
.allowIfSubType("java.util") // 컬렉션(List, Map 등) 허용
.allowIfSubType("com.example") // 사용자 정의 클래스 허용
.build(),
ObjectMapper.DefaultTyping.EVERYTHING // 모든 객체에 타입 정보 추가
);
전체 코드
@Configuration
@EnableRedisRepositories
public class RedisConfig {
@Value("${spring.data.redis.host}")
private String host;
@Value("${spring.data.redis.port}")
private int port;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
ObjectMapper objectMapper = new ObjectMapper();
// 타입 정보를 활성화
objectMapper.activateDefaultTyping(
BasicPolymorphicTypeValidator.builder()
.allowIfSubType("java.util") // 컬렉션 타입 허용
.allowIfSubType("java.lang") // 기본 타입 허용
.allowIfSubType("com.example") // 사용자 정의 클래스 허용
.build(),
DefaultTyping.EVERYTHING // 모든 객체에 타입 정보 추가
);
// GenericJackson2JsonRedisSerializer에 ObjectMapper 설정
GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(objectMapper);
RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer))
.entryTtl(Duration.ofHours(1L));
return RedisCacheManager
.RedisCacheManagerBuilder
.fromConnectionFactory(factory)
.cacheDefaults(cacheConfig)
.build();
}
}
