CodeGym /행동 /JAVA 25 SELF /컬렉션 직렬화에서 흔히 발생하는 오류 분석

컬렉션 직렬화에서 흔히 발생하는 오류 분석

JAVA 25 SELF
레벨 44 , 레슨 4
사용 가능

1. NotSerializableException: 컬렉션이 직렬화를 거부할 때

컬렉션 직렬화에서 가장 흔하고 까다로운 오류는 java.io.NotSerializableException입니다. 컬렉션의 요소 중 하나라도 Serializable 인터페이스를 구현하지 않으면 발생합니다.

간단한 예를 보겠습니다:

import java.io.*;
import java.util.*;

class Book {
    String title;
    Book(String title) { this.title = title; }
}

public class LibraryApp {
    public static void main(String[] args) throws Exception {
        List<Book> books = new ArrayList<>();
        books.add(new Book("돔비와 아들"));

        // 컬렉션을 직렬화하려는 시도
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("books.ser"))) {
            oos.writeObject(books); // 붐! NotSerializableException
        }
    }
}

무슨 일이 일어날까요? oos.writeObject(books) 단계에서 다음 예외가 발생합니다:

java.io.NotSerializableException: Book

왜일까요? 클래스 BookSerializable 인터페이스를 구현하지 않았기 때문입니다. 컬렉션(ArrayList) 자체가 직렬화를 지원하더라도, 컬렉션의 요소들도 직렬화 가능해야 합니다!

진단 방법

오류 메시지에는 항상 문제를 일으킨 클래스가 명시됩니다 — 예외 메시지에서 해당 클래스를 확인하세요. 컬렉션이 크고 특정 조건에서만 오류가 발생한다면, 우연히 Serializable을 구현하지 않은 요소가 섞였을 가능성이 있습니다.

해결 방법

클래스에 implements Serializable을 추가하세요:

class Book implements Serializable {
    String title;
    Book(String title) { this.title = title; }
}

팁: 컬렉션에 여러 종류의 객체가 들어있다면, 모든 타입이 Serializable을 만족하는지 확인하세요!

2. 역직렬화 시 ClassCastException: 제네릭이 발목을 잡을 때

Java에서는 컬렉션의 제네릭 매개변수 정보가 컴파일 후 소거됩니다(type erasure). 즉, List<String>을 직렬화하고 List<Integer>로 역직렬화해도 컴파일러는 눈치채지 못하지만, 실행 시점에는 ClassCastException이 발생합니다.

예:

// 직렬화
List<String> names = Arrays.asList("안나", "보리스");
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("names.ser"))) {
    oos.writeObject(names);
}

// 역직렬화 (위험!)
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("names.ser"))) {
    List<Integer> numbers = (List<Integer>) ois.readObject(); // unchecked cast
    Integer first = numbers.get(0); // 붐! ClassCastException
}

오류:

java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer

예방 방법

  • “raw type(로 타입)”을 사용하지 말고, 불필요한 캐스팅을 피하세요.
  • 내용이 확실하지 않다면, 역직렬화 후 요소 타입을 검증하세요.
  • 어떤 타입의 컬렉션을 직렬화하고 읽을 때 무엇을 기대하는지 문서화하세요.

안전한 역직렬화 예:

Object obj = ois.readObject();
if (obj instanceof List<?>) {
    List<?> list = (List<?>) obj;
    if (!list.isEmpty() && list.get(0) instanceof String) {
        @SuppressWarnings("unchecked")
        List<String> safeNames = (List<String>) obj; // 경고는 억제했지만, 타입은 확인함!
    }
}

3. 클래스 구조 변경: serialVersionUID와 하위 호환성

컬렉션을 직렬화한 뒤 요소 클래스에 새 필드를 추가하거나, 필드 이름을 바꾸거나, 아예 클래스 구조를 변경했다면, 오래된 파일을 역직렬화할 때 다음과 같은 난해한 오류가 발생할 수 있습니다:

java.io.InvalidClassException: Book; local class incompatible: stream classdesc serialVersionUID = 1234, local class serialVersionUID = 5678

왜 이런 일이 생길까

모든 직렬화 가능한 클래스에는 고유한 버전 식별자 — serialVersionUID — 가 부여됩니다. 클래스가 변경되면(예: 필드를 추가), JVM이 새로운 serialVersionUID를 계산하고, 역직렬화 시 직렬화 당시 버전과 일치하지 않음을 감지합니다.

예방 방법

  • 클래스에 serialVersionUID를 명시적으로 선언하세요:
class Book implements Serializable {
    private static final long serialVersionUID = 1L;
    String title;
    // ...
}
  • 하위 호환성을 유지하세요: 오래된 파일을 읽을 계획이라면 필드를 삭제하거나 이름을 바꾸지 마세요.
  • 변경 후 반드시 역직렬화를 테스트하세요.

그래도 클래스를 변경해야 한다면?

  • 직렬화를 수동으로 제어하기 위해 readObject/writeObject 메서드 구현을 고려하세요.
  • 또는 데이터를 마이그레이션하세요: 구버전 클래스로 오래된 파일을 읽은 뒤, 새 포맷으로 다시 저장합니다.

4. 불변 컬렉션 직렬화 시 데이터 손실

최신 Java에는 List.of(), Set.of(), Map.of()처럼 불변 컬렉션을 생성하는 API가 있습니다. 오래된 Java 버전(12 이전)이나 일부 서드파티 구현에서는 이러한 컬렉션의 직렬화가 올바르게 동작하지 않을 수 있습니다. 역직렬화 후 컬렉션이 일반 변경 가능 컬렉션이 되거나, 아예 오류가 발생할 수 있습니다.

예:

List<String> list = List.of("a", "b", "c");
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("list.ser"))) {
    oos.writeObject(list);
}

오래된 JVM에서는 역직렬화 시 오류가 발생하거나 컬렉션이 불변성을 잃는 문제가 있었습니다.

예방 방법

  • 사용 중인 Java 버전의 문서를 확인하세요.
  • 이러한 컬렉션의 직렬화와 역직렬화를 직접 테스트하세요.
  • 불변성을 유지해야 한다면, 역직렬화 후 Collections.unmodifiableList(list)로 감싸세요.

5. transient 및 static 필드의 직렬화

이런 필드들은 어떻게 처리될까:

  • transient — 이 키워드로 표시된 필드는 아예 직렬화되지 않습니다. 역직렬화 후 기본값(예: null 또는 0)을 갖습니다.
  • static — 클래스(객체가 아님)의 필드는 결코 직렬화되지 않습니다.

예:

class Book implements Serializable {
    String title;
    transient String cache; // 직렬화되지 않음!
    static String publisher = "Default"; // 이것도 직렬화되지 않음!
}

왜 중요한가

객체 내부에 계산된 값이나 캐시를 보관한다면, 이를 transient로 표시하세요 — 공간을 절약하고 직렬화를 빠르게 합니다.

주의: 역직렬화 후 transient 필드는 다시 계산하거나 재초기화해야 합니다.

6. 대용량 컬렉션 직렬화: 성능과 파일 크기

문제점:

  • 대용량 컬렉션(예: 백만 개의 객체)은 거대한 파일, 긴 쓰기/읽기 시간, 심지어 메모리 부족(OutOfMemoryError)을 유발할 수 있습니다.
  • 객체 그래프(예: 복잡하게 얽힌 컬렉션)를 직렬화하면 파일 크기가 예상보다 커질 수 있습니다.

대응 방법

  • 컬렉션을 부분적으로 직렬화하세요: 예를 들어, 객체를 하나씩 또는 작은 묶음으로 기록합니다.
  • 스트리밍 처리 방식을 사용하세요: 컬렉션 전체를 한꺼번에 직렬화하기보다, 필요에 따라 요소를 순차적으로 직렬화합니다.
  • 파일을 압축하세요: GZIPOutputStream을 사용해 파일 크기를 줄일 수 있습니다.

스트리밍 직렬화 예:

try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("books.ser"))) {
    for (Book book : bigList) {
        oos.writeObject(book);
    }
}

주의: 이런 방식에서는 역직렬화 시 기록된 객체의 개수를 알아야 하며(또는 특별한 “종료 마커”를 사용해야 합니다).

1
과제
JAVA 25 SELF, 레벨 44, 레슨 4
잠금
데이터 오해: `ClassCastException` 역직렬화 후 🚨
데이터 오해: `ClassCastException` 역직렬화 후 🚨
1
과제
JAVA 25 SELF, 레벨 44, 레슨 4
잠금
디지털 아카이브 관리자: 압축을 통한 대용량 데이터의 효율적 저장 🗄️
디지털 아카이브 관리자: 압축을 통한 대용량 데이터의 효율적 저장 🗄️
1
설문조사/퀴즈
복잡한 구조의 직렬화, 레벨 44, 레슨 4
사용 불가능
복잡한 구조의 직렬화
복잡한 구조의 직렬화
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION