• 먼저 고민해보고 괜찮다고 판단될 때만 기본 직렬화 형태를 사용하라
  • 직접 설계하더라도 기본 직렬화 형태와 거의 같은 결과가 나올 경우에만 기본형태를 써야한다.
  • 객체의 물리적 표현과 논리적 내용이 같다면 기본 직렬화 형태라도 무방하다.
// 기본 직렬화 형태에 적합한 후보

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 클래스는 연결 리스트 표현 입력을 항상 처리할 수 있어야 한다.
    • 너무 많은 공간을 차지할 수 있다.
      • 엔트리와 연결 정보는 내부 구현에 해당하니 직렬화 형태에 포함할 가치가 없다.
    • 시간이 너무 많이 걸릴 수 있다.
      • 직렬화 로직은 객체 그래프의 위상에 관한 정보가 없으니 그래프를 직접 순회해야 한다.
    • 스택 오버플로를 일으킬 수 있다.
      • 기본 직렬화 과정은 객체 그래프를 재귀 순회하는데 이 작업은 중간 정도 크기의 객체 그래프에서도 자칫 스택 오버플로를 일으킬 수 있다.

 

 

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());
        }
    }
}

+ Recent posts