Item 74. Serializable 인터페이스를 구현할 때는 신중하라

Serializable 구현시 생각해야 하는 것들

객체 필드 직렬화시 유의 사항

객체 필드를 갖는 클래스를 직렬화 가능하고 계승 가능한 클래스로 구현할 때는 반드시 조심해야 할 것이 하나 있다. 객체 필드가 기본값으로 초기화되면 위배되는 불변식이 있는 경우 (정수 : 0, boolean : false, 일반 객체 : null등과 같이 default 초기화가 일어나면 안되는 경우)에는 아래의 readObjectNoData 함수를 클래스에 반드시 추가해야 한다.

// 상태유지 계승 가능 직렬화 가능
// 클래스에 대한 readObjectNoData 함수 
private void readObjectNoData() throws InvalidObjectException {
  throw new InvalidObjectException("Stream data requied");
}

계승을 고려해 설계한 직렬화 불가능 클래스에는 무인자 생성자를 제공하는 것이 어떨지 따져봐야 한다.

// 직렬화가 불가능한 상태유지(stateful) 클래스.
// 하지만 직렬화가 가능한 자식 클래스를 만들 수 있다.
public abstract class AbstractFoo{
  private int x, y; // 상태

  // 아래 enum과 필드는 초기화 과정을 추적하기 위한 것이다.
  private enum State { NEW, INITIALIZING, INITIALIZED };
  /**
   * 가령 어떤 스레드가 객체에 initialize를 호출하는 순간에 두 번째 스레드가 
   * 그 객체를 사용하려 한다고 해 보자. 그 두 번째 스레드는 상태가 깨진 객체를 이용하게 될 수 있다. 
   * compareAndSet을 사용해 enum에 대한 참조를 원자적으로 조작하는 이 패턴은, 
   * 다목적(general-purpose) 스레드 안전 상태 기계(thread safe state machine)를 구현하기 좋다. 
   */
  private final AtomicReference<State> init = 
    new AtomicReference<State>(State.NEW);

  public AbstractFoo(int x, int y) { initialize(x, y); }

  // 이 생성자와 그 아래 함수는 자식 클래스의 readObject 함수가
  // 상태를 초기화할 수 있도록 하기 위한 것이다.
  protected AbstractFoo() { }
  protected final void initialize(int x, int y){
    if (!init.compareAndSet(State.NEW, State.INITIALIZING))
      throw new IllegalStateException("Already initialized");

    this.x = x;
    this.y = y;
    ...//원래 생성자가 하던 나머지 작업
    init.set(State.INITIALIZED);
  }

  // 이 함수들은 자식 클래스의 writeObject 함수에 의해 객체가
  // 수동적으로 직렬화될 수 있도록 내부 상태 정보를 제공하는 역할을 한다.
  protected final int getX() { checkInit(); return x; }
  protected final int getY() { checkInit(); return y; }

  // 모든 public 및 protected 객체 함수에서 반드시 호출해야 하는 함수
  private void checkInit(){
    if(init.get() != State.INITIALIZED)
      throw new IllegalStateException("Uninitialized");
  }

  ... // 이하 생략
}

다목적(general-purpose) 스레드 안전 상태 기계(thread safe state machine)를 구현하면, 직렬화 가능 자식 클래스를 구현하기가 쉽다.

// 직렬화 불가능 상태유지(stateful) 클래스의 직렬화 가능 자식 클래스
public class Foo extends AbstractFoo implements Serializable{
  private void readObject(ObjectInputStream s)
    throw IOException, ClassNotFountException{
      s.defaultReadObject();

      // 부모 클래스 상태를 수동으로 역직렬화 한 다음 초기화
      int x = s.readInt();
      int y = s.readInt();
      initialize(x, y);
    }

    private void writeObject(ObjectOutputStream s) throws IOException {
      s.defaultWriteObject();

      // 부모 클래스 상태를 수동으로 직렬화
      s.writeInt(getX());
      s.writeInt(getY());
    }

    // 생성자는 이 메커니즘과 상관없음
    public Foo(int x, int y) { super(x, y); }

    private static final long serialVersionUID = 2213124123213123L;
}

내부 클래스(inner class)는 Serializable을 구현하면 안된다.

내부 클래스에는 바깥 객체에 대한 참조를 보관하고 바깥 유효범위의 지역 변수 값을 보관하기 위해 컴파일러가 자동으로 생성하는 인위생성 필드(synthetic field)가 있다. 익명 클래스나 지역 클래스 이름과 마찬가지로, 자바 명세서에는 이런 필드가 클래스 정의에 어떻게 들어맞는지 나와 있지 않다. 따라서 내부 클래스의 직렬화 형식은 정의될 수 없다. 하지만 정적 멤버 클래스는 Serializable을 구현해도 된다.

결론