Skip to content

Latest commit

 

History

History
133 lines (100 loc) · 5.03 KB

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

File metadata and controls

133 lines (100 loc) · 5.03 KB

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

괜찮다고 판단될 때만 기본 직렬화 형태를 사용하라

  • 기본 직렬화 형태는 유연성, 성능, 정확성 측면에서 고민 후 합당할 때만 사용해야 한다.

객체의 물리적 표현과 논리적 내용이 같다면 기본 직렬화 형태라도 무방하다.

  • 물리적 표현 : 코드로 어떻게 구현했는지
  • 논리적 내용 : 실제로 어떤 것을 의미하는지
// 기본 직렬화에 적합
public class Name implements Serializable {
    
    private final Stirng lastName;

    private final String firstName;

    private final String middleName;
}
  • 기본 직렬화가 적합하다고 결정해도 불변식 보장과 보안을 위해 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;
    }
    // ... 생략
}
  • 공개API가 현재의 내부 표현 방식에 영구히 묶인다.
    • 연결리스트를 더 이상 사용하지 않더라도 관련 코드를 제거할 수 없다.
  • 너무 많은 공간을 차지할 수 있다.
    • 앞의 예에서 내부 구현에 해당하는 정보까지 모두 직렬화 형태에 포함됨
    • 네트워크 전송 속도만 느려짐
  • 시간이 너무 많이 걸릴 수 있다.
    • 직렬화 로직은 객체 그래프의 위상에 관한 정보를 알 수 없으니, 직접 순회할 수 밖에 없음
  • 스택 오버플로를 일으킬 수 있다.
    • 기본 직렬화 과정은 객체 그래프를 재귀 순회하는데, 스택 오버플로 일어킬 수도 있다.

합리적 직렬화 형태 (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());
    }

    // 나머지 코드는 생략
}
  • writeObjectreadObject가 직렬화 형태를 처리한다.
  • transient 한정자는 해당 인스턴스 필드가 기본 직렬화 형태에 포함되지 않는다는 표시다.
    • 여기서 defaultWriteObject() 메서드는 기본 직렬화하는데 포함 안된다.
  • 해당 객체의 논리적 상태와 무관한 필드라고 확신할 때만 transient 한정자를 생략해야한다.
  • 기본 직렬화를 하면 transient 필드들은 역직렬화 할때 기본값으로 초기화 된다.

serialVersionUID

  • 어떤 직렬화 형태를 택하든 직렬화 가능 클래스 모두에 직렬 버전 UID를 명시적으로 부여하자.
  • 잠재적 호환성 문제 사라짐 (아이템 86참조)
// 무작위로 고른 long 값 (고유할 필요 없음)
private static final long serialVersionUID = 234L
  • 구버전으로 직렬화된 인스턴스들과의 호환성을 끊으려는 경우를 제외하고는 직렬 버전 UID를 절대 수정하지 말자.

요약

  • 기본 직렬화 형태는 객체를 직렬화한 결과가 해당 객체의 논리적 표현에 부합할 때만 사용하고, 아니면 적절히 설명하는 커스텀 직렬화 형태를 고안하라
  • 직렬화 형태도 공개 메서드를 설계할 때에 준하는 시간을 들여 설계해야한다.
  • 직렬화 호환성을 유지하기 위해서 영원히 지원해야하기 때문이다.