Redis, Kafka 등의 NoSQL를 비롯하여 다양한 RDB와 연동한 Spring Batch 프로젝트를 진행하면서, 자연스럽게 객체와 외부세계간의 경계를 이어주기위한 직렬화 및 역직렬화 개념에 대해 궁금증이 생기게 되었다.
일전에는 JVM/Kafka/Redis 등 각각의 주체 별로 직렬화/역직렬화의 방향이 다르고 그에 따른 개념도 다르게 해석될 수 있을 것으로 인지하였는데, 알아보면서 직렬화 및 역직렬화는 주체에 따라 달라지는 개념이 아닌 JVM을 기준으로 명확하게 경계가 있는 개념임을 알 수 있었다.
이에 대한 내용을 정리하였다.
직렬화(Serialization)와 역직렬화(DeSerialization)은 각각 메모리 안에 있는 객체를 외부 세계가 이해할 수 있는 문자열 및 바이트 코드로 변환하는 과정 및 그 반대(복원) 과정이다.
직렬화는
역직렬화는
확실한 방향성이 존재하며, 그 기준은 JVM메모리이다.
[ JVM 메모리 ] <——역직렬화—— [ 외부 데이터 ]
[ JVM 메모리 ] ——직렬화——> [ 외부 데이터 ]
JVM에서 나가는 순간 "직렬화"이고, 반대로 들어오는 순간 "역직렬화"이다.
| 구간 | 의미 |
|---|---|
| JVM 내부 | 객체(Object), 클래스, 필드 |
| JVM 외부 | byte[], String, 파일, 네트워크 패킷 |
이때 직렬화는 아래와 같은 다양한 외부형태로 "변환"하는, 외부로 전달한 형태로 바꾸어주는 개념이다.
예를 들어 실무에서 사용가능한 직렬화 과정은 .csv 등의 flat file로의 변환, redis나 kafka에서 이해가능한 수준의 byte[] (바이트 배열)로의 변환 등 다양한 형태가 존재한다.
각각의 실무 예시를 들어 이해를 해보도록 하자.
Java Object
↓
String (컬럼별 분해)
↓
CSV line (text)
↓
File
객체를 flat file로 변환하는 과정은 객체를 프로퍼티별 JSON 문자열화하거나, 인스턴스 그 자체를 txt화하는 과정을 모두 포함할 수 있고, 이는 모두 직렬화의 과정이다.
CSV line
↓
String split
↓
Java Object
반대로 Java 측 객체정보를 구성하기 위해 flat file에서 객체로 읽어오는, 파싱 및 매핑과정을 역직렬화라고 한다.
이때 매핑은 개발자, 즉 백엔드에서 구현이 이루어져야 한다.
Redis, Kafka도 마찬가지이다.
Java Object
↓
Serializer
↓
byte[]
↓
Redis
Redis의 경우 해당 시스템에서 이해할 수 있는 형태는 byte배열이기에, 객체를 JSON화하고, 최종적으로 JSON배열을 byte배열 형태로 하는 모든 일련의 과정을 직렬화라 한다.
Java Object
↓
Serializer
↓
byte[]
↓
Kafka Topic
Kafka의 경우 Producer 측에서 직렬화를 하여 Kafka로 전송하는 과정 그대로 이해하면 되겠다. 최종적으로는 내부적으로 byte배열화하여 Kafka가 이해할 수 있는 형태로 직렬화한다.
Redis byte[]
↓
Deserializer
↓
Java Object
반대로, Redis의 byte배열을 java측에서 이해할 수 있는 객체형태로 변환하는 과정은 역직렬화이다.
Kafka byte[]
↓
Deserializer
↓
Java Object
Kafka에서 byte배열을 java측에서 이해할 수 있는 객체형태로 변환하는 과정 역시 역직렬화이다.
이 DeSerializer는 Redis, Kafka 각각 별도의 인터페이스 및 구현체를 제공하여준다(주로 제공하는 것은 직렬화 대상임).
RedisSerializer<T>
- StringRedisSerializer
- JdkSerializationRedisSerializer
- Jackson2JsonRedisSerializer
- GenericJackson2JsonRedisSerializer
Serializer<T>
Deserializer<T>
- StringSerializer
- ByteArraySerializer
- JsonSerializer (Spring Kafka)
- AvroSerializer (Schema Registry)
인터페이스는 그렇다치고 구현체를 보면 RedisTemplate/KafkaTemplate을 구현하면서 많이 사용한 익숙한 형태들이 많이 보이는데, 모두 직렬화 관련 구현체이다.
참고로 Redis, Kafka는 모두 key-value 형태가 기본 포맷이기에 직렬화를 할때도 Key/Value Serializer가 별도로 제공이 되며, Kafka의 경우 Producer와 Consumer의 직렬화 방식이 반드시 호환되어야 한다.
JVM 기본 직렬화가 가능한 인터페이스를 java에서는 제공해주기도 한다.
public interface Serializable {}
보통의 경우에는 사용하지 않지만, 환경적 차이와 프록시 객체의 직렬화 속성, (특히) Spring Security에서 사용하는 session 복제 등에서는 반드시 entity에 Serializable 인터페이스를 구체화하는 직렬화 전략을 선택해야 한다.
ObjectOutputStream.writeObject(obj);
외부적으로 이러한 직렬화를 필요로 하는 경우, 위 로직을 적용하였을때 예외가 발생한다.
@Entity
class User implements Serializable {}
위와 같이 직렬화 인터페이스를 구성해야, JVM이 reflection 시점에 필드값을 추출하고 메타데이터 포함, binary stream을 생성하는 등의 직렬화 과정이 가능하다는 점을 유의해야 한다(단, 필요 시점에).
이러한 기본적인 직렬화 과정은 아래와 같다.
ObjectOutputStream
↓
클래스 메타데이터
- 클래스명
- serialVersionUID
↓
필드 정보
- 타입
- 값
↓
byte stream
반대로 역직렬화 과정은 아래와 같다.
ObjectInputStream
↓
클래스 로딩
↓
필드 값 주입
↓
객체 복원
이처럼 외부환경이 아닌, 내부적인 직렬화 과정을 의미할때는 객체를 byte stream으로 변환하여 WAS 등에 전달/보관하는 용도로 사용하는 과정을 의미한다.
단, 이러한 Serializable 인터페이스의 기능은 그리 좋지는 못하다.
| 항목 | 설명 |
|---|---|
| 성능 | 느림 |
| 용량 | 큼 |
| 가독성 | Binary |
| 버전 변경 | serialVersionUID 충돌 |
| 보안 | 취약 |
구조적으로 반드시 사용해야 하는(Spring Security) 상황이 아니라면, (Redis/Kafka) 환경에서 사용하는 직렬화 인터페이스는 구현체가 따로 존재하며, 해당 구현체를 사용하도록 하자.
이러한 내용을 정리하면 아래와 같다.
직렬화란 “JVM 내부 객체를 외부 세계가 이해할 수 있는 byte 또는 text로 바꾸는 행위”이고,
역직렬화란 “그 외부 데이터를 다시 JVM 객체로 복원하는 행위”다.
CSV, Redis, Kafka는 모두 byte/text만 알 뿐 객체를 모르기 때문에,
각 시스템에 맞는 Serializer/Deserializer 인터페이스를 통해 이 변환이 수행된다.
보통의 경우 JVM내부에서 byte배열(최종적으로 인식 가능한 형태)까지 변환하고, 그 다음에 전달한다.
Redis 직렬화는 RedisTemplate 내부적으로 호출하는 직렬화 인터페이스를 통해 이루어지며, 이는 RedisSerialization으로 항상 정해져있다.
단, 그 구현체를 어떤 것을 사용하느냐에 따라 직렬화 변환 형태가 달라진다.
String → UTF-8 byte[]
redisTemplate.setKeySerializer(new StringRedisSerializer());
Object → JVM binary stream
redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
Object ↔ JSON byte[]
new Jackson2JsonRedisSerializer<>(AttackLog.class)
{
"_class": "com.xxx.AttackLog",
"id": 1
}
이러한 구현체를 등록하여 RedisTemplate에서 key, value 직렬화 진행 시
RedisTemplate.opsForValue().set(key, value)
↓
keySerializer.serialize(key)
valueSerializer.serialize(value)
↓
byte[]
↓
Redis
이와 같이 전달받은 key, value를 JVM 내부적으로 1차 직렬화하며, 최종적으로 Redis 내부에서 byte배열로 직렬화한다.
Kafka도 마찬가지이다.
public interface Serializer<T> {
byte[] serialize(String topic, T data);
}
public interface Deserializer<T> {
T deserialize(String topic, byte[] data);
}
위와 같이 직렬화/역직렬화를 위한 기본 인터페이스를 제공하며, 최종 변환형태는 byte배열이다.
Kafka의 경우 Producer, Consumer에서 사용하는 직렬화 방식이나 전략이 다를 수 있다는 점만 유의하면 되겠다(다만 Consumer는 Kafka 내부 컴포넌트이므로 여기서는 논외로 한다).
String ↔ byte[]
ByteArraySerializer : raw byte 배열로 변환한다.
JsonSerializer / JsonDeserializer : KafkaTemplate에서 사용할 수 있는 json 기반 및 target class를 매개변수로 사용하는 직렬화 인터페이스 구현체.
JsonSerializer<AttackLog>
JsonDeserializer<AttackLog>
최종적으로는, Producer가 생산한 key-value를 아래와 같은 형태로 kafka는 받고 이해한다.
Producer.send(object)
↓
Serializer.serialize()
↓
byte[]
↓
Kafka Topic
↓
Deserializer.deserialize()
↓
Consumer Object
참고로, Serializer.serialize()와 Deserializer.deserialize()는 Kafka 입장에서 보았을때는 byte배열화/객체화하는 과정으로 각각 별도로 직렬화/역직렬화의 관점으로 볼 수 있겠다. 하지만, 전체적인 과정으로 보았을때는 하나의 직렬화 과정으로 보면 된다.
Redis와 Kafka는 모두 byte[] 기반 시스템이므로,
객체를 직접 알지 못하고 Serializer/Deserializer 전략 인터페이스를 통해
JVM 객체 ↔ byte[] 변환을 수행한다.
Redis는 RedisSerializer, Kafka는 Serializer/Deserializer를 사용하며,
Java의 Serializable은 JVM 전용 바이너리 직렬화 방식으로
외부 통신에는 부적합하고 JdkSerialization 계열에서만 사용된다.
| 항목 | Redis | Kafka | Serializable |
|---|---|---|---|
| 저장 단위 | byte[] | byte[] | byte[] |
| 직렬화 주체 | RedisTemplate | Producer | JVM |
| 인터페이스 | RedisSerializer | Serializer | Serializable |
| 방식 | 전략 패턴 | 전략 패턴 | JVM 고정 |
| 가독성 | JSON 가능 | JSON 가능 | Binary |
| 언어 독립 | 가능 | 가능 | 불가능 |
┌──────── JVM ────────┐
│ │
│ Object (Heap) │
│ ↓ │
│ Serializer │ ← 여기서 "완전변환"
│ ↓ │
│ byte[] / String │
└────────┬────────────┘
│
▼
Redis / Kafka / File / Network
그리고 이러한 직렬화과정을 내부적으로 완전변환 후 최종 Redis/Kafka 등에 전달한다.
이와 같이 JVM 입장에서, 명확한 기준으로 직렬화/역직렬화 개념이 비롯되었음을 인지하고 본다면 이해하기가 용이할 것이다.
특히, Kafka의 경우 인터페이스 명이 Serializable, DeSerializable 등으로 되어있기에 직렬화/역직렬화 개념에 대해 혼동할 수 있을텐데 JVM이라는 기준을 명확하게 잡고, byte배열로 바꾸는 과정과 내부적으로 다시 "객체"화하는 작업으로 분리되는 것으로 이해한다면 직렬화/역직렬화 개념은 어느정도 이해할 수 있을 것이다.
특히, 직렬화과정은 숲을 보는 것처럼 크게 보는 것이 좋다. 위와 같이 외부세계로 전송되는 과정에서 여러 형태변환이 있을 수는 있지만, 그 모든 과정들은 하나의 "직렬화", 직렬화의 일부분일 뿐이란 점을 이해하는 것이 중요하다.
이러한 이해를 바탕으로 직렬화/역직렬화 컴포넌트 구성 및 구현을 정확하게, 명확하게 진행하도록 하자.