Search

Item 90. 직렬화된 인스턴스 대신 직렬화 프록시 사용을 검토하라

생성일
2023/08/25 04:02
챕터
12장 - 직렬화

요약

직렬화 프록시 패턴을 사용하자.

직렬화 프록시

직렬화 프록시 패턴(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이 발생할 것이다.
직렬화 프록시 패턴을 적용하면 방어적 복사보다 성능이 저하된다.