Item 87 : 커스텀 직렬화 형태를 고려해보라
2021. 9. 24. 14:20
- 먼저 고민해보고 괜찮다고 판단될 때만 기본 직렬화 형태를 사용하라
- 직접 설계하더라도 기본 직렬화 형태와 거의 같은 결과가 나올 경우에만 기본형태를 써야한다.
- 객체의 물리적 표현과 논리적 내용이 같다면 기본 직렬화 형태라도 무방하다.
// 기본 직렬화 형태에 적합한 후보
public calss Name implements Serializable {
/**
* 성. null이 아니어야함
* @serial
@serial 태그로 기술한 내용은 API 문서에서 직렬화 형태를 설명하는 특별한 페이지에 기록된다.
*/
private final String lastName;
/**
* 이름. null이 아니어야함
* @serial
*/
private final String firstName;
/**
* 중간이름. 없으면 null
* @serial
*/
private final String middleName;
//다른 코드 생략
}
- 성명은 논리적으로 이름, 성, 중간이름이라는 3개의 문자열로 구성된다.
- 앞 코드의 인스턴스 필드들은 이 논리적 구성요소를 정확히 반영했다.
기본 직렬화 형태가 적합하다고 결정했더라도 불변식 보장과 보안을 위해 readObject 메소드를 제공해야 할 때가 많다.
- 앞의 Name 클래스의 경우에는 readObject 메소드가 lastName과 firstName 필드가 null이 아님을 보장해야 한다.
// 기본 직렬화 형태에 적합하지 않은 클래스
public final class StringLIst implements Serializable {
private int size = 0;
private Entry head = null;
private static class Entry implements Serializable {
Stirng data;
Entry next;
Entry previous;
}
}
- 논리적으로 이 클래스는 일련의 문자열을 표현함
- 물리적으로 문자열들을 이중 연결 리스트로 연결해놓음
- 객체의 물리적 표현과 논리적 표현의 차이가 클 때 기본 직렬화 형태를 사용하면 크게 네가지 면에서 문제가 생긴다.
- 공개 API가 현재의 내부 표현 방식에 영원히 묶인다.
- StringList.Entry가 공개 API가 되면서 StringList 클래스는 연결 리스트 표현 입력을 항상 처리할 수 있어야 한다.
- 너무 많은 공간을 차지할 수 있다.
- 엔트리와 연결 정보는 내부 구현에 해당하니 직렬화 형태에 포함할 가치가 없다.
- 시간이 너무 많이 걸릴 수 있다.
- 직렬화 로직은 객체 그래프의 위상에 관한 정보가 없으니 그래프를 직접 순회해야 한다.
- 스택 오버플로를 일으킬 수 있다.
- 기본 직렬화 과정은 객체 그래프를 재귀 순회하는데 이 작업은 중간 정도 크기의 객체 그래프에서도 자칫 스택 오버플로를 일으킬 수 있다.
- 공개 API가 현재의 내부 표현 방식에 영원히 묶인다.
StringList를 위한 합리적인 직렬화 형태
- 단순히 리스트가 포함한 문자열의 개수를 적은 다음, 그 뒤로 문자열들을 나열하는 수준이면 된다.
- 물리적인 상세 표현은 배제한 채 논리적인 구성만 남게 된다.
- transient 한정자는 해당 인스턴스 필드가 기본 직렬화 형태에 포함되지 않는다는 표시다.
- 해당 객체의 논리적 상태와 무관한 필드라고 확신할 때만 transient 한정자를 생략해야 한다.
// 합리적인 커스텀 직렬화 형태를 갖춘 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());
}
}
}
'책 > 이펙티브자바' 카테고리의 다른 글
Item 89 : 인스턴스 수를 통제해야 한다면 readResolve 보다는 열거 타입을 사용하라 (0) | 2021.09.26 |
---|---|
Item 88 : readObject 메소드는 방어적으로 작성하라 (0) | 2021.09.25 |
Item 86 : Serializable을 구현할지는 신중히 결정하라 (0) | 2021.09.23 |
Item 85 : 자바 직렬화의 대안을 찾으라 (0) | 2021.09.22 |
Item 84 : 프로그램의 동작을 스레드 스케줄러에 기대지 말라 (0) | 2021.09.21 |