본문 바로가기

개발/백엔드

Redis Serializer 비교

3줄요약

GenericJackson2JsonRedisSerializer 클래스 정보가 들어가는게 싫었다.

RedisSerializer를 구현한 CustomJsonRedisSerializer 를 사용했다.

성능도 괜찮고 사용도 편했다.

 

-----------------------------------------------------------------------------------------

 

GenericJackson2JsonRedisSerializer, StringRedisSerializer

일반적으로 위 2개를 가장 많이 쓰는 것 같습니다.

 

업무에서 레디스를 주로 캐싱 용도로 사용 했는데, 스프링에서는 @Cacheable 메서드로 간단하게 사용 할 수 있습니다.

간단하고 사용하기 편한 GenericJackson2JsonRedisSerializer 를 사용했는데 역직렬화를 위해 @Class 와 패키지 정보를 포함하고 있어 추가적인 메모리 소모가 발생한다는 단점이 있습니다.

 

단순한 객체의 경우

// GenericJackson2JsonRedisSerializer
// 총 약 60 bytes
{
    "@class": "com.example.User",
    "id": 1,
    "user": "test user"
}

// StringRedisSerializer
// 총 약 30 bytes
{
    "id": 1,
    "user": "test user"
}

 

복잡한 객체의 경우

// GenericJackson2JsonRedisSerializer
// 약 329 bytes, @class 제외시 약 195 byes
{
    "@class": "com.example.ComplexUser",
    "id": 1,
    "name": "Complex Test User",
    "email": "complex@test.com",
    "createdAt": "2024-12-31T10:00:00",
    "address": {
        "@class": "com.example.Address",
        "street": "123 Test St",
        "city": "Test City"
    },
    "orders": [
        {
            "@class": "com.example.Order",
            "orderId": 1,
            "items": [
                {
                    "@class": "com.example.OrderItem",
                    "productId": 1,
                    "name": "Product 1"
                }
            ]
        }
    ]
}

 

레디스에서 관리에 사용되는 추가 메모리 등이 있어 실제 메모리 사용량과는 약간의 차이가 있을 수 있지만, 두 시리얼라이저 간의 상대적인 크기 차이를 비교 해볼수 있습니다.

 

StringRedisSerializer를 사용하면 리턴타입마다 역직렬화 매퍼를 추가해줘야 하는 번거로움이 있습니다.

키가 많아지니 불필요한 메모리 소모가 증가하는 것 같아 RedisSerializer를 구현한 CustomJsonRedisSerializer 를 사용 하기로 결정했습니다.

 

메모리 이점을 가져가면서, 성능도 챙겨야 하고, 사용도 편하게 해봅시다.

 

구현 코드

// CustomJsonRedisSerializer.java
public class CustomJsonRedisSerializer<T> implements RedisSerializer<T> {
    private final ObjectMapper objectMapper;
    private final TypeReference<T> typeReference;

    public CustomJsonRedisSerializer(TypeReference<T> typeReference) {
        this.typeReference = typeReference;
        this.objectMapper = JsonMapper.builder()
            .addModule(new JavaTimeModule())
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .build();
    }

    @Override
    public byte[] serialize(T t) throws SerializationException {
        if (t == null) {
            return new byte[0];
        }
        try {
            return objectMapper.writeValueAsBytes(t);
        } catch (JsonProcessingException e) {
            throw new SerializationException("Error serializing object", e);
        }
    }

    @Override
    public T deserialize(byte[] bytes) throws SerializationException {
        if (bytes == null || bytes.length == 0) {
            return null;
        }
        try {
            return objectMapper.readValue(bytes, typeReference);
        } catch (IOException e) {
            throw new SerializationException("Error deserializing object", e);
        }
    }
}


// CacheConfig.java
@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> customRedisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new CustomJsonRedisSerializer<>(new TypeReference<>(){}));
        return template;
    }

    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();

        cacheConfigurations.put("user", createConfig(new TypeReference<User>() {
        }, 60));
        cacheConfigurations.put("order", createConfig(new TypeReference<Order>() {
        }, 30));
        cacheConfigurations.put("users", createConfig(new TypeReference<List<User>>() {
        }, 360));
        cacheConfigurations.put("orders", createConfig(new TypeReference<List<Order>>() {
        }, 180));

        return RedisCacheManager.builder(connectionFactory)
                .withInitialCacheConfigurations(cacheConfigurations)
                .build();
    }

    private <T> RedisCacheConfiguration createConfig(TypeReference<T> typeReference, long ttlMinutes) {
        return RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(ttlMinutes))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new CustomJsonRedisSerializer<>(typeReference)));
    }

}

 

위처럼 RedisSerializer를 구현해 사용하면 @Cacheable 추가시 config에 리턴 클래스, TTL만 추가해주면 쉽게 사용이 가능합니다.

List나 다른 타입도 TypeReference로 유연하게 사용 가능하다는 장점도 있습니다.

 

성능 비교

위에 설명한 단순한 객체/복잡한 객체를 만들어서 약 10000개의 키를 set/get 하고 역직렬화 후 성능을 비교해봤습니다.

여러번 테스트 시에도 GenericJackson2JsonRedisSerializer 보다는 괜찮은 성능을 보여줬습니다.

 

모니터링시 실제 운영 환경에서 성능도 잘 나오고 있습니다.

@Cacheable 등 스프링 캐시를 많이 사용하고 저장되는 메모리를 줄이고 싶다면 좋은 방법이라고 생각됩니다.

'개발 > 백엔드' 카테고리의 다른 글

Spring GraphQL  (1) 2025.01.06
빠르게 GraphQL 기본 개념 정리  (0) 2025.01.06
Late Limiter vs Circuit Breaker  (0) 2024.12.30
Spring Circuit Breaker  (0) 2024.12.30
Spring Rate Limiter  (1) 2024.12.26