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 |