Java

[Java] 직렬화(Serialization)

누구세연 2024. 9. 17. 12:46

직렬화(Serialization)는 객체를 바이트 스트림(Byte Stream)으로 변환하여, 이를 파일로 저장하거나 네트워크를 통해 전송할 수 있게 하는 과정입니다.

반대로, 역직렬화(Deserialization)는 바이트 스트림을 다시 객체로 복원하는 과정입니다.

Java에서는 직렬화를 통해 객체의 상태를 저장하거나 다른 시스템으로 데이터를 전달할 수 있습니다.

이번 글에서는 직렬화에 대해 알아보겠습니다.🧐

 

직렬화의 필요성

자바 프로그램에서는 메모리 내에서만 객체를 사용할 수 있습니다.

하지만 때로는 객체를 파일로 저장하거나 네트워크를 통해 전송할 필요가 있습니다.

이때 객체의 상태를 바이트 스트림으로 변환하여 저장하거나 전송할 수 있는데, 이 과정이 바로 직렬화입니다.

 

예를 들어, 한 객체의 데이터를 파일에 저장해 프로그램이 종료된 후에도 다시 불러와야 할 경우, 직렬화를 사용하면 쉽게 객체를 저장하고 다시 복원할 수 있습니다.

 

직렬화의 흐름

객체를 직렬화

자바의 'ObjectOutputStream'을 사용해 객체를 바이트 스트림으로 변환합니다.

이때, 객체의 상태(필드 값)가 전부 직렬화되어 스트림에 기록됩니다.

ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("objectData.ser"));
out.writeObject(someObject);
out.close();

역직렬화

바이트 스트림을 객체로 변환하는 과정입니다.

'ObjectInputStream'을 사용하여 직렬화된 데이터를 다시 객체로 복원합니다.

ObjectInputStream in = new ObjectInputStream(new FileInputStream("objectData.ser"));
SomeClass someObject = (SomeClass) in.readObject();
in.close();

 

직렬화의 기본 구현

Java에서 Java.io.Serializable 인터페이스를 통해 직렬화를 구현합니다.

이 인터페이스는 특별한 메서드를 포함하지 않지만, 객체를 직렬화할 수 있음을 자바 런타임에 알려주는 역할을 합니다.

import java.io.*;

// Serializable 인터페이스를 구현하여 직렬화 가능하도록 설정
class Person implements Serializable {
    private static final long serialVersionUID = 1L; // 직렬화 버전 관리용 ID
    String name;
    int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

public class SerializationExample {
    public static void main(String[] args) {
        Person person = new Person("John Doe", 30);
        
        // 객체를 직렬화하여 파일에 저장
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.ser"))) {
            oos.writeObject(person);
            System.out.println("직렬화 완료!");
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 파일에서 객체를 역직렬화하여 다시 읽기
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.ser"))) {
            Person deserializedPerson = (Person) ois.readObject();
            System.out.println("역직렬화 완료! 이름: " + deserializedPerson.name + ", 나이: " + deserializedPerson.age);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

 

 

주요 특징

  1. Serializable 인터페이스
    직렬화 가능 객체를 만들기 위해서는 Serializable 인터페이스를 반드시 구현해야 합니다.
    이 인터페이스는 메서드가 없기 때문에 구현하는 클래스에 추가적인 구현을 요구하지 않습니다.
  2. serialVersionUID
    자바 직렬화에서 클래스가 변경되면 역직렬화 시 오류가 발생할 수 있습니다.
    이를 방지하기 위해 serialVersionUID라는 고유 ID를 부여하여 클래스 버전 관리를 할 수 있습니다.
    만약 serialVersionUID를 명시하지 않으면 자바가 자동으로 생성하지만, 수동으로 명시해 주는 것이 좋습니다.
  3. transient 키워드
    직렬화하고 싶지 않은 필드가 있을 경우, 해당 필드에 transient 키워드를 붙이면 그 필드는 직렬화 대상에서 제외됩니다.
class Person implements Serializable {
    String name;
    transient int age; // 직렬화되지 않음
}

 

 

직렬화의 활용 사례

  1. 세션 관리
    웹 애플리케이션에서 세션 상태를 직렬화하여 저장할 수 있습니다.
    서버가 재시작되거나 세션 상태를 다른 서버로 전달해야 할 때 유용합니다.
  2. 데이터베이스 캐시
    객체를 직렬화하여 캐시 시스템에 저장하고, 필요할 때 다시 객체로 역직렬화하여 빠르게 접근할 수 있습니다.
  3. 네트워크 통신
    객체를 바이트 스트림으로 변환하여 네트워크를 통해 다른 서버나 클라이언트로 전송할 때 직렬화를 활용할 수 있습니다.

 

직렬화의 위험성

자바 직렬화는 유용하지만, 보안적인 위험이 존재합니다.

특히 역직렬화 과정에서 외부의 바이트 스트림을 처리할 때, 악의적인 데이터가 포함될 수 있습니다.

이로 인해 시스템에 해킹이나 원격 코드 실행 등의 심각한 문제가 발생할 수 있습니다.

주요 위험 요소

  • 역직렬화 취약점
    직렬화된 바이트 스트림이 조작되면, 악성 객체를 생성하여 시스템을 공격할 수 있습니다.
    특히, 역직렬화 과정에서 예기치 않은 클래스가 로드되면서 악의적인 코드를 실행할 가능성이 큽니다.
  • 클래스 변경 시 문제
    클래스 구조가 변경되었을 때(ex: 필드 추가/삭제), 이전에 직렬화된 객체를 다시 읽어올 때 불일치로 인한 호환성 문제가 발생할 수 있습니다.

 

직렬화의 방어 방법

serialVersionUID 사용

클래스 변경에 따른 호환성 문제를 방지하기 위해, serialVersionUID를 명시적으로 선언해야 합니다.

serialVersionUID는 클래스 버전 관리를 위한 고유 식별자 역할을 하며, 클래스가 변경되더라도 일관성을 유지할 수 있습니다.

private static final long serialVersionUID = 1L;

신뢰할 수 없는 데이터 역직렬화 방지

역직렬화 과정에서 외부 입력을 검증하는 것이 중요합니다.

Java 9부터는 ObjectInputFilter를 도입해 역직렬화 시 필터링을 적용할 수 있습니다.

ObjectInputFilter filter = ObjectInputFilter.Config.createFilter("com.example.*;!*");
ObjectInputStream in = new ObjectInputStream(new FileInputStream("data.ser"));
in.setObjectInputFilter(filter);

역직렬화를 피하기

직렬화 대신 더 안전한 데이터 포맷(JSON, XML 등)을 사용하는 것이 일반적으로 권장됩니다.

이를 통해 데이터 구조가 명확하게 정의되고, 보안 위험을 줄일 수 있습니다.

서명된 객체 검증

객체를 직렬화할 때, 디지털 서명을 통해 데이터의 무결성을 보장할 수 있습니다.

직렬화된 데이터에 서명을 추가하여 역직렬화 시 데이터가 위변조 되지 않았음을 확인할 수 있습니다.

 

 

 

💡 Java 직렬화는 객체를 저장하고 전송하는 데 매우 유용하지만, 보안적인 취약점이 있습니다.
이러한 위험을 최소화하기 위해서는 serialVersionUID를 명시적으로 정의하고, 역직렬화 필터를 사용하거나 안전한 데이터 포맷을 선택하는 것이 중요합니다.