아이템 90. 직렬화된 인스턴스 대신 직렬화 프록시 사용을 검토하라
Serializable을 구현한 순간 언어 정상 매커니즘인 생성자 이외의 방법으로 인스턴스를 생성할 수 있게된다.
이것은 버그와 보안 문제가 일어날 가능성이 크다.
하지만 직렬화 프록시 패턴이 이 위험을 줄여줄 수 있다.
바깥 클래스의 논리적 상태를 표현하는 중첩 클래스를 설계하여 private static으로 선언한다.
여기서 중첩 클래스가 바깥 클래스의 직렬화 프록시다.
중첩 클래스의 생성자는 단 하나여야 하며, 바깥 클래스를 매개변수로 받아야한다. 일관성 검사나 방어적 복사가 필요없다.
public final class Period implements Serializable {
private final Date start ;
private final Date end ;
/**
* @param start 시작 시각
* @param end 종료 시각; 시작 시각보다 뒤여야 한다.
* @throws IllegalArgumentException 시작 시각이 종료 시각보다 늦을 때 발생한다.
* @throws NullPointerException start나 end가 null이면 발행한다.
*/
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 + "가 " + end + "보다 늦다." );
}
public Date start () { return new Date (start .getTime ()); }
public Date end () { return new Date (end .getTime ()); }
public String toString () { return start + " - " + end ; }
// 코드 90-1 Period 클래스용 직렬화 프록시 (479쪽)
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 ;
}
/**
* Deserialize 할 때 호출된다. 바깥 클래스의 인스턴스로 변환한다.
* 장점으로 일반 인스턴스를 만들 때와 똑같은 생성자, 정적 팩터리를 사용해 인스턴스를 생성할 수 있다.
*/
private Object readResolve () {
return new Period (start , end );
}
private static final long serialVersionUID = 34098243823485285L ; // 아무 값이나 상관없다. (아이템 87)
}
// 직렬화 프록시 패턴용 writeReplace 메서드
// 바깥 클래스의 직렬화된 인스턴스를 생성할 수 없게한다.
// 직렬화할 때 호출되는데, 프록시를 반환하게 하고 있다.
private Object writeReplace () {
return new SerializationProxy (this );
}
// 직렬화 프록시 패턴용 readObject 메서드 (480쪽)
// 추가해서 불변식 훼손 공격을 막아낼 수 있다.
private void readObject (ObjectInputStream stream ) throws InvalidObjectException {
throw new InvalidObjectException ("프록시가 필요합니다." );
}
}
직렬화 프록시는 Period의 필드를 final로 선언해도 되므로 불변으로 만들 수 있다.
또한 직렬화 프록시 패턴은 역직렬화한 인스턴스와 원래의 직렬화된 클래스가 달라도 정상적으로 동작한다.
예를들어 직렬화는 RegularEnumSet
을 사용하고, 역직렬화는 JumboEnumSet
사용
클라이언트가 마음대로 확장할 수 있는 클래스에는 적용할 수 없다.
객체가 서로 참조하는 상황, 그러니까 객체 그래프에 순환이 있는 클래스에도 적용할 수 없다. 이런 객체의 메서드를 직렬화 프록시의 readResolve 안에서 호출하려 하는 경우 예외가 발생할 것이다. 직렬화 프록시만 가진 것이지 실제 객체는 아직
만들어지지 않았기 때문이다.
방어적 복사보다 상대적으로 속도도 느리다.