[EffectiveJava] item90 - 직렬화된 인스턴스 대신 직렬화 프록시 사용을 검토하라
19 Aug 2023이번 장 전반에서 이야기 했듯, Serializable을 구현하기로 결정한 순간 언어의 정상 메커니즘인 생성자 이외의 방법으로 인스턴스를 생성할 수 있게 된다.
이는 버그와 보안 문제가 일어날 가능성이 커진다는 뜻이다. 하지만 이 위험을 직렬화 프록시 패턴(serialization proxy pattern)을 이용하면 크게 줄 일 수 있다.
직렬화 프록시 패턴
바깥 클래스의 논리적 상태를 정밀하게 표현하는 중첩 클래스를 설계해 private static
으로 선언한다. 이 중첩 클래스가 바깥 클래스의 직렬화 프록시다.
중첩 클래스의 생성자는 단 하나여야 하며, 바깥 클래스를 매개변수로 받아야 한다. 이 생성자는 단순히 인수로 넘어온 인스턴스 데이터를 복사한다. 그리고 바깥 클래스와 직렬화 프록시 모두 Serializable
을 구현한다고 선언해야 한다.
Period 클래스용 직렬화 프록시
public class SerializationProxy implements Serializable {
private static final long serialVersionUID = 1L;
private final Date start;
private final Date end;
public SerializationProxy(Period p) {
this.start = p.start();
this.end = p.end();
}
}
다음으로 바깥 클래스에 다음의 writeReplace 메서드를 추가한다. 이 메서드는 범용적이니 직렬화 프록시를 사용하는 모든 클래스에 그대로 복사해 쓰면 된다.
직렬화 프록시 패턴용 writeReplace 메서드
public class Period implements Serializable {
...
private Object writeReplace() {
return new SerializationProxy(this);
}
}
이 메서드는 자바의 직렬화 시스템이 바깥 클래스의 인스턴스 대신 SerializationProxy의 인스턴스를 반환하게 하는 역할을 한다. 달리 말해, 직렬화가 이뤄지기 전에 바깥 클래스의 인스턴스를 직렬화 프록시로 변환해준다.
만약 바깥 클래스의 직렬화된 인스턴스를 생성하기 위한 공격이 들어온다면 다음과 같은 메서드를 추가해서 막아낼 수 있다.
직렬화 프록시 패턴용 readObject 메서드
public class Period implements Serializable {
...
private void readObject(ObjectInputStream stream) throws InvalidObjectException {
throw new InvalidObjectException("프록시가 필요합니다.");
}
}
마지막으로, 바깥 클래스와 논리적으로 동일한 인스턴스를 반환하는 readResolve 메서드를 SerializationProxy
클래스에 추가한다.
이 메서드는 역직렬화 시에 직렬화 시스템이 직렬화 프록시를 다시 바깥 클래스의 인스턴스로 변환하게 해준다.
Period.SerializationProxy용 readResolve
public class SerializationProxy implements Serializable {
...
private Object readResolve() {
return new Period(start, end); /* public 생성자를 사용한다. */
}
}
직렬화 프록시 패턴의 장점
직렬화의 특성 제거
직렬화는 생성자를 이용하지 않고도 인스턴스를 생성하는 기능을 제공하는데, 이 패턴은 직렬화이 이러한 특성을 상당 부분 제거한다.
즉, 일반 인스턴스를 만들 때와 똑같은 생성자, 정적 팩터리, 혹은 다른 메서드를 사용해 역직렬화된 인스턴스를 생성하므로 역직렬화된 인스턴스가 해당 클래스의 불변식을 만족하는지 검사할 또다른 수단을 강구하지 않아도 된다.
외부 공격 방어
item88의 방어적 복사처럼 가짜 바이트 스트림 공격과 내부 필드 탈취 공격을 프록시 수준에서 차단해준다.
직렬화를 사용하는 불변 객체 생성
직렬화 프록시는 앞서 만든 필드를 final로 선언해도 되므로 진정한 불변 클래스를 생성할 수 도 있다.
상이한 클래스 끼리의 작동 여부
역직렬화한 인스턴스와 원래의 직렬화된 인스턴스의 클래스가 달라도 정상 작동한다. (예시: EnumSet)
직렬화 프로시 패턴의 한계
- 클라이언트가 멋대로 확장할 수 있는 클래스에는 적용할 수 없다.
- 객체 그래프에 순환이 있는 클래스에도 적용할 수 없다. 직렬화 프록시만 가졌을 뿐 실제 객체는 아직 만들어진 것이 아니기 때문이다.
- 직렬화 프록시 패턴이 주는 강력함과 안정성에는 성능과 같은 대가가 따른다.