![article thumbnail image](https://blog.kakaocdn.net/dn/bsPQE2/btrBT3mHeQ4/deN46Cvt2Datt1ihKFU2P1/img.png)
RedisTemplate 설정 시 직렬화 역직렬화 문제
RedisTemplate 설정을 하고 Redis에 저장을 해보면, byte코드로 저장이되는 현상이있다.
key가 저장되는데 저렇게 저장되니 cli에서 값을 조회를 할 수가 없었다. 아주 불편한 사항이다.
(아래 사진과 같이)
문제1 - 직렬화
key가 사람이 알기 어려운 문자로 저장되어 있다.
먼저 직렬화 역직렬화 인터페이스는 `RedisSerializer
`이다. 알아만 놓자.
Spring Data Redis는 기본 직렬화 구현체가 JdkSerializationRedisSerializer
이다.
여기서 직렬화하는 내부 로직을 살펴보면, conver()가 있다. 여기를 살펴보자.
JdkSerializationRedisSerializer.java
SerializingConverter.java
serializeToByteArray
를 살펴보자.
Serializer.java(interface)
인터페이스인데 default 메서드에서 직렬화를 하고있다.
이러한 이유로 비정상적으로 값이 들어간 것 같다.
어떻게 해결할까?(StringRedisSerializer.java)
아주 간단하다. StringRedisSerializer
로구현체만 변경해주면 된다.
StringRedisSerializer.java
getBytes()를 더 뜯어보자.
뭔지는 모르겠지만, StringCoding.encode()
를 하고있다.
StringEncoding는 문자열 인코딩 및 디코딩에 대한 유틸리티 클래스이다.
즉, 정상적으로 String을 인코딩 해주는 클래스였기 때문에 구현체를 변경해서 해결했다.
해결한 설정 코드
@Configuration
public class RedisConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}
@Bean
public RedisTemplate<String, ?> redisTemplate() {
RedisTemplate<String, ?> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
}
문제2 - 역직렬화
RedisTemplate에서 값을 Integer로 가져오길 원했다.
처음에는 키를 직렬화했던 것과 동일하게 StringRedisSerializer
를 vlaue에도 동일하게 설정했다.
하지만 ClassCastException
이 발생했다.
@Bean
public RedisTemplate<String, ?> redisTemplate() {
RedisTemplate<String, ?> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new StringRedisSerializer());
return redisTemplate;
}
먼저 원인만 간단히 하자면 deserialize()가 String을 반환하고 있기 때문이었다.
StringRedisSerializer.java
그러면 String을 반환한다고 왜 Casting을 못할까?
먼저 나는 조회해오기 위해 아래와 같은 코드를 사용했다.
hash자료구조를 사용했고, hash value
는 Integer
를 원했기에 Integer로 선언했다.
@Override
public Integer getHits(Long postId) {
HashOperations<String, String, Integer> hashOperations = redisTemplate.opsForHash();
String key = "posts:" + postId;
String hashKey = "hits";
return hashOperations.get(key, hashKey) == null ? null : Integer.parseInt(hashOperations.get(key, hashKey));
}
DefaultHashOperations.java
이 클래스는 HashOperations
의 구현체이다. 조회하는 get()을 보면, null이 아닐 경우 deserializeHashValue()
을 호출한다.
그리고 (HV)로 변환하는데 HV는 위에 코드에서 선언한 Integer인 hash value이다.
AbstractOperations.java
deserializeHashValue()
을 구현한 클래스이다.
비슷하게 (HV)로 변환하고 hashValueSerializer().deserialize(*value*)
를 호출한다.deserialize()
는 StringRedisSerializer.java
가 제공하는 것이다. StringRedisSerializer
을 value의 구현체로 셋팅했기 때문에
해당 클래스를 구현체로 사용하는 것이다.
StringRedisSerializer.java
강제로 new String()
을 하여 String
으로 반환하고 있다.
결국 HV
가 의미가 없어지는 것이 원인이었다.
즉, String으로 반환된 것을 직접 작성한 getHits
에서 Integer로 반환하려고 하니까 예외가 발생한 것이다.
해결 방법 1
제네릭 타입을 String으로 변경한다.
단점은 두 가지가 있다.
- 명시적으로 String으로 되어있기 때문에 사용자는 착각할 가능성이 크다.
- 제네릭을 활용하지 못한 것이다. 즉, String으로 타협한 것이다.
@Override
public Integer getHits(Long postId) {
HashOperations<String, String, String> hashOperations = redisTemplate.opsForHash();
String key = "posts:" + postId;
String hashKey = "hits";
return hashOperations.get(key, hashKey) == null ? null : Integer.parserInt(hashOperations.get(key, hashKey));
}
해결 방법 2
채택한 방법이다.
구현체를 StringRedisSerializer
에서 GenericToStringSerializer
로 변경한다.
@Bean
public RedisTemplate<String, ?> redisTemplate() {
RedisTemplate<String, ?> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));
return redisTemplate;
}
어떻게 해결이 가능한지 알아보자.
GenericToStringSerializer.java
- 설정할 때 생성자로 넘겼다. Integer.class를 필드로 갖는다.
- convert()를 하는데 해당 클래스 타입(Integer.class)로 변환을 한다.
StringRedisSerializer
와는 다르게convert()
를 실행한다.
(이후 컨버팅이 되면, 클래스 타입인 T 타입(Integer)로 반환을 하게 된다.) - conver()는 넘어온 클래스타입(Integer)로 컨버팅을 하고 해당 타입으로 반환한다.
이와 같은 코드가 내부적으로 돌기 때문에 해결할 수 있는 것이다.
해결 방법 1의 단점들 때문에 이 방법을 채택했다.
해결 방법 3
value에는 아무런 설정을 하지않으면 어떻게 될까? 의문이 들었다.
당연히 기본 구현체는 JdkSerializationRedisSerializer
이다.
내부적으로 들어가다 보면, DefaultDeserializer
가 나오는데 70번 째 라인에서 EOFException
을 발생시킨다.
그래서 예외 처리를 해주면 될 것 같은데 해결 방법2가 있기 때문에 고려하지 않았다.
'기타 IT' 카테고리의 다른 글
인프콘 2022 나의 첫 컨퍼런스! 후기 (4) | 2022.08.27 |
---|---|
데드락이란? (0) | 2022.07.05 |
Spring Data Redis에서 O(N) 명령어인 Keys를 Scan으로 대체하기 (0) | 2022.05.11 |
Redis는 언제 사용해야 할까? (0) | 2022.05.11 |
테스트 더블 테스트 VS 실제 객체 테스트(classicist vs mockist) (0) | 2022.04.15 |