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
왜일까요? 클래스 Book이 Serializable 인터페이스를 구현하지 않았기 때문입니다. 컬렉션(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);
}
}
주의: 이런 방식에서는 역직렬화 시 기록된 객체의 개수를 알아야 하며(또는 특별한 “종료 마커”를 사용해야 합니다).
GO TO FULL VERSION