Serialization and Deserialization

직렬화(Serialization)와 역직렬화(Deserialization)는 객체 지향 프로그래밍, 데이터 저장, 네트워크 통신 등 다양한 분야에서 핵심적인 역할을 한다.

직렬화와 역직렬화의 개념

직렬화(Serialization)는 데이터 구조나 객체 상태를 저장하거나 전송할 수 있는 형식으로 변환하는 과정이다. 이 과정에서 객체의 상태는 바이트 스트림이나 텍스트 형식(예: JSON, XML)으로 변환된다. 직렬화는 복잡한 데이터 구조를 선형적인(linear) 형태로 “펼치는” 작업이라고 볼 수 있다.

역직렬화(Deserialization)는 직렬화의 반대 과정으로, 저장되거나 전송된 바이트 스트림이나 텍스트를 원래의 객체 구조로 다시 변환하는 작업이다. 이 과정을 통해 저장된 데이터를 애플리케이션에서 다시 사용할 수 있게 된다.

직렬화와 역직렬화의 필요성과 용도

  • 주요 용도

    1. 데이터 저장(Persistence): 객체의 상태를 파일이나 데이터베이스에 저장할 때 사용된다.
    2. 네트워크 통신: 객체를 네트워크를 통해 다른 시스템으로 전송할 때 사용된다.
    3. 원격 프로시저 호출(RPC): 분산 시스템에서 객체를 다른 프로세스나 머신으로 전달할 때 사용된다.
    4. 캐싱: 객체를 메모리나 디스크에 캐싱할 때 사용된다.
    5. 딥 클로닝(Deep Cloning): 객체의 완전한 복사본을 만들 때 사용될 수 있다.
  • 필요성
    직렬화와 역직렬화는 다음과 같은 이유로 필요하다:
    - 프로그램의 실행 상태 유지: 애플리케이션이 종료되더라도 상태를 유지할 수 있다.
    - 시스템 간 통신: 다른 프로그래밍 언어나 플랫폼 간에 데이터를 교환할 수 있다.
    - 분산 시스템: 여러 서버나 클라이언트 간에 객체를 공유할 수 있다.

직렬화의 형식과 방법

직렬화는 다양한 형식과 방법으로 이루어질 수 있다.

주요 형식과 방법은 다음과 같다:

  1. 바이너리 직렬화
    바이너리 직렬화는 객체를 이진 형식으로 변환하는 방법이다. 이 방식은 일반적으로 효율적이며 데이터 크기가 작다.
    장점:

    • 압축된 크기로 효율적인 저장 및 전송
    • 빠른 처리 속도
    • 플랫폼 내부 사용에 적합

    단점:

    • 사람이 읽을 수 없음
    • 플랫폼 간 호환성 문제 가능성
    • 버전 관리 어려움
      예시 (Java):
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    // 직렬화
    try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.dat"))) {
        oos.writeObject(myObject);
    } catch (IOException e) {
        e.printStackTrace();
    }
    
    // 역직렬화
    try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.dat"))) {
        MyClass myObject = (MyClass) ois.readObject();
    } catch (IOException | ClassNotFoundException e) {
        e.printStackTrace();
    }
    

텍스트 기반 직렬화

텍스트 기반 직렬화는 객체를 XML, JSON, YAML 등의 텍스트 형식으로 변환하는 방법이다.

JSON (JavaScript Object Notation)

JSON은 경량화된 데이터 교환 형식으로, 사람이 읽고 쓰기 쉽고 기계가 분석하고 생성하기 쉽다.

장점:

  • 가독성이 좋음
  • 대부분의 프로그래밍 언어에서 지원
  • 웹 애플리케이션에 적합

단점:

  • 바이너리 형식보다 크기가 큼
  • 복잡한 객체 구조 표현에 제한이 있을 수 있음

예시 (JavaScript):

1
2
3
4
5
// 직렬화
const jsonString = JSON.stringify(myObject);

// 역직렬화
const myObject = JSON.parse(jsonString);

예시 (Python):

1
2
3
4
5
6
7
import json

# 직렬화
json_string = json.dumps(my_object)

# 역직렬화
my_object = json.loads(json_string)
XML (eXtensible Markup Language)

XML은 구조화된 데이터를 표현하기 위한 마크업 언어.

장점:

  • 강력한 스키마 지원
  • 복잡한 데이터 구조 표현 가능
  • 다양한 도구와 라이브러리 지원

단점:

  • JSON보다 더 장황하고 파싱이 복잡함
  • 파일 크기가 상대적으로 큼

예시 (Java - JAXB):

1
2
3
4
5
6
7
8
9
// 직렬화
JAXBContext context = JAXBContext.newInstance(MyClass.class);
Marshaller marshaller = context.createMarshaller();
marshaller.marshal(myObject, new File("object.xml"));

// 역직렬화
JAXBContext context = JAXBContext.newInstance(MyClass.class);
Unmarshaller unmarshaller = context.createUnmarshaller();
MyClass myObject = (MyClass) unmarshaller.unmarshal(new File("object.xml"));

프로토콜 버퍼(Protocol Buffers)

Google에서 개발한 구조화된 데이터 직렬화 메커니즘이다.
언어 중립적이고 플랫폼 중립적인 특성을 가진다.

장점:

  • 바이너리 형식으로 효율적
  • 강력한 스키마 정의
  • 다양한 언어 지원
  • 빠른 처리 속도

단점:

  • 추가 도구와 정의 파일 필요
  • 사람이 읽을 수 없음

예시:

1
2
3
4
5
6
7
8
// 프로토콜 버퍼 정의 (.proto 파일)
syntax = "proto3";

message Person {
  string name = 1;
  int32 age = 2;
  string email = 3;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Python 사용 예시
import person_pb2

# 객체 생성 및 직렬화
person = person_pb2.Person()
person.name = "홍길동"
person.age = 30
person.email = "hong@example.com"
serialized_data = person.SerializeToString()

# 역직렬화
new_person = person_pb2.Person()
new_person.ParseFromString(serialized_data)

다양한 프로그래밍 언어에서의 직렬화/역직렬화

Java

Java에서는 Serializable 인터페이스를 구현하여 직렬화를 지원한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.io.*;

public class Person implements Serializable {
    private static final long serialVersionUID = 1L; // 버전 관리용
    
    private String name;
    private int age;
    
    // 생성자, getter, setter 생략
    
    // 직렬화 예제
    public static void serialize(Person person, String filename) {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filename))) {
            oos.writeObject(person);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    // 역직렬화 예제
    public static Person deserialize(String filename) {
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename))) {
            return (Person) ois.readObject();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
            return null;
        }
    }
}

Java에서는 jackson, gson 등의 라이브러리를 사용해 JSON 직렬화/역직렬화도 지원한다.

Python

Python에서는 pickle 모듈을 사용하여 객체를 직렬화할 수 있다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import pickle

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

# 직렬화
person = Person("홍길동", 30)
with open("person.pkl", "wb") as file:
    pickle.dump(person, file)

# 역직렬화
with open("person.pkl", "rb") as file:
    loaded_person = pickle.load(file)

Python에서는 json 모듈을 사용한 JSON 직렬화/역직렬화도 널리 사용된다.

JavaScript

JavaScript에서는 주로 JSON을 사용한다.

1
2
3
4
5
6
7
8
// 직렬화
const person = { name: "홍길동", age: 30 };
const serialized = JSON.stringify(person);
localStorage.setItem("person", serialized);

// 역직렬화
const retrieved = localStorage.getItem("person");
const deserialized = JSON.parse(retrieved);

직렬화/역직렬화의 고려사항과 문제점

보안 고려사항

  1. 역직렬화 취약점: 신뢰할 수 없는 데이터를 역직렬화하면 원격 코드 실행 등의 보안 위험이 발생할 수 있다.
  2. 데이터 노출: 직렬화된 데이터에 민감한 정보가 포함될 수 있다.

해결 방법:

  • 신뢰할 수 없는 소스의 데이터를 역직렬화할 때 주의
  • transient 키워드(Java) 등을 사용하여 민감한 필드를 직렬화에서 제외
  • 역직렬화 필터링 메커니즘 사용

버전 관리

객체 구조가 변경되면 이전에 직렬화된 데이터와 호환성 문제가 발생할 수 있다.

해결 방법:

  • 버전 관리 필드 사용 (예: Java의 serialVersionUID)
  • 변경에 강한 직렬화 형식 사용 (예: Protocol Buffers)
  • 하위 호환성을 고려한 클래스 설계
1
2
3
4
5
6
7
8
// Java에서 버전 관리 예시
public class Person implements Serializable {
    private static final long serialVersionUID = 2L; // 버전 변경
    
    private String name;
    private int age;
    private String email; // 새로운 필드 추가
}

성능 고려사항

  1. 크기: 직렬화된 데이터의 크기가 저장 공간이나 네트워크 대역폭에 영향을 미칠 수 있다.
  2. 처리 시간: 복잡한 객체 그래프를 직렬화/역직렬화하는 데 시간이 많이 소요될 수 있다.

최적화 방법:

  • 효율적인 직렬화 형식 선택 (바이너리 vs 텍스트)
  • 필요한 데이터만 직렬화
  • 고성능 라이브러리 사용

실제 적용 사례

웹 애플리케이션에서의 JSON 직렬화

현대 웹 애플리케이션에서는 JSON을 사용해 클라이언트와 서버 간 데이터를 교환한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 프론트엔드(React 예시)
async function fetchUser(userId) {
    const response = await fetch(`/api/users/${userId}`);
    const user = await response.json(); // JSON 역직렬화
    return user;
}

// 백엔드(Node.js 예시)
app.get('/api/users/:id', (req, res) => {
    const user = findUserById(req.params.id);
    res.json(user); // JSON 직렬화
});

마이크로서비스 간 통신

마이크로서비스 아키텍처에서는 서비스 간 통신에 Protocol Buffers나 Avro 같은 효율적인 직렬화 형식을 사용한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// gRPC(Protocol Buffers 기반) 사용 예시
public class UserService extends UserServiceGrpc.UserServiceImplBase {
    @Override
    public void getUser(UserRequest request, StreamObserver<UserResponse> responseObserver) {
        // 사용자 정보 조회
        User user = userRepository.findById(request.getUserId());
        
        // Response 객체 생성 및 직렬화
        UserResponse response = UserResponse.newBuilder()
                .setId(user.getId())
                .setName(user.getName())
                .setEmail(user.getEmail())
                .build();
                
        responseObserver.onNext(response);
        responseObserver.onCompleted();
    }
}

데이터베이스에 객체 저장

ORM(Object-Relational Mapping) 시스템에서는 객체를 데이터베이스에 저장하기 위해 내부적으로 직렬화를 사용한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// Hibernate(Java) 예시
@Entity
public class Product {
    @Id
    @GeneratedValue
    private Long id;
    
    private String name;
    private BigDecimal price;
    
    // getter, setter 생략
}

// 저장 (내부적으로 객체를 관계형 데이터베이스 형식으로 직렬화)
session.save(product);

// 조회 (내부적으로 데이터베이스 결과를 객체로 역직렬화)
Product product = session.get(Product.class, productId);

캐싱 시스템

Redis 같은 캐싱 시스템에서는 객체를 저장하기 위해 직렬화가 사용된다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Spring Boot Redis 캐싱 예시
@Service
public class ProductService {
    @Autowired
    private ProductRepository productRepository;
    
    @Cacheable(value = "products", key = "#id")
    public Product getProduct(Long id) {
        return productRepository.findById(id).orElse(null);
        // 결과가 자동으로 직렬화되어 Redis에 저장됨
    }
}

최신 동향과 발전

  1. 스키마 진화(Schema Evolution)
    최신 직렬화 프레임워크는 스키마 변경에 더 유연하게 대응할 수 있도록 발전하고 있다.

    • Protocol Buffers 3: 필드 추가/제거에 유연한 설계
    • Avro: 스키마 레지스트리를 통한 스키마 관리
    • FlatBuffers: 유연한 스키마 지원과 빠른 접근 속도
  2. 고성능 직렬화 라이브러리
    더 빠른 처리 속도와 작은 크기를 제공하는 라이브러리들이 등장하고 있다.

    • MessagePack: JSON보다 작고 빠른 바이너리 형식
    • Kryo: Java용 고성능 직렬화 라이브러리
    • Cap’n Proto: 역직렬화 없이 직접 데이터에 접근 가능
  3. 제로 복사(Zero-Copy) 직렬화
    메모리 복사를 최소화하여 성능을 향상시키는 기술이 발전하고 있다.

    • FlatBuffers: 바이너리 버퍼에서 직접 데이터 접근
    • Cap’n Proto: 직렬화 및 역직렬화 단계 제거
    • Apache Arrow: 메모리 형식과 네트워크 프로토콜 통합

용어 정리

용어설명

참고 및 출처