Item 75. 사용자 지정 직렬화 형식을 사용하면 좋을지 따져 보자.
- 어떤 직렬화 형식이 적ㄹ절할지 따져보지도 않고 기본 직렬화 형식(default serialized form)을 그대로 받아들이지 말라.
- 기본 직렬화 형시글 받아들일 때는 인코딩 유연성(flexibility), 성능, 그리고 정확성(correctness)관점에서 적절하다는 판단이 서야 한다.
- 기본 직렬화 형식은 그 객체의 물리적 표현이 논리적 내용과 동일 할때만 적절하다.
// 기본 직렬화 형식을 그대로 써도 좋은 클래스 후보 public class Name implements Serializable { //성(last name) null이 될 수 없다. //@Serial private final String lastName; //이름(first name) null이 될 수 없다. //@Serial private final String fisrtName; //중간 이름(middle name) 없을 때는 null이다. //@Serial private final String middleName; … // 이하 생략 }
- 객체의 물리적 표현 형태가 논리적 내용과 다를 경우 기본 직렬화 형식을 그대로 받아 드리면 아래의 네 가지 문제가 생김
- 공개 API가 현재 내부 표현 형태에 영원히 종속됨
- 너무 많은 공간(excessive space)을 차지하는 문제가 생김
- 너무 많은 시간을 소비하는 문제가 생김
- 스택 오버플로(stack overflow)문제가 생길수 있음
// 기본 직렬화 형식이 쓸만하지 않은 클래스 사례 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; } …// 이하 생략 }
StringList
에서 직렬화 저장되어야 하는 데이터는StringList.Entry.data
뿐이다. (이중 연결 리스트로 구현된 StringList를 직렬화하기 위해 사용된 모든 데이터를 저장할 필요가 없다.)// 사용자 정의 직렬화 형식을 이용하는 StringList public final class StringList implements Serializable { private transient int size = 0; private transient Entry head = null; // 더 이상 Serializable일 필요가 없음 ! private static class Entry { String data; Entry next; Entry previous; } // 주어진 문자열을 리스트에 추가 pulbic final void add(String s) { … } /** * this가 가리키는 {@code StringList} 객체를 직렬화 * *@serialData 리스트의 크기(리스트 내 문자열 개수)가 먼저 기록되고 *({@code int}), 그 다음에는 모든 문자열({@code String} 각각)이 순서대로 기록된다. * */ private void writeObject(ObjectOutputStream s) throws IOException { s.defaultWriteObject(); // 이중 연결 리스트의 크기 저장 s.writeInt(size); // 순서대로 모든 원소(Entity.data) 기록 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()); } … // 이하 생략 }
private로 선언 되어 있음에도,
writeObject
함수에는 문서화 주석이 달려 있는 것을 주의하자. 이 함수는 직렬화 형식을 규정하므로 private라 해도 public API다. 따라서, 문서를 만들어야 한다.- 사용자 정의 직렬화 형식을 이용할 때는
StringList
예제에서 처럼 객체 필드 대부분을transient
로 선언 해야 한다. - 기본 직렬화 형식을 사용하는 경우,
transient
레이블이 붙은 필드들은 역질렬화 되었을 때 기본값(default value)으로 초기화된다.
- 객체를 직렬화 할때는 객체의 상태를 전부 읽는 함수에 적용할 동기화 수단을 반드시 적용해야 한다.
예를 들어, 모든 함수를 동기화해서 스레드 안전성을 달성하는 객체가 있다고 하자. 그리고 직렬화 형식을 그대로 사용하기로 했다고 하자 그럴 때는 아래와 같은
writeObject
함수를 작성해야 한다.private synchronized void writeObject(ObjectOutputStream s) throws IOException{ s.defaultWriteObject(); }
- 어떤 직렬화 형식을 이용하건, 직렬화 가능 클래스를 구현할 때는 직렬 버전 UID(serial version UID)를 명시적으로 선언해야 한다.
private static final long serialVersionUID = <무작위로 고른 Long 값>;
결론
- 직렬화 가능한 클래스를 만들기로 했다면, 직렬화 형식에 대해서 심각하게 고민해야 한다.
- 기본 직렬화 형식은 그 형태가 객체의 논리적 상태에 부합할 때만 이용하라.
- 그렇지 않다면, 논리적 상태를 적절히 표현하는 사용자 정의 직렬화 형식을 사용하라.(
writeObject
,readObject
) - 일단 공개된 함수는 향수 버전에서 제거 할 수 없듯이, 직렬화 형식에 포함시킨 필드는 나중에 제거 할 수 없다.