1. 서론: 참조 필드를 가진 객체의 직렬화
실제 애플리케이션에서 완전히 “플랫”한 클래스는 드뭅니다. 보통 한 객체는 다른 객체를 포함하고, 그 객체도 다시 무언가를 포함할 수 있습니다. 이를 객체의 컴포지션(또는 중첩)이라고 합니다. 예를 들어:
public class Address {
String city;
String street;
}
public class Person {
String name;
int age;
Address address; // 중첩 객체!
}
이런 객체를 직렬화할 때 질문이 생깁니다. address 필드는 어떻게 할까요? Java가 Person과 함께 이것도 직렬화해야 할까요? 또 Address 내부에 다른 객체가 더 있다면요? 다행히도(혹은 상황에 따라 불행히도) Java는 기본적으로 모든 중첩 객체를 재귀적으로 직렬화합니다. 단, 그 객체들도 Serializable 인터페이스를 구현하고 있어야 합니다.
Java에서의 직렬화는 항상 깊은 직렬화(deep serialization)입니다. 즉, 객체 자신뿐 아니라 그 객체가 자신의 (비 transient) 필드를 통해 참조하는 모든 객체가 계속해서 “바닥”까지 직렬화됩니다.
과정 시각화
graph TD
A[Person] --> B[Address]
B --> C[CityInfo]
A --> D[Pet]
요컨대, Person을 직렬화하기로 했다면 Java는 Address와 Address 내부의 모든 것까지 차례로 직렬화합니다.
2. 중첩 객체의 요구 사항: Serializable은 필수!
이미 눈치채셨겠지만 중요한 포인트가 있습니다. 직렬화되는 모든 중첩 객체도 Serializable 인터페이스를 구현해야 합니다.
만약 하나라도 참조 필드가 Serializable을 구현하지 않은 객체를 가리킨다면, 직렬화를 시도하는 순간 java.io.NotSerializableException 예외가 발생합니다.
이제 깊은 직렬화의 예를 살펴봅시다.
예시: 정상 동작
import java.io.Serializable;
public class Address implements Serializable {
String city;
String street;
}
public class Person implements Serializable {
String name;
int age;
Address address;
}
두 클래스 모두 Serializable을 구현합니다. 모든 것이 정상적으로 작동하며 직렬화가 성공적으로 수행됩니다.
예시: 오류!
public class Address { // Serializable을 구현하지 않음!
String city;
String street;
}
public class Person implements Serializable {
String name;
int age;
Address address;
}
보시다시피 여기서 Address는 Serializable 인터페이스를 구현하지 않습니다. 따라서 Person을 직렬화하려 하면 NotSerializableException이 발생합니다.
3. 예제: 중첩 객체와 함께 직렬화/역직렬화
실제로 어떻게 동작하는지 살펴봅시다. 이전 단계에서 우리는 “연락처 관리자” 애플리케이션으로 작업했습니다. 이제 각 사용자에게 주소를 추가해 보겠습니다.
import java.io.*;
class Address implements Serializable {
String city;
String street;
Address(String city, String street) {
this.city = city;
this.street = street;
}
}
class Person implements Serializable {
String name;
int age;
Address address;
Person(String name, int age, Address address) {
this.name = name;
this.age = age;
this.address = address;
}
}
public class SerializationDemo {
public static void main(String[] args) throws Exception {
Person p = new Person("이반", 30, new Address("프라하", "슬라빈스카, 1"));
// 직렬화
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"));
out.writeObject(p);
out.close();
// 역직렬화
ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.ser"));
Person restored = (Person) in.readObject();
in.close();
System.out.println(restored.name + ", " + restored.age + ", " +
restored.address.city + ", " + restored.address.street);
}
}
결과:
이반, 30, 프라하, 슬라빈스카, 1
모든 것이 잘 동작합니다. 중첩 객체 Address가 Person과 함께 직렬화되고 복원됩니다.
4. 중첩 객체가 직렬화 가능하지 않다면?
하나라도 참조 필드가 Serializable을 구현하지 않은 객체를 가리키는 상태에서 직렬화를 시도하면, Java는 시도하는 즉시 예외를 던집니다.
오류 시연
class Address { // Serializable 아님!
String city;
String street;
}
class Person implements Serializable {
String name;
Address address;
}
public class Test {
public static void main(String[] args) throws Exception {
Person p = new Person();
p.name = "페차";
p.address = new Address();
p.address.city = "데리";
p.address.street = "뱌조프";
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"));
out.writeObject(p); // <-- 예외가 발생함!
out.close();
}
}
오류:
java.io.NotSerializableException: Address
5. 중첩 객체에 대한 transient
직렬화하면 안 되는 객체(예: 캐시, 데이터베이스 연결, 임시 객체)에 대한 참조 필드가 있다면 어떻게 해야 할까요? 이런 경우 해당 필드를 transient로 선언하세요. 그러면 Java는 직렬화 시 이 필드를 건너뜁니다.
transient 예시
class Address { // Serializable 아님
String city;
String street;
}
class Person implements Serializable {
String name;
transient Address address; // transient!
Person(String name, Address address) {
this.name = name;
this.address = address;
}
}
public class Test {
public static void main(String[] args) throws Exception {
Address addr = new Address();
addr.city = "로스 산토스";
addr.street = "멀홀랜드 드라이브";
Person p = new Person("사샤", addr);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"));
out.writeObject(p);
out.close();
// 역직렬화
ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.ser"));
Person restored = (Person) in.readObject();
in.close();
System.out.println(restored.name); // "사샤"
System.out.println(restored.address); // null!
}
}
결과:
필드 address는 역직렬화 후 null입니다. 이유는 이 필드가 transient이기 때문입니다.
6. 깊은 중첩과 재귀
직렬화는 재귀적으로 동작합니다. Person에 Address 필드가 있고, Address에 CityInfo 필드가 있고, 계속 이어지는 경우, 직렬화기는 직렬화가 불가능한 무언가를 만나거나 메모리가 바닥날 때까지(절반은 농담입니다) 계속 “깊이 잠수”합니다.
중요: 순환 참조
Java 직렬화기는 순환 참조를 처리할 수 있습니다. 예를 들어 어떤 객체가 다른 객체를 참조하고, 그 객체가 다시 원래 객체를 참조하더라도, 직렬화기는 무한 루프에 빠지지 않고 구조를 올바르게 저장합니다.
class A implements Serializable {
B b;
}
class B implements Serializable {
A a;
}
서로를 참조하는 A와 B 객체를 만들어도 직렬화는 StackOverflowError를 유발하지 않습니다. Java는 이미 직렬화한 객체를 기억합니다.
7. 예제: 중첩 리스트가 있는 객체 직렬화
객체 내부에 다른 객체들의 컬렉션이 들어 있는 경우가 흔합니다. 예를 들어 사용자에게 친구 목록이 있을 수 있습니다.
import java.io.*;
import java.util.*;
class Person implements Serializable {
String name;
List<Person> friends;
Person(String name) {
this.name = name;
this.friends = new ArrayList<>();
}
}
public class FriendsSerialization {
public static void main(String[] args) throws Exception {
Person alice = new Person("Alice");
Person bob = new Person("Bob");
alice.friends.add(bob);
// 직렬화
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("friends.ser"));
out.writeObject(alice);
out.close();
// 역직렬화
ObjectInputStream in = new ObjectInputStream(new FileInputStream("friends.ser"));
Person restored = (Person) in.readObject();
in.close();
System.out.println(restored.name); // Alice
System.out.println(restored.friends.get(0).name); // Bob
}
}
중요: Java 표준 라이브러리의 컬렉션(ArrayList, HashMap 등)은 Serializable을 구현하므로 “바로” 동작합니다.
8. 중첩 객체 직렬화 시 흔한 오류
오류 №1: 중첩 객체 중 하나가 Serializable을 구현하지 않음. 중첩 클래스들 중 하나에 implements Serializable를 추가하는 것을 잊었습니다. 결과는 첫 직렬화 시도에서의 NotSerializableException입니다. 직렬화 체인에 있는 모든 클래스가 이 인터페이스를 지원하는지 확인하세요.
오류 №2: 직렬화 불가 필드를 transient로 선언하지 않음. 직렬화해서는 안 되는 필드(예: 스트림, DB 연결, 일시적 객체 등)가 있는데도 이를 transient로 선언하지 않으면 직렬화가 오류와 함께 실패합니다. transient를 잊지 마세요!
오류 №3: 중첩 클래스들의 serialVersionUID 불일치. 중첩 클래스들에 대해 serialVersionUID를 명시적으로 선언하고 구조를 변경한다면, 해당 식별자도 업데이트해야 합니다. 그렇지 않으면 역직렬화 시 오류가 발생할 수 있습니다.
오류 №4: 변경 가능한 중첩 객체. 컬렉션이나 이후에 변경되는 객체(예: 친구 목록)를 직렬화하면, 역직렬화된 결과는 직렬화 시점의 “스냅샷”일 뿐입니다. 원본에서의 새로운 변경 사항은 역직렬화된 객체에 반영되지 않습니다.
오류 №5: 거대한 객체 그래프 직렬화. 매우 복잡한 구조로 많은 중첩 객체가 있는 경우, 직렬화에는 많은 시간과 메모리가 필요할 수 있습니다. 때로는 전체 구조가 아니라 핵심 데이터만 직렬화하는 편이 더 낫습니다.
GO TO FULL VERSION