직렬화 프록시 패턴
- 저번주의 직렬화 전반에 걸쳐 이야기하고 Item 88, 89에서 언급했듯이
Serializable
을 구현하기로 결정한 그 순간부터 언어의 정상 메커니즘인 생성자 이외의 방법으로 인스턴스를 생성할 수 있게 됩니다.
- 이런 특성 때문에 버그와 보안 문제가 일어날 가능성이 커진다는 의미입니다. 하지만 다행히도, 이런 위험을 크게 줄여줄 기법이 하나 있습니다. 그것이 바로 이번 장에서 소개할 직렬화 프록시 패턴입니다.
프록시 패턴이란?
직렬화 프록시 패턴 구현
- 해당 패턴을 구현하는 것은 그리 복잡하지 않습니다. 먼저 바깥 클래스의 논리적 상태를 정밀하게 표현하는 중첩 클래스를 설계에
private static
으로 선언합니다.
- 이때 중첩 클래스에도
implements Serializable
을 선언해주어 기본 직렬화를 사용하게 합니다.
- 해당 중첩 클래스가 바깥 클래스의 직렬화 프록시가 될 것입니다. 이때 중첩 클래스의 생성자는 단 하나여야 하며, 바깥 클래스를 매개변수로 받아야 합니다.
- 이 생성자는 단순히 인수로 넘어온 인스턴스의 데이터를 복사합니다. 이때 주목할 점은 일관성 검사나 방어적 복사를 전혀 진행하지 않고 복사해도 된다는 사실입니다.
- 이렇게 생성된 프록시 객체의 기본 직렬화 형태는 바깥 클래스의 직렬화 형태로 쓰기에 적합합니다.
- 아래는 이전에 우리가 봤던
Period
객체의 직렬화 프록시 클래스입니다.
private static class SerializationProxy implements Serializable {
private static final long serialVersionUID = 1L;
private final Date start;
private final Date end;
SerializationProxy(Period p) {
this.start = p.start;
this.end = p.end;
}
}
- 이후 외부 클래스에 다음의 writeReplace 메서드를 추가합니다.
private Object writeReplace() {
return new SerializationProxy(this);
}
writeReplace
메서드는 직렬화 시스템이 바깥 클래스의 인스턴스 대신 SerializationProxy
의 인스턴스를 반환하게 하는 역할을 합니다. 즉 외부의 Period
인스턴스를 직렬화하기 이전에 직렬화 프록시로 변환하는 역할을 수행합니다.
writeReplace
메서드 덕분에 직렬화 시스템은 절대로 바깥 클래스의 직렬화된 인스턴스를 생성할 수 없습니다. 하지만 공격자는 불변식을 손상시키기 위해 여러 시도를 할 수 있습니다.
- 이를 해결하는 방법은 외부 클래스에
readObject
메서드를 다음과 같이 구현하면 막아낼 수 있습니다.
private void readObject(ObjectInputStream ois) throws InvalidObjectException {
throw new InvalidObjectException("need proxy object");
}
- 마지막으로 바깥 클래스와 논리적으로 동일한 인스턴스를 반환하는
readResolve
메서드를 SerializationProxy
클래스에 추가합니다. 이 메서드는 역직렬화 시 직렬화 시스템이 직렬화 프록시를 다시 바깥 클래스의 인스턴스로 변환하게 해줍니다.