1. 소개
Java에서 객체를 직렬화할 때는 보통 인터페이스 Serializable을 사용합니다. 간단합니다: 인터페이스만 구현하면 ObjectOutputStream/ObjectInputStream을 통해 객체를 기록/읽기 할 수 있습니다. 하지만 때로는 이것만으로 충분하지 않습니다:
- 어떤 필드를 어떤 방식으로 직렬화할지 완전히 제어해야 한다.
- 클래스의 서로 다른 버전 간 호환성을 보장해야 한다.
- 직렬화된 파일 크기를 줄이거나 처리 속도를 높이는 것이 중요하다.
이런 경우를 위해 Java에는 Externalizable 인터페이스가 있습니다 — 보다 "수동적"이고 유연한 직렬화 방식입니다.
요약:
- Serializable — 자동 직렬화: 무엇을 어떻게 기록할지 Java가 결정한다.
- Externalizable — 수동 직렬화: 무엇을 어떻게 저장/복원할지 개발자가 직접 지정한다.
2. 계약 Externalizable: writeExternal과 readExternal 구현
Externalizable을 사용하려면 다음이 필요합니다:
- java.io.Externalizable 인터페이스를 구현한다.
- 반드시 두 메서드를 구현한다:
- void writeExternal(ObjectOutput out) throws IOException
- void readExternal(ObjectInput in) throws IOException, ClassNotFoundException
예시:
import java.io.*;
public class User implements Externalizable {
private String name;
private int age;
// 필수: 매개변수가 없는 public 기본 생성자!
public User() {}
public User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(name);
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = in.readUTF();
age = in.readInt();
}
@Override
public String toString() {
return name + " (" + age + ")";
}
}
중요: 어떤 필드를 어떤 순서로 직렬화할지 개발자가 직접 결정합니다. 하지만 필수 요구 사항이 하나 있습니다. 클래스에는 public 매개변수 없는 생성자가 있어야 합니다. 그렇지 않으면 역직렬화 시 InvalidClassException이 발생합니다.
3. Externalizable을 언제 사용할까?
Externalizable을 사용하세요, 다음과 같은 경우:
- 데이터 포맷에 대한 완전한 제어가 필요하다. 예: 일부 필드만 직렬화하거나, 특정 순서/형식으로 직렬화하고 싶을 때.
- 성능과 파일 크기 최적화. 표준 직렬화는 부가 정보(메타데이터, 클래스 이름, 타입 등)를 추가합니다. Externalizable을 사용하면 필요한 데이터만 기록합니다.
- 하위 호환성 확보. 클래스 구조가 변경될 때 오래된/새로운 버전의 데이터를 읽는 로직을 직접 구현할 수 있습니다.
- 비표준 객체 직렬화. 예를 들어 표준 방식으로 직렬화할 수 없는 필드가 있는 경우(예: transient, volatile, 또는 복잡한 구조).
언제 사용하지 말아야 할까?
- 완전한 제어가 필요 없다면 — Serializable을 사용하세요. 더 간단하고 안전합니다.
- 클래스가 변경될 때 데이터 포맷의 호환성을 계속 유지할 자신이 없다면 사용하지 마세요.
4. Externalizable과 Serializable의 장단점
장점:
- 직렬화에 대한 완전한 제어. 무엇을 어떻게 기록/읽을지 직접 결정.
- 컴팩트함. 불필요한 메타데이터가 없다 — 오직 필요한 데이터만.
- 속도. 데이터가 적으면 기록/읽기가 더 빠르다.
- 유연성. 다양한 포맷 버전을 지원하는 로직을 구현하거나, 압축·암호화 등을 추가할 수 있다.
단점:
- 수동 구현 — 실수하기 쉽다. 기록/읽기 순서를 혼동하면 직렬화가 "망가진다"(오류나 잘못된 데이터).
- 자동 지원 없음 transient, serialVersionUID. 모든 것을 직접 설계·구현해야 한다.
- 유지보수가 더 어렵다. 클래스 구조가 바뀌면 직렬화 메서드를 잊지 말고 업데이트해야 한다.
- 매개변수가 없는 public 생성자가 필수.
- "마법"은 줄고 — 책임은 늘어난다.
5. 예제: 간단한 객체의 직렬화와 역직렬화
객체 직렬화
User user = new User("Alice", 30);
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("user.bin"))) {
out.writeObject(user);
}
객체 역직렬화
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("user.bin"))) {
User loaded = (User) in.readObject();
System.out.println(loaded); // Alice (30)
}
주의: 필드 기록/읽기 순서를 바꾸거나, 어떤 필드를 직렬화하는 것을 잊으면 데이터가 잘못됩니다! writeExternal과 readExternal 메서드는 작업 순서가 정확히 일치해야 합니다.
예제: 일부 필드만 직렬화하기
public class SecretUser implements Externalizable {
private String login;
private transient String password; // Externalizable에는 transient가 영향을 주지 않음
public SecretUser() {}
public SecretUser(String login, String password) {
this.login = login;
this.password = password;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(login);
// 비밀번호는 직렬화하지 않음!
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
login = in.readUTF();
password = null; // 비밀번호는 복원하지 않음
}
}
6. 실습: 직렬화 파일 크기 비교
Serializable과 Externalizable로 직렬화했을 때 파일이 얼마나 "무거운지" 비교해 봅시다.
Serializable을 사용하는 클래스
public class UserSerializable implements Serializable {
private String name;
private int age;
public UserSerializable(String name, int age) {
this.name = name;
this.age = age;
}
}
Externalizable을 사용하는 클래스
public class UserExternalizable implements Externalizable {
private String name;
private int age;
public UserExternalizable() {}
public UserExternalizable(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(name);
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = in.readUTF();
age = in.readInt();
}
}
비교 코드
import java.io.*;
public class CompareSerialization {
public static void main(String[] args) throws Exception {
UserSerializable s = new UserSerializable("Bob", 25);
UserExternalizable e = new UserExternalizable("Bob", 25);
// Serializable
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.bin"))) {
out.writeObject(s);
}
// Externalizable
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ext.bin"))) {
out.writeObject(e);
}
System.out.println("Serializable file size: " + new File("ser.bin").length());
System.out.println("Externalizable file size: " + new File("ext.bin").length());
}
}
결과:
파일 ser.bin(Serializable)은 일반적으로 더 큽니다 — Java의 부가 정보가 포함되기 때문입니다. 파일 ext.bin(Externalizable)은 필요한 데이터만 포함하므로 보통 더 작습니다.
7. Externalizable 사용 시 흔한 실수
오류 №1: 매개변수가 없는 public 생성자가 없음.
Externalizable을 구현하는 클래스에는 반드시 public 무인자 생성자가 있어야 합니다. 그렇지 않으면 역직렬화 중 InvalidClassException이 발생합니다.
오류 №2: 필드 기록과 읽기 순서가 일치하지 않음.
메서드 writeExternal과 readExternal은 동일한 순서로 동작해야 합니다. 기록 시 먼저 name을 저장해 놓고, 읽을 때 먼저 age를 읽으려 하면 데이터가 왜곡됩니다.
오류 №3: 직렬화 중 일부 필드를 누락함.
writeExternal에서 어떤 필드를 기록하지 않으면, 역직렬화 시 그 필드는 참조 타입은 null, 숫자 타입은 0 값을 갖게 됩니다.
오류 №4: transient 또는 serialVersionUID를 잘못 사용함.
Serializable과 달리 Externalizable에서는 이러한 메커니즘이 자동으로 동작하지 않습니다 — 어떤 필드를 저장하고 저장하지 않을지 직접 제어해야 합니다.
오류 №5: 클래스 구조를 변경했지만 메서드를 갱신하지 않음.
필드를 추가하거나 삭제하고도 writeExternal과 readExternal에 해당 변경을 반영하지 않으면, 이전에 저장된 데이터를 제대로 불러오지 못할 수 있습니다.
GO TO FULL VERSION