Spring 캐시 Redis 직렬화 이해하기
Understand Redis Serializers with Spring Cache
데이터를 교환하고 처리하는 과정에서 객체를 이진 형태로 변환하거나, 이진 형태에서 원래의 객체로 복원하는 과정이 필요합니다. 여기서 객체를 이진 데이터로 변환하는 과정을 직렬화(Serialization)라고 하며, 그 반대 과정을 역직렬화(Deserialization)라고 합니다.
Spring Cache 환경에서 캐시 저장소로 Redis를 사용할 때, 직렬화 및 역직렬화를 담당하는 여러 구현체들이 있습니다. 이 글에서는 이 구현체들에 대해 각각의 특징을 비교하고, 어떤 상황에서 가장 적합한지를 알아보도록 하겠습니다.
Prerequisite
직렬화 & 역직렬화 과정을 살펴보기 위해서 아래의 클래스를 활용합니다.
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class User {
private String name;
private int age;
@Override
public String toString() {
return "User{name='" + name + "', age=" + age + "}";
}
}
Implement RedisSerializer
RedisSerializer
는 Spring Data Redis의 중요한 인터페이스입니다. 이 인터페이스는 레디스에 저장할 객체를 직렬화하거나, 레디스에서 데이터를 가져올 때 객체로 역직렬화하는 역할을 합니다.
Spring Data Redis에서 제공하는 여러 직렬화 구현체에 대해 살펴보고, 언제 어떤 구현체를 사용하는 것이 가장 좋은지 알아보겠습니다.
JdkSerializationRedisSerializer
JdkSerializationRedisSerializer
는 이름에서 알 수 있듯이, Java의 기본 직렬화 메커니즘을 사용하여 객체를 직렬화하고 역직렬화하는 역할을 합니다. 이 구현체는 모든 Java 객체를 직렬화할 수 있는 이점이 있습니다.
JdkSerializationRedisSerializer jdkSerializationRedisSerializer = new JdkSerializationRedisSerializer();
User user = new User("John Doe", 25);
byte[] serializedUser = jdkSerializationRedisSerializer.serialize(user);
결과적으로 원시적인 바이트 배열 형태로 직렬화된 데이터를 얻을 수 있습니다:
\xac\xed\x00\x05sr\x00\x04User\x95]\x9a\x9b\xc7\xf4\x12\x06\x02\x00\x02I...
- Pros:
- Java의 기본 직렬화를 사용하므로, 자료 구조와 객체 모델이 복잡한 경우에는 더 나은 선택일 수 있습니다.
- 모든 자바 객체를 처리할 수 있습니다.
- Cons:
- 직렬화된 데이터의 크기가 크므로, Redis의 메모리 사용량이 증가할 수 있습니다.
- 성능상의 이점이 없습니다. 특히, 대량의 데이터를 처리하거나 네트워크 전송이 필요한 경우 성능 저하가 발생할 수 있습니다.
- 직렬화와 역직렬화 과정에서 발생할 수 있는 여러 보안 이슈에 주의해야 합니다.
Jackson2JsonRedisSerializer
Jackson2JsonRedisSerializer
는 Jackson 라이브러리를 사용하여 객체를 직렬화하고 역직렬화하는 역할을 합니다. JSON 포맷의 가독성과 범용성으로 인해 많은 경우에 사용할 수 있습니다.
Jackson2JsonRedisSerializer<User> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<User>(User.class);
User user = new User("John Doe", 25);
byte[] serializedUser = jackson2JsonRedisSerializer.serialize(user);
JSON 형식으로 직렬화된 결과를 얻을 수 있습니다:
{
"name" : "John Doe",
"age" : 25
}
- Pros:
- JSON 포맷으로 데이터를 저장하므로, 다양한 시스템 및 언어와의 호환성이 좋습니다.
- 직렬화된 데이터의 사이즈가 상대적으로 작아짐으로써, Redis의 메모리 사용량을 줄일 수 있습니다.
- 직렬화와 역직렬화 과정 중에 JSON 포맷을 사용함으로써, 데이터를 더 쉽게 볼 수 있어 디버깅에 유리합니다.
- Cons:
- 대상 클래스가 기본 생성자를 필요로 합니다.
- 일부 복잡한 객체 모델에서는 처리가 힘들 수 있습니다.
- 직렬화된 데이터에 클래스 정보가 포함되어 있지 않으므로, 역직렬화 과정에서 클래스 타입 정보가 필요합니다.
GenericJackson2JsonRedisSerializer
GenericJackson2JsonRedisSerializer
는 Jackson2JsonRedisSerializer
와 비슷하지만, 한 가지 중요한 차이점이 있습니다. 그것은 직렬화된 JSON 데이터에 클래스의 타입 정보를 포함한다는 것입니다. 이러한 특징으로 인해 단순히 JSON 데이터 만으로 역직렬화를 수행할 수 있습니다.
GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
User user = new User("John Doe", 25);
byte[] serializedUser = genericJackson2JsonRedisSerializer.serialize(user);
JSON 형식으로 데이터를 직렬화하며, Java 클래스의 풀 네임을 추가적인 메타데이터로 포함시킵니다:
{
"@class" : "app.catsriding.waves.User",
"name" : "John Doe",
"age" : 25
}
- Pros:
- JSON 포맷으로 데이터를 저장하므로, 다양한 시스템 및 언어와의 호환성이 좋습니다.
- 직렬화된 JSON 데이터에 클래스의 타입 정보를 포함하므로 역직렬화를 수월하게 할 수 있습니다.
Jackson2JsonRedisSerializer
에 비해 더욱 다양한 수준의 복잡한 객체 모델을 처리할 수 있습니다.
- Cons:
- 데이터에 타입 정보가 추가되므로, 직렬화된 데이터의 사이즈가 증가할 수 있습니다. 따라서 네트워크 대역폭 및 스토리지에 추가적인 비용이 발생할 수 있습니다.
- 타입 정보를 암호화하기 위해 추가적인 성능 이슈가 발생할 수 있습니다.
StringRedisSerializer
StringRedisSerializer
는 문자열 데이터를 처리하기 위한 간단한 직렬화 도구입니다. 이것은 문자열을 UTF-8 바이트 배열로 변환하여 레디스에 저장하며, 레디스로부터 데이터를 가져올 때는 이를 다시 문자열로 변환합니다.
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
User user = new User("John Doe", 25);
byte[] serializedUser = stringRedisSerializer.serialize(user.toString());
직렬화 결과는 User
객체의 toString()
메소드에서 반환된 문자열입니다:
User{name='John Doe', age=25}
- Pros:
- 간단하고 가볍습니다. 상대적으로 빠른 성능을 제공합니다.
- 문자열 데이터는 가독성이 높고 다양한 시스템 및 언어와 호환되므로 유연성이 좋습니다.
- Cons:
- 문자열 형태의 데이터만 처리할 수 있습니다. 따라서, 복잡한 객체를 직렬화하거나 역직렬화할 수 없습니다.
- 문자열 이외의 데이터 타입을 사용해야 하는 경우에는 적합하지 않습니다.
GenericToStringSerializer
GenericToStringSerializer
는 특정 Class의 객체를 문자열 형태로 직렬화하는데 사용됩니다. StringRedisSerializer
와는 달리 GenericToStringSerializer
를 사용하면 문자열로 변환될 수 있는 클래스의 객체를 Redis에 저장하는 것이 가능합니다. 하지만 이 경우, 역직렬화 시 원래 클래스로 정확히 복원되지 않을 수 있으므로 주의가 필요합니다. 이는 구조가 복잡한 데이터를 처리할 때 문제가 될 수 있습니다.
GenericToStringSerializer<User> genericToStringSerializer = new GenericToStringSerializer<User>(User.class);
User user = new User("John Doe", 25);
byte[] serializedUser = genericToStringSerializer.serialize(user);
전환된 데이터는 사용자가 정의한 toString()
메소드에 따라 문자열 형태로 식별됩니다:
User@3f49dace[name = 'John Doe', age = 25]
- Pros:
- 주어진 클래스의 모든 객체를 문자열로 변환할 수 있습니다. 이는
Spring Converter
를 사용하여 객체를 문자열로 변환함으로써 단순한 문자열 이외의 객체도 처리할 수 있도록 합니다. - 간단한 객체 모델의 경우, 데이터를 직관적이고 가독성이 높은 문자열 포맷으로 저장하는 데 유용할 수 있습니다.
- 주어진 클래스의 모든 객체를 문자열로 변환할 수 있습니다. 이는
- Cons:
- 역직렬화 시 원래의 객체로 복원되지 않을 수 있습니다. 따라서 복잡한 객체 구조를 해결하는 데 적합하지 않습니다.
- Java에서만 사용되기 때문에 다른 시스템과의 상호 운용성이 떨어집니다.
Choosing RedisSerializers
지금까지 RedisSerializer
의 다양한 구현체를 살펴보았습니다. 이들 중에서도 어느 구현체를 선정할지는 항상 주어진 상황과 요구사항에 따라 달라집니다. 하지만 실무에서는 StringRedisSerializer
와 GenericJackson2JsonRedisSerializer
가 가장 보편적으로 사용되는 것으로 알려져 있습니다.
StringRedisSerializer
는 키(Key) 직렬화에 가장 적합한 선택입니다. Redis 자체는 Key-Value 데이터베이스이며, 모든 키는 문자열 형태입니다. 따라서 StringRedisSerializer
는 문자열 키를 적절하게 처리할 수 있으며, 애플리케이션이 사용하는 문자열 키의 직렬화와 역직렬화에 가장 효율적입니다.
caching-posts::postId=1348&pageSize=24
caching-posts::postId=1324&pageSize=24
caching-posts::postId=1300&pageSize=24
GenericJackson2JsonRedisSerializer
는 동일한 서버 내에서 객체의 값을 직렬화 및 역직렬화하는 데 가장 유용한 도구입니다. 직렬화된 데이터에 클래스 정보가 포함되어 있기 때문에 별도의 변환 작업 없이 역직렬화할 수 있습니다.
그러나, 현대적인 애플리케이션에서 많이 채택하는 Microservice Architecture(MSA) 환경에는 적합하지 않을 수 있습니다. 이 직렬화된 데이터를 역직렬화 하려면 동일한 위치에 해당 클래스가 존재해야 하는데 외부 서버에서 이를 항상 보장할 수는 없기 때문입니다.
이와 같은 구조에서는 StringRedisSerializer
를 활용하는 것이 좋은 대안일 수 있습니다:
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
ObjectMapper objectMapper = new ObjectMapper();
User user = new User("John Doe", 30);
try {
String jsonString = objectMapper.writeValueAsString(user);
byte[] serializedUser = stringRedisSerializer.serialize(jsonString);
} catch (Exception e) {
e.printStackTrace();
}
객체를 직렬화하기 전에 먼저 ObjectMapper
를 사용하여 객체를 문자열로 변환한 후 이를 StringRedisSerializer
을 통해 직렬화하였습니다. 한편으로는 이러한 접근 방식은 ObjectMapper
의 예외 처리가 필요하므로 코드 복잡성이 증가할 수 있지만, 변환된 데이터는 어디서든 다시 ObjectMapper
를 통해 객체로 변환할 수 있다는 장점이 있습니다.
- Spring
- Redis