기본 직렬화 형태는 주의해서 사용하자
클래스가 Serializable을 구현하고 기본 직렬화 형태를 사용한다면 다음 릴리스 때 제한이 생겨버린다. 기본 직렬화 형태는 유연성, 성능, 정확성 측면에서 신중히 고민한 후 합당할 때만 사용해야 한다.
기본 직렬화 형태
- 객체가 포함한 데이터들과 그 객체에서부터 시작해 접근할 수 있는 모든 객체를 담아낸다.
이상적인 직렬화 형태는 물리적인 모습과 독립된 논리적인 모습만 표현해야 한다.ㅍ 만약 객체의 물리적 표현과 논리적 내용이 같다면 기본 직렬화 형태라도 무방하다.
e.g ) 성명은 논리적으로 이름, 성, 중간이름이라는 3개의 문자열로 구성되며, 다음 코드의 인스턴스 필드들은 이 논리적 구성요소를 정확히 반영한다.
import java.io.Serializable;
public class Name implements Serializable {
private final String lastName;
private final String firstName;
private final String middleName;
}
기본 직렬화 형태가 적합하다고 결정했더라도 불변식 보장과 보안을 위해 readObject 메서드를 제공하자.
커스텀 직렬화를 고려하자
다음은 기본 직렬화에 적합하지 않은, 즉 물리적 표현과 논리적 표현이 일치하지 않는 경우이다.
import java.io.Serializable;
public final class StringList implements Serializable {
private int size = 0;
private Entry head = null;
private static class Entry implements Serializable {
String data;
Entry next;
Entry previous;
}
// Appends the specified string to the list
public final void add(String s) {
}
}
객체의 물리적 표현과 논리적 표현의 차이가 클 때 기본 직렬화 형태를 사용하면 다음과 같은 문제가 생긴다.
- 공개 API가 현재의 내부 표현 방식에 영구히 묶인다.
- 너무 많은 공간을 차지할 수 있다.
- 연결 리스트의 모든 엔트리와 연결 정보까지 기록했지만, 이 정보는 내부 구현에 해당하니 직렬화 형태에 포함할 가치가 없다.
- 직렬화 형태가 커져서 디스크에 저장하거나, 네트워크 전송하는 속도가 느려진다.
- 시간이 너무 많이 걸릴 수 있다.
- 직렬화 로직은 객체 그래프의 위상에 관한 정보가 없으니 그래프를 직접 순회해볼 수밖에 없다.
- 스택 오버플로우를 일으킬 수 있다.
- 기본 직렬화 과정은 객체 그래프를 재귀 순회한다.
StringList를 위한 합리적인 직렬화 형태는 단순히 리스트가 포함한 문자열의 개수를 적은 다음, 그 뒤로 문자열들을 나열하는 것이다. 즉, StringList의 물리적인 상세 표현은 배제한 채 논리적인 구성만 담는 것이다.
public final class StringList implements Serializable {
private transient int size = 0;
private transient Entry head = null;
// No longer Serializable!
private static class Entry {
String data;
Entry next;
Entry previous;
}
public final void add(String s) { }
private void writeObject(ObjectOutputStream s)
throws IOException {
s.defaultWriteObject();
s.writeInt(size);
// Write out all elements in the proper order.
for (Entry e = head; e != null; e = e.next)
s.writeObject(e.data);
}
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {
s.defaultReadObject();
int numElements = s.readInt();
// Read in all elements and insert them in list
for (int i = 0; i < numElements; i++)
add((String) s.readObject());
}
// Remainder omitted
}
클래스의 인스턴스 필드 모두가 transient면 defaultWriteObject와 defaultReadObject를 호출하지 않아도 된다고 들었을지 모르지만, 이 작업은 무조건 하자. 이렇게 해야 향후 릴리스에서 trasient가 아닌 인스턴스 필드가 추가되더라도 상호 호환이 된다.
Transient 한정자
- defaultWriteObject 메서드를 호출하면 transient로 선언하지 않은 모든 인스턴스 필드가 직렬화된다.
- 해당 객체의 논리적 상태와 무관한 필드라고 확신할 때만 transient 한정자를 생략하자. (커스텀 직렬화 형태에서는 대부분 인스턴스 필드가 transient여야 한다)
- 기본 직렬화를 사용한다면 transient 필드들은 역직렬화될 때 기본값으로 초기화된다.
- 기본값을 그대로 사용해서는 안 된다면 readObject 메서드에서 defaultReadObject를 호출한 다음, 해당 필드를 원하는 값으로 복원하자.
- 객체의 전체 상태를 읽는 메서드에 적용해야 하는 동기화 메커니즘을 직렬화에도 적용해야 한다.
- 모든 메서드를 synchronized로 선언하여 스레드 안전하게 만든 객체에서 기본 직렬화를 사용하려면 synchronized로 선언해야 한다.
직렬 버전 UID를 명시적으로 부여하자
어떤 직렬화 형태를 택하든 직렬화 가능 클래스 모두에 직렬 버전 UID를 명시적으로 부여하자. 이렇게 하면 직렬 버전 UID가 일으키는 잠재적인 호환성 문제가 사라지며, 성능도 조금 빨라진다. 직렬 버전 UID 선언은 각 클래스에 아래 같은 한 줄만 추가해주면 된다.
private static final long serialVersionUID = <무작위로 고른 long값>