// 기본 직렬화 형태에 적합하지 않은 StringList 클래스
public final class StringList implements Serializable {
private int size = 0;
private Entry head = null;
private static class Entry {
String data;
Entry next;
Entry previous;
}
... // 나머지 코드 생략
}
논리적 관점 : 위 클래스는 일련의 문자열을 표현
물리적 관점 : 문자열들을 이중 연결 리스트로 연결
위 클래스는 기본 직렬화 형태를 사용하면 각 노드의 양방향 연결 정보를 포함해 모든 엔트리를 철두철미하게 기록해야함
이처럼 객체의 물리적 표현과 논리적 표현의 차이가 클 때 기본 직렬화 형태를 사용하면 크게 4가지 측면에서 문제 발생
공개 API가 현재의 내부 표현 방식에 영구히 묶인다.
너무 많은 공간을 차지할 수 있다.
시간이 너무 많이 걸릴 수 있다.
스택오버플로를 일으킬 수 있다.
합리적인 직렬화 형태
StringList를 위한 합리적인 직렬화 형태는?
단순히 리스트가 포함된 문자열의 개수를 적은 다음, 그 뒤로 문자열을 나열하는 수준이면 될 것
(물리적인 상세 표현은 배제한 채 논리적인 구성만 담은 것)
// 합리적인 커스텀 직렬화 형태를 갖춘 StringList
public final class StringList implements Serializable {
private transient int size = 0;
private transient Entry head = null;
// 이제는 직렬화되지 않는다.
private static class Entry {
String data;
Entry next;
Entry previous;
}
// 지정한 문자열을 이 리스트에 추가한다.
public final void add(String s) { ... }
/**
* 이 {@code StringList} 인스턴스를 직렬화한다.
*
* @serialData 이 리스트의 크기(포함된 문자열의 개수)를 기록한 후
* ({@code int}), 이어서 모든 원소를(각각은 {@code String})
* 순서대로 기록한다.
*/
private void writeObject(ObjectOutputStream s) throws IOException {
s.defaultWriteObject();
s.writeInt(size);
// 모든 원소를 올바른 순서로 기록한다.
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();
// 모든 원소를 읽어 이 리스트에 삽입한다.
for (int i = 0; i < numElements; i++)
add((String) s.readObject());
}
... // 나머지 코드는 생략
}
StringList의 필드 모두가 transient더라도 writeObject와 readObject는 가장 먼저 defaultWriteObject와 defaultReadObject를 호출
클래스의 인스턴스 필드 모두가 transient여도, defaultWriteObject/defaultReadObject를 호출해야 함
직렬화 명세에서 이 작업을 무조건 하라고 요구!
향후 transient가 아닌 인스턴스 필드가 추가되더라도 상위와 하위 모두 호환이 가능
신버전 인스턴스를 직렬화한 후 구버전으로 역직렬화하면 새로 추가된 필드들은 무시될 것
만약, readObject에서 defaultReadObject를 호출하지 않는다면 역직렬화시, StreamCorruptedException 발생
기본 직렬화 사용시 주의사항
기본 직렬화를 수용하든 하지 않든 defaultWriteObject 메서드를 호출하면 transient로 선언하지 않은 모든 인스턴스 필드는 직렬화된다.
따라서 해당 객체의 논리적 상태와 무관한 필드라고 확신할 때만 transient 한정자를 생략
기본 직렬화를 사용한다면 transient 필드들은 역직렬화될때 기본값으로 초기화된다.
객체 참조 필드는 null로, 숫자 기본 타입 필드는 0으로, boolean 필드는 false로
기본값을 그대로 사용해서는 안된다면 readObject 메서드에서 defaultReadObject를 호출한 다음, 해당 필드를 원하는 값으로 복원하거나, 혹은 그 값을 처음 사용할 때 초기화 하는 방법이 있음
객체 전체 상태를 읽는 메서드에 적용해야 하는 동기화 메커니즘을 직렬화에도 적용
모든 메서드를 synchronized로 선언하여 스레드 안전하게 만든 객체에서 기본 직렬화를 사용하려면 writeObject도 synchronized 선언
어떠한 직렬화 형태를 택하든 직렬화 가능 클래스 모두에 serialVersionUID 를 명시적으로 부여하자.
직렬버전 UID가 일으키는 잠재적인 호환성 문제가 사라짐(#86)
성능도 조금 빨라짐 -> UID가 없으면 런타임에 이 값을 생성하느라 복잡한 연산을 수행하게 됨
private static final long serialVersionUID = 234098243823485285L; // 아무 값이나 상관없다.
기본 버전 클래스와의 호환성을 끊고 싶다면UID값을 바꿔주면 됨
이럴 경우 기존 버전의 직렬화된 인스턴스를 역직렬화할때 InvalidClassException을 던짐
구버전으로 직렬화된 인스턴스들과 호환성을 끊으려는것이 아니면 serialVersionUID 를 바꾸지 말것
먼저 고민해보고 괜찮다면 기본 직렬화 형태를 사용하자
유연성
,성능
,정확성
측면에서 신중히 고민 후 합당할때만 사용해야 함객체의 물리적 표현과 논리적 내용이 같다면 기본 직렬화 형태라도 무방
/**
/**
/**
... // 나머지 코드는 생략 }
불변식 보장
과보안
을 위해 readObject 메서드를 제공해야 할때가 많음논리적 관점 : 위 클래스는 일련의 문자열을 표현
물리적 관점 : 문자열들을 이중 연결 리스트로 연결
위 클래스는 기본 직렬화 형태를 사용하면 각 노드의 양방향 연결 정보를 포함해 모든 엔트리를 철두철미하게 기록해야함
이처럼 객체의 물리적 표현과 논리적 표현의 차이가 클 때 기본 직렬화 형태를 사용하면 크게 4가지 측면에서 문제 발생
합리적인 직렬화 형태
기본 직렬화 사용시 주의사항
기본 직렬화를 수용하든 하지 않든 defaultWriteObject 메서드를 호출하면 transient로 선언하지 않은 모든 인스턴스 필드는 직렬화된다.
기본 직렬화를 사용한다면 transient 필드들은 역직렬화될때 기본값으로 초기화된다.
객체 전체 상태를 읽는 메서드에 적용해야 하는 동기화 메커니즘을 직렬화에도 적용
어떠한 직렬화 형태를 택하든 직렬화 가능 클래스 모두에 serialVersionUID 를 명시적으로 부여하자.
기본 버전 클래스와의 호환성을 끊고 싶다면UID값을 바꿔주면 됨