-
item 87 커스텀 직렬화 형태를 고려해보라책/이펙티브 자바 2022. 4. 28. 14:37
ITEM 87 커스텀 직렬화 형태를 고려해보라
먼저 고민해보고 괜찮다고 판단될 때만 기본 직렬화 형태를 사용하라
- 기본 직렬화 형태는 유연성, 성능, 정확성 측면에서 신중히 고민한 후 합당할 때만 사용해야 한다
- 어떤 객체의 기본 직렬화 형태는 그 객체를 루트로 하는 객체 그래프의 물리적 모습을 나름 효율적으로 인코딩 한다
- 객체가 포함한 데이터와 그 객체부터 시작해 접근할 수 있는 모든 객체를 담아내고 이 객체들이 연결된 위상까지 기술한다
- 이상적인 직렬화 형태면 물리적인 모습과 독립된 논리적인 모습만 표현해야 한다
객체의 물리적 표현과 논리적 내용이 같다면 기본 직렬화 형태라도 무방하다
사람의 성명을 간략하게 표현한 코드 기본 직렬화 형태를 써도 괜찮다 public class Name implements Serializable { /** * 성. null이 아니어야 함. * @serial * / private final String lastName; /** * 이름. null이 아니어야 함. * @serial * / private final String firstName; /** * 중간이름. 중간이름이 없다면 null * @serial * / private final String middleName; ... // 나머지 코드 생략 } 성명은 논리적으로 이름, 성, 중간이름 3개의 문자열로 구성된다 위 코드는 인스턴스 필드들이 이 논리적 구성요소를 정확히 반영하고 있다
기본 직렬화 형태가 적합하다고 결정했더라도 불변식 보장과 보안을 위해 readObject 메서드를 제공해야 할 때가 많다
- 앞의 Name 클래스의 경우에는 readObject 메서드가 lastName과 firstName 필드가 null이 아님을 보장해야 한다
Name의 세 필드 모두 private임에도 문서화 주석이 달려 있는데 이 필드들은 결국 클래스의 직렬화 형태에 포함되는 공개 API에 속하며 공개 API는 모두 문서화해야 하기 때문이다
기본 직렬화 형테에 적합하지 않은 클래스 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; } ... //나머지 코드 생략 } 논리적으로 이 클래스는 일련의 문자열을 표현하며 물리적으로는 문자열들을 이중 연결 리스트로 연결했다 이 클래스에 기본직렬화 형태를 사용하면 각 노드의 양방향 연결 정보를 포함해 모든 엔트리(Entry)를 철두철미하게 기록한다
객체의 물리적 표현과 논리적 표현의 차이가 클 때 기본 직렬화 형태를 사용하면 크게 4가지 문제가 생긴다
- 공개 API가 현재의 내부 표현 방식에 영구히 묶인다
- 위 예제에서 private 클래스인 StringList.Entry가 공개 API가 된다
- 다음 배포시 내부 표현식을 바꿔도 StringList 클래스는 여전히 연결 리스트로 표현된 입력도 처리할 수 있어야 된다
- 연결리스트를 더는 사용하지 않더라도 관련 코드를 절대 제거할 수 없다
- 너무 많은 공간을 차지할 수 있다
- 위 예제에서 직렬화 형태는 연결 리스트의 모든 엔트리와 연결 정보까지 기록했지만 엔트리와 연결 정보는 내부 구현에 해당하니 직렬화 형태에 포함할 가치가 없다
- 이처럼 직렬화 형태가 너무 커져서 디스크에 저장하거나 네트워크로 전송하는 속도가 느려진다
- 시간이 너무 많이 걸릴 수 있다
- 직렬화 로직은 객체 그래프의 위상에 관한 정보가 없으니 그래프를 직접 순회해볼 수밖에 없다
- 스택 오버플로를 일으킬 수 있다
- 기본 직렬화 과정은 객체 그래프를 재귀 순회하며 이 작업은 중간 정도 크기의 객체 그래프에서도 자칫 스택 오버플로를 일으킬 수 있다
해당 객체의 논리적 상태와 무관한 필드라고 확신하는 경우에만 transient 한정자를 생략하자
- 커스텀 직렬화 형태를 사용한다면 앞의 StringList 같이 대부분, 혹은 모든 인스턴스 필드를 transient로 선언해야 한다
- 기본 직렬화를 사용한다면 transient 필드들은 역직렬화 될 때 기본값으로 초기화되는걸 잊지말자
기본 직렬화 사용 여부와 상관없이 객체의 전체 상태를 읽는 메서드에 적용해야 하는 동기화 메커니즘을 직렬화에도 적용해야 한다
기본 직렬화를 사용하는 동기화된 클래스를 위한 writeObject 메서드 private synchronized void writeObject(ObjectOutputStream s) throws IOException { s.defaultWriteObject(); } 모든 메서드를 synchronized로 선언해 스레드 안전하게 만든 객체에서 기본 직렬화를 사용하려면 writeObject도 위 코드처럼 synchronized로 선언해야 한다 writeObject 메서드 안에서 동기화하고 싶다면 클래스의 다른 부분에서 사용하는 락 순서를 똑같이 따라야 하며 그렇지 않으면 자원 순서 교착상태에 빠질 수 있다
어떤 직렬화 형태를 택하든 직렬화 가능 클래스 모두에 직렬 버전 UID를 명시적으로 부여하자
- 이러면 직렬 버전 UID가 일으키는 잠재적인 호환성 문제가 사라진다
- 직렬 버전 UID를 명시하지 않으면 런타임시 이 값을 생성하느라 복잡한 연산을 수행하기 때문에 명시적으로 부여하면 성능도 조금 빨라진다
직렬 버전 UID 선언 (각 클래스에 아래 같은 한줄만 추가하면 끝) private static final long serialVersionUID = <무작위로 고른 long 값>;
구버전으로 직렬화된 인스턴스들과의 호환성을 끊으려는 경우를 제외하고는 직렬 버전 UID를 절대 수정하지 말자
클래스를 직렬화하기로 했다면 어떤 직렬화 형태를 사용하리 심사숙고하자
자바의 기본 직렬화 형태는 객체를 직렬화한 결과가 해당 객체의 논리적 표현에 부합할 때만 사용하고 그렇지 않으면 객체를 적절히 설명하는 커스텀 직렬화 형태를 고안하자
직렬화 형태도 공개 메서드를 설계할 때에 준하는 시간을 들여 설계해야 한다
한번 공개된 메서드는 향후 릴리스에서 제거할 수 없듯이 직렬화 형태에 포함된 필드도 마음대로 제거할 수 없다
직렬화 호환성을 유지하기 위해 영원히 지원해야 하는 것이다
잘못된 직렬화 형태를 선택하면 해당 클래스의 복잡성과 성능에 영구히 부정적인 영향을 남긴다
'책 > 이펙티브 자바' 카테고리의 다른 글
item 89 인스턴스 수를 통제해야 한다면 readResolve 보다는 열거 타입을 사용하라 (0) 2022.04.28 item 88 readObject 메서드는 방어적으로 작성하라 (0) 2022.04.28 item 86 Serializable을 구현할지는 신중히 결정하라 (0) 2022.04.26 item 85 자바 직렬화의 대안을 찾으라 (0) 2022.04.26 item 84 프로그램의 동작을 스레드 스케줄러에 기대지 말라 (0) 2022.04.25