Spring RedisTemplate Serializer 설정

김현교·2022년 11월 9일
4

1. 개요


Spring Data Redis의존성을 추가하면 RedisTemplate 객체를 이용하여 Redis를 사용할 수 있다. 이때 데이터를 직렬화 / 역직렬화 하여 저장 / 조회를 하므로 적절한 직렬화 방식을 설정해주어야 한다. 스프링에서는 RedisTemplate Bean 생성 시 직렬화 구현체를 설정할 수 있다.

@Bean
public RedisTemplate<?, ?> redisTemplate() {
    RedisTemplate<?, ?> redisTemplate = new RedisTemplate<>();
    redisTemplate.setConnectionFactory(redisConnectionFactory());
    redisTemplate.setEnableTransactionSupport(true);

    redisTemplate.setKeySerializer(new JdkSerializationRedisSerializer());
    redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());

    redisTemplate.setHashKeySerializer(new Jackson2JsonRedisSerializer());
    redisTemplate.setHashValueSerializer(new StringRedisSerializer());
    return redisTemplate;
}

redis-cli로 저장된 Value값을 확인해보면, 각 구현체마다 저장되는 값이 다른 것을 확인할 수 있을 것이다. 그러면 각각의 구현체 별로 어떤 특징이 있는지 확인해보자.

2. 직렬화 구현체 종류

JdkSerializationRedisSerializer (Default)

디폴트로 적용되는 Serializer로, 기본 자바 직렬화 방식을 사용한다.

자바 직렬화는 java.io.Serializable 인터페이스만 구현하면 별도의 작업 없이 간편하게 사용 가능하다는 장점이 있지만, DB 등에 장기간 저장하는 정보에 사용하기에는 여러 단점들이 존재한다.

  • 클래스 구조 변경 시 역직렬화 문제
    1. serialVersionUID 설정을 하지 않으면 클래스의 기본 해쉬값을 serialVersionUID 로 사용한다. 따라서 클래스 구조가 조금이라도 바뀌면 serialVersionUID 값이 달라서 기존 저장 데이터의 역직렬화에 실패한다.

    2. serialVersionUID 를 설정한다 해도, 클래스 내부 필드의 타입이 변경되면 역직렬화 시 예외가 발생한다. (java.io.InvalidClassException) (필드의 추가, 삭제는 예외가 발생하지 않는다.)

      따라서 자바 직렬화는 자주 변경되는 클래스의 객체에는 지양하는 것이 좋다. 사소한 변경에도 생각지 못한 예외사항들이 발생할 가능성이 높다. (역직렬화 실패 시 예외처리를 꼭 구현하는 것을 추천한다.) 또한 자바 직렬화 기술은 중간에 끼어들 여지가 없기 때문에 변경에 취약하다.

  • 용량 문제
    자바 직렬화는 기본적으로 타입에 대한 정보 등 클래스 메타 정보들을 가지고 있기 때문에 다른 포맷에 비해 용량이 크다. 이렇게 직렬화된 데이터를 메모리 서버 등에 저장하는 용도로 활용하는 경우 이 용량 문제는 중요한 문제가 된다.

GenericJackson2JsonRedisSerializer

이 Serializer는 별도의 Class Type을 지정할 필요 없이 자동으로 Object를 Json으로 직렬화해주는 장점이 있다. 하지만 Object의 Class Type을 포함한 데이터를 저장하게 된다는 단점이 있다.

함께 저장되는 @class 필드에는 해당 Class의 패키지까지 함께 저장된다. 이게 단점인 이유는 어떤 Application이던 해당 데이터를 꺼내오기 위해서는 무조건 해당 루트, 경로에 같은 이름으로 해당 DTO Class를 생성해야만 사용이 가능해지기 때문이다.

MSA 프로젝트처럼 여러 Application이 상호작용하며 같은 데이터를 사용하는 프로젝트 구조를 가졌다면 해당 MSA API들이 해당 데이터의 Class Type에 묶인다는 문제가 발생한다.

Jackson2JsonRedisSerializer

이 Serializer는 @class 필드를 포함하지 않고 Json으로 저장해준다. 하지만 항상 ClassType을 Serializer에 함께 지정해주어야 한다. 즉, RedisTemplate 객체를 저장하는 DTO 타입 별로 생성해서 각각 Serializer의 ClassType을 지정해주어야 한다는 것이다.

@Bean
public RedisTemplate<?, ?> redisTemplate() {
    RedisTemplate<?, ?> redisTemplate = new RedisTemplate<>();
    redisTemplate.setConnectionFactory(redisConnectionFactory());
    redisTemplate.setEnableTransactionSupport(true);

    redisTemplate.setKeySerializer(new Jackson2JsonRedisSerializer(String.class));
    redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer(DTO.class));
    return redisTemplate;
}

만약 사용하는 Class Type의 종류가 많아진다면 이런 부분이 단점으로 작용할 수 있다.

StringRedisSerializer

이 Serializer는 이름처럼 String 값을 그대로 저장하는 Serializer이다. 따라서 객체를 Json형태로 변환하여 Redis에 저장하기 위해서는 직접 Encoding, Decoding을 해주어야 한다는 단점이 존재한다. 하지만, 위에서 설명한 Serializer의 단점들을 보완할 수 있는 방법이기도 하다.

  1. Class Type을 별도로 지정할 필요가 없다.
  2. Package정보를 포함하지 않을 수 있다.
  3. 용량을 최소화하여 저장할 수 있다.

따라서 이 Serializer를 사용하고 직접 Json Parser를 적용하는 방식으로 RedisTemplate을 사용하는 방식을 선택한다.

3. 적용 방법


RedisTemplate Bean 설정

@Bean
public RedisTemplate<?, ?> redisTemplate() {
    RedisTemplate<?, ?> redisTemplate = new RedisTemplate<>();
    redisTemplate.setConnectionFactory(redisConnectionFactory());
    redisTemplate.setEnableTransactionSupport(true);

    redisTemplate.setKeySerializer(new StringRedisSerializer());
    redisTemplate.setValueSerializer(new StringRedisSerializer());

    redisTemplate.setHashKeySerializer(new StringRedisSerializer());
    redisTemplate.setHashValueSerializer(new StringRedisSerializer());
    return redisTemplate;
}

RedisTemplate 사용

@Slf4j
@RequiredArgsConstructor
@Repository
public class GameRedisRepository {
  private final RedisTemplate<String, Object> redisTemplate;
  private final ObjectMapper objectMapper;

	private <T> Optional<T> getData(String key, Class<T> classType) {
      String jsonData = (String) redisTemplate.opsForValue().get(key);

      try {
          if (StringUtils.hasText(jsonData)) {
              return Optional.ofNullable(objectMapper.readValue(jsonData, classType));
          }
          return Optional.empty();
      } catch (JsonProcessingException e) {
          throw new RuntimeException(e);
      }
  }

}

참고자료

https://techblog.woowahan.com/2551/

https://lingi04.tistory.com/9?category=1026428

https://velog.io/@kshired/Spring-Redis에서-객체-캐싱하기

profile
백엔드 개발자로 취업 준비 중입니다.

0개의 댓글