Java의 직렬화(Serialization)와 역직렬화(Deserialization)

Sony·2025년 2월 16일
0

☕️ JAVA

목록 보기
4/4
post-thumbnail

Java에서 직렬화(Serialization) 는 객체를 저장하거나 네트워크를 통해 전송할 수 있도록 바이트 스트림으로 변환하는 과정이다. 반대로, 역직렬화(Deserialization) 는 바이트 스트림을 다시 객체로 변환하는 과정이다. 직렬화는 파일 저장, 캐시 시스템, 분산 시스템 등에서 널리 사용되지만, 보안 문제도 동반하기 때문에 주의가 필요하다.

이번 포스팅에서는 직렬화의 개념, 사용 방법, 역직렬화 취약점 및 안전한 직렬화 방식을 다뤄보자.


1. 직렬화(Serialization)란?

직렬화는 객체의 상태를 유지한 채 저장하거나 전송할 수 있도록 변환하는 과정이다.
Java에서는 Serializable 인터페이스를 구현하여 직렬화를 수행할 수 있다.

직렬화가 필요한 이유

  1. 객체를 파일로 저장하여 프로그램 종료 후에도 데이터를 유지
  2. 네트워크를 통해 객체를 전송하여 분산 시스템에서 활용
  3. 캐시 시스템에 객체를 저장하여 빠른 데이터 조회 가능

2. Java에서 직렬화 구현 방법

1) Serializable 인터페이스 사용

import java.io.*;

class Person implements Serializable {
    private static final long serialVersionUID = 1L; // 직렬화 버전 ID
    private String name;
    private int age;

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

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + "}";
    }
}

public class SerializationExample {
    public static void main(String[] args) {
        Person person = new Person("Alice", 25);

        // 객체 직렬화 (파일 저장)
        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);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

실행 결과

객체가 직렬화되어 저장됨.
역직렬화된 객체: Person{name='Alice', age=25}

위 코드에서 ObjectOutputStream을 사용해 객체를 파일(person.ser)에 저장하고, ObjectInputStream을 사용해 파일에서 객체를 복원했다.


3. serialVersionUID의 역할

serialVersionUID는 클래스의 버전 정보를 나타내는 고유한 값으로, 객체가 역직렬화될 때 같은 클래스로 복원되는지 확인하는 역할을 한다.

private static final long serialVersionUID = 1L;

serialVersionUID가 다르면, 역직렬화 시 InvalidClassException 예외가 발생할 수 있다.

java.io.InvalidClassException: Person; local class incompatible: 
stream classdesc serialVersionUID = 1, local class serialVersionUID = 2

이를 방지하려면 클래스 변경 시에도 동일한 serialVersionUID를 유지하는 것이 좋다.


4. 직렬화 대상에서 제외하기 (transient)

어떤 필드는 직렬화하지 않고 싶다면, transient 키워드를 사용하면 된다.

class Person implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private transient int age; // 직렬화 제외

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

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + "}";
    }
}

출력 결과

객체가 직렬화되어 저장됨.
역직렬화된 객체: Person{name='Alice', age=0}
  • age 필드는 transient 키워드로 인해 직렬화되지 않음 → 기본값 0으로 설정됨.

5. 직렬화의 문제점과 해결책

1) 보안 취약점 (역직렬화 공격)

Java 직렬화는 보안 취약점을 가지고 있어, 악의적인 직렬화 데이터를 역직렬화하면 임의의 객체를 생성하거나, JVM에서 위험한 동작을 실행할 수 있다.

ObjectInputStream ois = new ObjectInputStream(new FileInputStream("malicious.ser"));
Object obj = ois.readObject(); // 악성 객체가 실행될 위험

이런 문제를 방지하려면:

  • 역직렬화된 객체의 타입을 검증한다.
  • 신뢰할 수 없는 데이터의 역직렬화를 피한다.
  • 직렬화를 완전히 차단하는 방법도 고려할 수 있다.

2) 역직렬화 시 생성자가 호출되지 않음

역직렬화할 때는 생성자가 실행되지 않으며, 필드 값만 복원된다.

class Person implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;

    public Person(String name) {
        this.name = name;
        System.out.println("생성자 호출됨");
    }
}

역직렬화하면 "생성자 호출됨"이 출력되지 않는다.


6. 안전한 직렬화 방법

1) readObject()를 이용한 검증

역직렬화 시 readObject()를 오버라이딩하여 유효성 검사를 수행하면 보안 취약점을 방지할 수 있다.

private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
    ois.defaultReadObject();
    if (age < 0) {
        throw new InvalidObjectException("나이는 0 이상이어야 합니다.");
    }
}

2) JSON 직렬화 활용 (Jackson, Gson)

객체 직렬화에는 JSON 변환 방식이 더 안전하다. Java의 기본 직렬화보다 범용성이 높고, 보안 리스크가 적다.

Jackson을 활용한 JSON 직렬화

import com.fasterxml.jackson.databind.ObjectMapper;

public class JsonSerializationExample {
    public static void main(String[] args) throws Exception {
        ObjectMapper objectMapper = new ObjectMapper();
        Person person = new Person("Alice", 25);

        // JSON 직렬화
        String jsonString = objectMapper.writeValueAsString(person);
        System.out.println("JSON: " + jsonString);

        // JSON 역직렬화
        Person deserializedPerson = objectMapper.readValue(jsonString, Person.class);
        System.out.println("역직렬화된 객체: " + deserializedPerson);
    }
}

3) Externalizable 인터페이스 사용

Externalizable 인터페이스를 구현하면, 어떤 필드를 어떻게 직렬화할지 직접 정의할 수 있다.

import java.io.*;

class CustomPerson implements Externalizable {
    private String name;
    private int age;

    public CustomPerson() {} // 기본 생성자 필수

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

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeUTF(name);
        out.writeInt(age);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException {
        name = in.readUTF();
        age = in.readInt();
    }
}
  • writeExternal()readExternal()을 구현해야 하며, 직렬화 과정을 직접 제어 가능하다.

7. 결론

  • 직렬화(Serialization) 는 객체를 바이트 스트림으로 변환하는 과정이며, Serializable 인터페이스를 사용한다.
  • 역직렬화(Deserialization) 는 바이트 스트림을 다시 객체로 변환하는 과정이다.
  • serialVersionUID 를 지정하여 버전 충돌을 방지하고, transient 키워드를 사용해 특정 필드를 제외할 수 있다.
  • 역직렬화는 보안 위험이 있기 때문에 JSON 직렬화(Jackson, Gson) 또는 Externalizable을 활용하는 것이 더 안전하다.
profile
Bamboo Tree 🎋 : 대나무처럼 성장하고 싶은 개발자, Sony입니다.

0개의 댓글

관련 채용 정보