[Java] Java 생태계가 제공하는 직렬화와 역직렬화에 대하여

Hyo Kyun Lee·2025년 12월 22일

Java

목록 보기
109/109

1. 개요

Redis, Kafka 등의 NoSQL를 비롯하여 다양한 RDB와 연동한 Spring Batch 프로젝트를 진행하면서, 자연스럽게 객체와 외부세계간의 경계를 이어주기위한 직렬화 및 역직렬화 개념에 대해 궁금증이 생기게 되었다.

일전에는 JVM/Kafka/Redis 등 각각의 주체 별로 직렬화/역직렬화의 방향이 다르고 그에 따른 개념도 다르게 해석될 수 있을 것으로 인지하였는데, 알아보면서 직렬화 및 역직렬화는 주체에 따라 달라지는 개념이 아닌 JVM을 기준으로 명확하게 경계가 있는 개념임을 알 수 있었다.

이에 대한 내용을 정리하였다.

2. 직렬화/역직렬화 개념

직렬화(Serialization)와 역직렬화(DeSerialization)은 각각 메모리 안에 있는 객체를 외부 세계가 이해할 수 있는 문자열 및 바이트 코드로 변환하는 과정 및 그 반대(복원) 과정이다.

직렬화는

  • 객체(Object) → 바이트(byte) 또는 문자열(String)의 과정이고

역직렬화는

  • 바이트/문자열 → 객체(Object)의 과정이다.

확실한 방향성이 존재하며, 그 기준은 JVM메모리이다.

[ JVM 메모리 ]  <——역직렬화——  [ 외부 데이터 ]
[ JVM 메모리 ]  ——직렬화——>  [ 외부 데이터 ]

JVM에서 나가는 순간 "직렬화"이고, 반대로 들어오는 순간 "역직렬화"이다.

구간의미
JVM 내부객체(Object), 클래스, 필드
JVM 외부byte[], String, 파일, 네트워크 패킷

이때 직렬화는 아래와 같은 다양한 외부형태로 "변환"하는, 외부로 전달한 형태로 바꾸어주는 개념이다.

  • 파일 (CSV, JSON, Binary)
  • 네트워크 (Kafka, HTTP)
  • 외부 저장소 (Redis, DB)

2. 다양한 직렬화 과정

예를 들어 실무에서 사용가능한 직렬화 과정은 .csv 등의 flat file로의 변환, redis나 kafka에서 이해가능한 수준의 byte[] (바이트 배열)로의 변환 등 다양한 형태가 존재한다.

각각의 실무 예시를 들어 이해를 해보도록 하자.

  • flat file(.csv)
Java Object
  ↓
String (컬럼별 분해)
  ↓
CSV line (text)
  ↓
File

객체를 flat file로 변환하는 과정은 객체를 프로퍼티별 JSON 문자열화하거나, 인스턴스 그 자체를 txt화하는 과정을 모두 포함할 수 있고, 이는 모두 직렬화의 과정이다.

CSV line
  ↓
String split
  ↓
Java Object

반대로 Java 측 객체정보를 구성하기 위해 flat file에서 객체로 읽어오는, 파싱 및 매핑과정을 역직렬화라고 한다.

이때 매핑은 개발자, 즉 백엔드에서 구현이 이루어져야 한다.

  • byte

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의 직렬화 방식이 반드시 호환되어야 한다.

3. Serializable

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 Serialization

Redis 직렬화는 RedisTemplate 내부적으로 호출하는 직렬화 인터페이스를 통해 이루어지며, 이는 RedisSerialization으로 항상 정해져있다.

단, 그 구현체를 어떤 것을 사용하느냐에 따라 직렬화 변환 형태가 달라진다.

  • StringRedisSerializer : String type의 key/value를 바이트배열화
String → UTF-8 byte[]
redisTemplate.setKeySerializer(new StringRedisSerializer());
  • JdkSerializationRedisSerializer : Java Serializable 기반의 기본적인 RedisTemplate의 value 직렬화 구현체
Object → JVM binary stream
redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
  • Jackson2JsonRedisSerializer : target Class를 직접 생성자로 전달하여, 타입이 고정된 채(type safe)로 Redis가 이해할 수 있는 형태의 직렬화가 가능하다.
Object ↔ JSON byte[]
new Jackson2JsonRedisSerializer<>(AttackLog.class)
  • GenericJackson2JsonRedisSerializer : 직렬화 시 target 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 Serialization

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 내부 컴포넌트이므로 여기서는 논외로 한다).

  • StringSerializer / StringDeserializer : 단순히 String을 byte배열로 변환한다.
Stringbyte[]
  • 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 계열에서만 사용된다.

항목RedisKafkaSerializable
저장 단위byte[]byte[]byte[]
직렬화 주체RedisTemplateProducerJVM
인터페이스RedisSerializerSerializerSerializable
방식전략 패턴전략 패턴JVM 고정
가독성JSON 가능JSON 가능Binary
언어 독립가능가능불가능
┌──────── JVM ────────┐
│                     │
│  Object (Heap)      │
│    ↓                │
│  Serializer         │  ← 여기서 "완전변환"
│    ↓                │
│  byte[] / String    │
└────────┬────────────┘
         │
         ▼
   Redis / Kafka / File / Network

그리고 이러한 직렬화과정을 내부적으로 완전변환 후 최종 Redis/Kafka 등에 전달한다.

4. 결론

이와 같이 JVM 입장에서, 명확한 기준으로 직렬화/역직렬화 개념이 비롯되었음을 인지하고 본다면 이해하기가 용이할 것이다.

특히, Kafka의 경우 인터페이스 명이 Serializable, DeSerializable 등으로 되어있기에 직렬화/역직렬화 개념에 대해 혼동할 수 있을텐데 JVM이라는 기준을 명확하게 잡고, byte배열로 바꾸는 과정과 내부적으로 다시 "객체"화하는 작업으로 분리되는 것으로 이해한다면 직렬화/역직렬화 개념은 어느정도 이해할 수 있을 것이다.

특히, 직렬화과정은 숲을 보는 것처럼 크게 보는 것이 좋다. 위와 같이 외부세계로 전송되는 과정에서 여러 형태변환이 있을 수는 있지만, 그 모든 과정들은 하나의 "직렬화", 직렬화의 일부분일 뿐이란 점을 이해하는 것이 중요하다.

이러한 이해를 바탕으로 직렬화/역직렬화 컴포넌트 구성 및 구현을 정확하게, 명확하게 진행하도록 하자.

0개의 댓글