반응형

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 valueInteger를 원했기에 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으로 변경한다.

단점은 두 가지가 있다.

  1. 명시적으로 String으로 되어있기 때문에 사용자는 착각할 가능성이 크다.
  2. 제네릭을 활용하지 못한 것이다. 즉, 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

  1. 설정할 때 생성자로 넘겼다. Integer.class를 필드로 갖는다.
  2. convert()를 하는데 해당 클래스 타입(Integer.class)로 변환을 한다.
    StringRedisSerializer와는 다르게 convert()를 실행한다.
    (이후 컨버팅이 되면, 클래스 타입인 T 타입(Integer)로 반환을 하게 된다.)
  3. conver()는 넘어온 클래스타입(Integer)로 컨버팅을 하고 해당 타입으로 반환한다.

이와 같은 코드가 내부적으로 돌기 때문에 해결할 수 있는 것이다.

해결 방법 1의 단점들 때문에 이 방법을 채택했다.

해결 방법 3

value에는 아무런 설정을 하지않으면 어떻게 될까? 의문이 들었다.
당연히 기본 구현체는 JdkSerializationRedisSerializer이다.
내부적으로 들어가다 보면, DefaultDeserializer가 나오는데 70번 째 라인에서 EOFException을 발생시킨다.

그래서 예외 처리를 해주면 될 것 같은데 해결 방법2가 있기 때문에 고려하지 않았다.

EOF란?

반응형
복사했습니다!