유랑하는 나그네의 갱생 기록

だけど素敵な明日を願っている -HANABI, Mr.children-

Study/Java

[Java] 직렬화(Serialization)와 역직렬화(Deserialization)

Madirony 2024. 11. 17. 23:12
728x90

Serialization

 Java에서 직렬화(Serialization)와 역직렬화(Deserialization)는 객체 데이터를 효율적으로 저장하거나 네트워크를 통해 전송하기 위한 중요한 메커니즘입니다. 직렬화와 역직렬화를 왜 사용해야 하는지, 그 동작 원리와 실질적인 활용 사례를 다루겠습니다. 그리고 직렬화 과정에서 사용하는 transient 키워드와 serialVersionUID의 역할 및 설정 방법까지 알아보겠습니다.

 

Serialization?

직렬화(Serialization)란 Java 객체를 바이트 스트림으로 변환하는 과정입니다. 이 과정을 통해 객체를 파일에 저장하거나 네트워크로 전송할 수 있습니다. 다시 말해, 객체의 상태를 영구적으로 저장하거나 전송 가능한 형식으로 변환하는 것입니다.

 

직렬화의 목적

1. 데이터 영속성 : 객체의 상태를 파일, 데이터베이스, 캐시 등에 저장하여 나중에 다시 복원할 수 있습니다.

2. 네트워크 전송 : 네트워크를 통해 객체를 전달하려면 바이트 스트림으로 변환해야 하며, 직렬화가 이를 가능하게 합니다.

3. 캐싱 시스템 : 애플리케이션에서 자주 사용하는 데이터를 직렬화해 캐싱하면 복원 시 빠르게 로드할 수 있습니다.

 

Deserialization?

역직렬화(Deserialization)란 바이트 스트림으로 저장된 데이터를 다시 Java 객체로 복원하는 과정입니다. 직렬화된 객체를 파일에서 읽거나 네트워크를 통해 전달받아 원래 객체 형태로 복원합니다.

 

이론으로는 이해가 잘 안 될 수도 있으니, 예시 코드로 한번 살펴보겠습니다.

 

Source Code

import java.io.*;

// Serializable 인터페이스를 구현해야 직렬화 가능
class Person implements Serializable {
    private static final long serialVersionUID = 1L; // serialVersionUID 설정

    private String name;
    private int age;

    // transient 키워드: 직렬화에서 제외할 필드
    private transient String password;

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

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

public class SerializationExample {
    public static void main(String[] args) {
        // 객체 생성
        Person person = new Person("John Doe", 30, "secret123");

        // 직렬화: 객체를 파일에 저장
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.ser"))) {
            oos.writeObject(person);
            System.out.println("직렬화 완료: " + person);
        } 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='John Doe', age=30, password='secret123'}
역직렬화 완료: Person{name='John Doe', age=30, password='null'}

 

클래스는 Serializable 인터페이스를 구현해야 직렬화가 가능합니다. 또한 password에 transient 키워드를 적용하였으므로 password 필드는 직렬화에서 제외되며, 역직렬화 후에는 기본값(null)으로 설정됩니다. 객체를 person.ser 파일로 직렬화하여 저장하였는데, 이를 다시 역직렬화하여 객체로 복원하는 과정을 거쳤습니다. 역직렬화했을 때, 중요한 정보인 password가 'null'로 표시됨을 확인할 수 있었습니다.

 


Transient?

 이처럼 Java의 transient 키워드는 직렬화에서 특정 필드를 제외하기 위해 사용됩니다. 비밀번호, API 키, 암호화 키 등 민감한 정보를 직렬화에서 제외하여 보안을 강화할 수도 있고, 캐싱이 필요 없는 일시적인 데이터나 Thread, Socket 등 직렬화가 불가능한 객체를 필드로 가질 때 활용할 수 있겠습니다.

 

private transient String password; // 직렬화에서 제외되는 필드

 


serialVersionUID?

 

 serialVersionUID는 직렬화된 객체의 버전을 나타내는 고유 식별자입니다. 객체를 역직렬화할 때, 직렬화된 클래스의 serialVersionUID와 현재 클래스의 serialVersionUID가 일치해야만 정상적으로 복원됩니다.

 

만약에 이 serialVersionUID를 지정하지 않으면 어떻게 될까요?

- JVM이 자동으로 생성하며, 클래스 구조가 변경되면 serialVersionUID 값도 변경됩니다. 그러면 역직렬화 실패(InvalidClassException)를 유발할 수 있습니다.

- 그렇기 때문에 명시적으로 serialVersionUID를 설정하면, 클래스의 구조가 변경되더라도 동일한 serialVersionUID를 유지하면 이전 데이터와 호환성을 유지할 수 있습니다.

 

serialver -classpath . ClassName

 serialVersionUID는 serialver 도구를 통해 자동 생성하는 방법도 있고..

 

private static final long serialVersionUID = 1L;

수동으로는 일반적으로 1L로 설정하지만, 클래스의 버전 변경 주기에 따라 의미 있는 값으로 설정하는 것이 좋습니다. 그래야 클래스의 구조가 변경되었을 때 버전 관리가 명확해지고, 데이터 호환성 여부를 쉽게 판단할 수 있습니다. 어떻게?

 

private static final long serialVersionUID = 20241109L; // 연도 기반
private static final long serialVersionUID = 102L;      // 내부 릴리스 버전 기반

이런 식으로 클래스의 릴리스 버전이나 변경 이력을 반영한 값으로 설정할 수 있습니다. 구조 변경이 거의 없는 단순 클래스로 예상되면 1L로 고정해도 됩니다.

 


 

serialver을 통해 자동으로 serialVersionUID를 설정하고 싶다면 아래 절차를 따르면 됩니다. Person으로 예를 들면..

javac Person.java

serialver -classpath . Person

Person:    static final long serialVersionUID = 1234567890123456789L;

여기서 생성된 값을 이제 활용하면 됩니다. 패키지 경로가 있는 클래스라면 명령어에 패키지가 명시된 전체 클래스 이름을 포함해야 합니다.

serialver로 생성된 값을 그대로 사용하면 직렬화 호환성이 유지되므로, 수동으로 임의 값을 설정할 때보다 안전합니다.

 


+ 추가 복습)

복습 Question

 

정답은 <더보기에서..>

더보기

1번

1번 정답

 

2번

2번 정답

 

3번

3번 정답

 


근데 이거 왜씀?????

망언들

프론트-백 사고에서 벗어나야 할 필요성이 있습니다.

 

답변

 

 

 

 

728x90