CodeGym /행동 /JAVA 25 SELF /Serializable 인터페이스: 기본 원칙

Serializable 인터페이스: 기본 원칙

JAVA 25 SELF
레벨 42 , 레슨 1
사용 가능

1. Serializable 인터페이스

지난 강의의 예시가 기억나시나요? 직렬화하려는 Java 클래스는 특별한 인터페이스 — java.io.Serializable — 를 구현해야 합니다. 이는 이른바 마커 인터페이스로, 어떤 메서드도 포함하지 않으며 단지 클래스를 직렬화 대상임을 “표시”합니다. 클래스가 이 인터페이스를 구현하면 JVM은 표준 수단으로 해당 객체를 직렬화하도록 허용합니다.

모든 것을 무조건 직렬화하는 것은 바람직하지 않습니다. 모든 객체가 직렬화될 수 있거나, 직렬화해야 하는 것은 아니기 때문입니다. 일부 객체는 운영체제 상태, 열린 파일 또는 네트워크 연결에 의존합니다. 따라서 Java는 클래스를 직렬화 가능하다고 명시적으로 표시하도록 요구합니다.

마커 인터페이스는 상자에 붙은 “포장 허용” 스티커와 같습니다. 그런 스티커가 없으면 포장 담당자(JVM)는 작업을 거부합니다.

예시: 직렬화 가능한 클래스 선언

import java.io.Serializable;

public class User implements Serializable {
    private String name;
    private int age;

    // 생성자, getter, setter
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 보기 좋게: toString() 메서드
    @Override
    public String toString() {
        return "User{name='" + name + "', age=" + age + "}";
    }
}

주의하세요:

  • 클래스 선언에 implements Serializable만 추가했습니다.
  • 어떤 메서드도 구현할 필요가 없습니다(인터페이스가 비어 있음).
  • 직렬화 가능한 모든 표준 Java 클래스(예: ArrayList, HashMap, String)는 이미 Serializable을 구현합니다.

2. 사용자 정의 클래스를 직렬화 가능하게 만드는 방법

규칙 1: implements Serializable만 추가하세요

클래스 자체에는 이것만으로 충분합니다. 하지만 주의할 점이 있습니다!

중요: 중첩된 객체들도 모두 직렬화 가능해야 합니다.
클래스에 다른 객체를 참조하는 필드가 있다면, 그 객체들도 직렬화 가능해야 합니다. 예:

public class Profile implements Serializable {
    private User user; // User도 직렬화 가능해야 합니다!
    private int level;
}

필드 중 하나라도 직렬화 불가능하면 직렬화 시도 중 예외가 발생합니다.

3. 직렬화와 역직렬화 예제

파일로 객체를 직렬화하고 역직렬화하는 방법을 살펴봅시다. 이를 위해 ObjectOutputStreamObjectInputStream 클래스를 사용합니다.

예시: User 객체를 파일로 직렬화

import java.io.*;

public class SerializeDemo {
    public static void main(String[] args) {
        User user = new User("Alice", 30);

        // 객체를 파일에 저장
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.ser"))) {
            oos.writeObject(user);
            System.out.println("객체가 user.ser 파일에 성공적으로 직렬화되었습니다");
        } catch (IOException e) {
            System.out.println("직렬화 오류: " + e.getMessage());
        }
    }
}

여기서 무슨 일이 일어나나요?

  • User 객체를 생성합니다.
  • ObjectOutputStream을 열어 "user.ser" 파일에 씁니다.
  • writeObject(user)를 호출합니다. 이때 JVM은 객체를 바이트 스트림으로 변환하여 파일에 저장합니다.

예시: 파일에서 객체 역직렬화

import java.io.*;

public class DeserializeDemo {
    public static void main(String[] args) {
        // 파일에서 객체 읽기
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"))) {
            User user = (User) ois.readObject();
            System.out.println("객체가 성공적으로 복원됨: " + user);
        } catch (IOException | ClassNotFoundException e) {
            System.out.println("역직렬화 오류: " + e.getMessage());
        }
    }
}

여기서 무슨 일이 일어나나요?

  • ObjectInputStream을 열어 "user.ser" 파일에서 읽습니다.
  • readObject()를 호출합니다. JVM이 바이트로부터 객체를 복원합니다.
  • readObject()는 Object를 반환하므로 결과를 필요한 타입(User)으로 캐스팅하는 것을 잊지 마세요.
  • 역직렬화 시 ClassNotFoundException이 발생할 수 있는데, 이는 User 클래스를 찾지 못한 경우입니다.

모두 합치기: 직렬화와 역직렬화

import java.io.*;

public class SerializationExample {
    public static void main(String[] args) {
        User user = new User("Bob", 22);

        // 직렬화
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.ser"))) {
            oos.writeObject(user);
            System.out.println("직렬화가 완료되었습니다!");
        } catch (IOException e) {
            System.out.println("직렬화 오류: " + e.getMessage());
        }

        // 역직렬화
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"))) {
            User loaded = (User) ois.readObject();
            System.out.println("역직렬화가 완료되었습니다! " + loaded);
        } catch (IOException | ClassNotFoundException e) {
            System.out.println("역직렬화 오류: " + e.getMessage());
        }
    }
}

결과:

직렬화가 완료되었습니다!
역직렬화가 완료되었습니다! User{name='Bob', age=22}

4. 직렬화 시 “내부적으로” 어떤 일이 일어나는가

writeObject를 호출하면, JVM은 먼저 클래스가 Serializable 인터페이스를 구현했는지 확인합니다. 클래스가 직렬화 가능하도록 표시되지 않았다면 예외가 던져집니다. 그런 다음 JVM은 객체의 모든 일반 필드(즉, statictransient도 아닌 필드)를 순회하며 그 값을 바이트 스트림에 기록합니다. 이들 필드 중 다른 객체가 있으면, 그 객체들도 Serializable을 구현한 경우에 한해 재귀적으로 직렬화합니다.

역직렬화 시에는 일반 생성자를 호출하지 않고 객체가 생성되며, 필드는 저장된 값으로 채워집니다 — 마치 “생성자 없이 만들기”로 바이트 스트림에서 객체가 되살아나는 것과 같습니다.

일부 필드는 직렬화되지 않습니다. static 필드는 개별 객체가 아니라 클래스 자체에 속하므로 그 값이 저장되지 않습니다. transient로 표시된 필드도 건너뛰는데, 이는 임시 데이터, 캐시 또는 비밀번호 같은 민감한 정보에 유용합니다.

직렬화 과정 다이어그램

flowchart TB
    A[메모리의 User 객체] -- writeObject --> B[ObjectOutputStream]
    B -- 바이트를 저장 --> C[user.ser 파일]
    C -- readObject --> D[ObjectInputStream]
    D -- 복원 --> E[메모리의 User 객체]

5. Serializable 사용 시 흔한 실수

실수 1: 직렬화 불가능한 객체에 대한 필드 참조. 예를 들어 User 클래스에 ThreadSocket 타입의 필드가 있다면 직렬화가 동작하지 않습니다. 모든 객체가 직렬화될 수 있는 것은 아니니 기억하세요!

실수 2: 직렬화 불가능한 내부 클래스. User 클래스가 static이 아닌 내부 클래스를 포함하면 직렬화가 실패할 수 있습니다. static 중첩 클래스나 별도의 최상위 클래스를 사용하는 것이 좋습니다.

실수 3: static 필드를 직렬화하려는 시도. static 필드는 직렬화되지 않습니다 — 이는 객체가 아니라 클래스에 속하기 때문입니다. 역직렬화 후 static 필드는 직렬화된 객체의 값이 아니라 클래스에서 정의된 값을 갖습니다.

실수 4: 클래스 버전 불일치. 직렬화 후 클래스 구조를 변경(예: 필드 추가/삭제)하고 이전 객체를 역직렬화하려 하면 InvalidClassException이 발생할 수 있습니다. 버전 관리를 위해 특별한 필드 serialVersionUID를 사용합니다 — 이에 대해서는 다음 강의에서 더 자세히 다룹니다.

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION