요약
•
직렬화 프록시 패턴을 사용하자.
직렬화 프록시
•
직렬화 프록시 패턴(serialization proxy pattern)을 통해 버그와 보안 문제가 일어날 가능성을 낮추자.
•
프록시 패턴은 바깥 클래스의 논리적 상태를 설명하는 중첩 클래스를 private final로 선언하여, 중첩 클래스가 바깥 클래스의 직렬화 프록시가 되도록 하면 된다.
private static class SerializationProxy implements Seralizable {
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;
}
Java
복사
•
위와 같이 이런 중첩 클래스의 생성자는 단 한개여야하며, 바깥 클래스를 매개변수로 받아야 한다. 이 생성자를 통해 인수로 넘어온 인스턴스의 데이터를 복사한다.
•
바깥 클래스와 직렬화 프록시 모두 Serializable을 구현해야하고, 바깥 클래스에 아래와 같은 writeReplace 메서드를 추가해야한다.
private Object writeReplace() {
return new SerializationProxy(this);
}
Java
복사
•
위 메서드를 통해 자바의 직렬화 시스템이 바깥 클래스의 인스턴스 대신 직렬화 프록시의 인스턴스를 반환하게 된다. 다시말해, 직렬화가 되기 전에 바깥 클래스의 인스턴스를 직렬화 프록시로 변환한다.
•
추가적으로 바깥 클래스에 아래와 같은 readObject 메서드를 추가하여 불변식을 훼손하려는 공격을 막아낼 수 있다.
private void readObject(ObjectInputStream stream) throws InvalidObjectException {
throw new InvalidObjectException("프록시가 필요합니다.");
}
Java
복사
•
직렬화 프록시에 바깥 클래스와 논리적으로 동일한 인스턴스를 반환하는 readResolve 메서드를 추가하면, 역직렬화 시에 직렬화 시스템이 직렬화 프록시를 다시 바깥 클래스의 인스턴스로 변환해준다.
// 직렬화 프록시 내 readResolve 메서드
private Object readResolve() {
return new Preiod(start, end);
}
Java
복사
•
직렬화 프록시 패턴은 가짜 바이트 스트림을 만들어 공격하거나 내부 필드를 탈취하는 공격을 프록시 수준에서 차단해준다.
•
또한 직렬화 프록시에서는 Period의 필드를 final로 선언해도 되므로, 진정한 불변 객체로 만들 수 있다.
•
이런 이유들로 어떤 필드가 공격의 목표가 될 지 고민하지 않아도 되고, 역직렬화 때 유효성 검사를 할 필요도 없다.
•
아래는 EnumSet의 직렬화 프록시 예시 코드이다.
private static class SerializationProxy<E extends Enum<E>> inplements Serializable {
// EnumSet의 원소 타입
private final Class<E> elementType;
// EnumSet의 원소들
private final Enum<?>[] elements;
SerializationProxy(EnumSet<E> set) {
elementType = set.elementType;
elements = set.toArray(new Enum<?>[0]);
}
private Object readResolve() {
EnumSet<E> result = EnumSet.noneOf(elementType);
for (Enum<?> e : elements)
result.add((E) e);
return result;
}
private static final long serialVersionUID = 3624911234563181265L;
}
Java
복사
직렬화 프록시의 한계
•
직렬화 프록시는 클라이언트가 멋대로 확장할 수 있는 클래스에는 적용할 수 없다.
•
직렬화 프록시는 객체 그래프에 순환이 있는 클래스에 적용할 수 없다.
◦
순환이 있는 객체의 메서드를 직렬화 프록시의 readResolve 안에서 호출하면, 직렬화 프록시만 있고 아직 실제 객체가 없기 때문에 ClassCastException이 발생할 것이다.
•
직렬화 프록시 패턴을 적용하면 방어적 복사보다 성능이 저하된다.