Search

Item 87. 커스텀 직렬화 형태를 고려해보라

생성일
2023/08/24 07:04
챕터
12장 - 직렬화

요약

기본 직렬화 형태와 커스텀 직렬화 형태를 상황에 맞게 잘 고려해서 사용하자.

직렬화 형태 선택

고민해보고 괜찮다고 판단될 때만 기본 직렬화 형태를 사용하자.
유연성, 성능, 정확성 측면에서 신중히 고민 후 합당할 때만 사용해야 한다.
직접 설계하더라도 기본 직렬화 형태와 거의 같은 결과가 나올땜ㄴ 기본 형태를 쓰자.
기본 직렬화 형태가 적합하다고 판단했더라도, 불변성과 보안을 위해 readObject 메서드를 제공해야 할 때가 많다.
객체의 물리적 표현과 논리적 내용이 같다면, 기본 직렬화 형태를 사용해도 된다.
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; } ... }
Java
복사
이와 같이 물리적 표현과 논리적 내용이 차이가 클 때 기본 직렬화를 사용하면 4가지 측면에서 문제가 생긴다.
현재의 내부 표현 방식이 직렬화로 인한 공개 API에 의해 영구히 묶이게 된다.
다음 릴리즈에서 내부 표현 방식을 바꾸더라도, 연결 리스트 표현된 입력에 대해서 여전히 처리할 수 있어야 한다.
그로 인해 연결 리스트는 더 이상 사용하지 않아도 관련 코드를 제거할 수 없다.
불필요한 요소까지 포함하여, 너무 많은 공간을 차지할 수 있다.
기본 형태를 사용하면 연결 리스트의 모든 엔트리와 관련된 모든 정보를 기록하게 되는데, 연결 정보는 내부 구현에 해당하기 때문에 직렬화 형태에 포함할 필요가 없다.
시간이 너무 많이 걸릴 수 있다.
객체 그래프의 위상에 관한 정보가 없으니 그래프를 직접 순회해야한다.
스택 오버플로우를 발생시킬 수 있다.
기본 직렬화 과정은 객체 그래프를 재귀 순회하는데, 이로인해 스택 오버플로우가 발생할 수 있다.
위 예시 코드에서 직렬화를 올바르게 사용하려면, 아래와 같이 커스텀 직렬화 형태를 사용하면 된다.
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; } 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()); } ... }
Java
복사
transient 키워드를 사용하자
만약 필드가 모두 transient 이어도, writeObject와 readObject에서 defaultWriteObject와 defaultReadObject는 꼭 호출하자. 그래야 차후 릴리즈에서 transient가 아닌 필드가 생겼을 때 호환이 가능하다.
transient는 해당 객체의 논리적 상태와 무관한 필드라고 확신할 때만 생략하고, 그 외에는 대부분의 인스턴스 필드를 transient를 선언해야 한다.
기본 직렬화를 사용하면 transient 필드들은 기본값으로 초기화 된다.
어떤 직렬화 형태를 사용하든, 모든 메서드에 synchronized로 선언하여 스레드 안전한 객체에서는 아래처럼 writeObject도 동기화 해야한다.
private synchronized void writeObject(ObjectOutputStream s) throws IOException { s.defaultWriteObject(); }
Java
복사
어떤 직렬화 형태를 사용하든, 직렬화 가능 클래스 모두 SerialVersionUID를 명시적으로 부여하자.
SUID가 발생시킬 수 있는 잠재적 호환성 문제가 전부 사라진다.
또한 런타임에 해당 값을 생성하는 복잡한 연산이 생략되어 성능도 개선된다.
구버전으로 직렬화된 인스턴스들과의 호환성을 끊으려는 경우를 제외하고는 SUID를 절대 수정하지 말자.