11장 — 직렬화

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

  • Serializable 구현과 관련된 가장 큰 문제는 일단 클래스를 릴리스하고 나면 클래스 구현을 유연하게 바꾸기 어려워진다는 것이다.
  • Serializable을 구현하면 생기는 두 번째 문제는, 버그나 보안 취약점(security hole)이 발생할 기능성이 높아진다는 것이다.
  • Serialization을 구현하면 생기는 세 번째 문제는, 새 버전 클래스를 내놓기 위한 데스트 부담이 늘어난다는 것이다.
  • Serializable 인터페이스를 구현한다는 것은 가벼이 볼 수 없는 결정이다.
  • 계승을 염두에 두고 설계하는 클래스는(규칙 17) Serializable을 구현하지 않는 것이 바람직하다. 또한 인터페이스는 가급적 Serializable을 계승하지 말아야 한다.
  • 계승을 고려해 설계한 직렬화 불가능 클래스에는 무인자 생성자를 제공하는 것이 어떨지 반드시 따져봐야 한다.
  • 내부 클래스(inner class)는(규칙 22) Serializable을 구현하면 안 된다.
  • 내부 클래스의 기본 직렬화 형식은 정의될 수 없다(ill-defined).

규칙 75 사용자 지정 직렬화 형식을 사용하면 좋을지 따져 보라

  • 어떤 직렬화 형식이 적절할지 따져보지도 않고 기본 직렬화 형식(default serialized form)을 그대로 받아들이지 마라.
  • 기본 직렬화 형식은 그 객체의 물리적 표현이 논리적 내용과 동일할 때만 적절하다.
  • 설사 기본 직렬화 형식이 만족스럽다 하더라도, 불변식(invariant)이나 보안 (security) 조건을 만족시키기 위해서는 readObject 메서드를 구현해야 마땅한 경우도 많다.
  • 객체의 물리적 표현 형태가 논리적 내용과 많이 다를 경우 기본 직렬화 형식을 그대로 빈아들이면 아래의 네 가지 문제가 생기게 된다.
  • 객체의 모든 필드가 transient 일 때는 defaultWriteObject나 defaultReadObject를 호출하지 않는 것도 기술적으로 가능하긴 하지만 권장하는 사항은 아니다.
  • 객체의 논리적 상태를 구성하는 값이라는 확신이 들기 전에는 비-transient 필드로 만들어야겠다는 결정을 내리지 마라.
  • 객체를 직렬화 할 때는 객체의 상태 전부를 읽는 메서드에 적용할 동기화 수단을 반드시 적용해야 한다.
  • 어떤 직렬화 형식을 이용하건, 직렬화 가능 클래스를 구현할 때는 직렬 버전 UID(serial version UID)를 명시적으로 선언해야 한다.

규칙 76 readObject 메서드는 방어적으로 구현하라

  • 객체를 역으로 직렬화할 때는 클라이언트가 가질 수 없어야 하는 객체 참조를 담은 모든 필드를 방어적으로 복사하도록 해야 한다.
  • writeUnshared와 readUnshared 메서드는 사용하지 마라.

규칙 77 개체 통제가 필요하다면 readResolve 대신 enum 자료형을 이용하라

  • 사실, 개체 통제를 위해 readResolve를 활용할 때는, 객체 참조 자료형으로 선언된 모든 객체 필드를 반드시 transient로 선언해야 한다.
  • readResolve 메서드의 접근 권한은 중요하다.

규칙 78 직렬화된 객체 대신 직렬화 프락시를 고려해 보라

  • 직렬화 프락시 패턴
    • 바깥 클래스 객체의 논리적 상태를 간결하게 표현하는 직렬화 기능 클래스를 private static 중첩 클래스로 설계
    • 중첩 클래스를 직렬화 프락시(serialization proxy)라고 부름
    • 바깥 클래스를 인자 자료형으로 사용하는 생성자를 하나만 가짐
    • 생성자는 인자에서 데이터를 복사
public final class Period implements Serializable {
	private final Date start;
	private final Date end;

	public Period(Date start, Date end) {
		this.start = new Date(start.getTime());
		this.end = new Date(end.getTime());
		if (this.start.compareTo(this.end) > 0)
			throw new IllegalArgumentException(start + " after " + end);
	}

	public Date start() {
		return new Date(start.getTime());
	}

	public Date end() {
		return new Date(end.getTime());
	}

	public String toString() {
		return start + " - " + end;
	}

	private static class SerializationProxy implements Serializable {
		private final Date start;
		private final Date end;

		SerializationProxy(Period p) {
			this.start = p.start;
			this.end = p.end;
		}

		private static final long serialVersionUID = 234098243823485285L;

		private Object readResolve() {
			return new Period(start, end);
		}
	}

	private Object writeReplace() {
		return new SerializationProxy(this);
	}

	private void readObject(ObjectInputStream stream)
			throws InvalidObjectException {
		throw new InvalidObjectException("Proxy required");
	}
}