Item 90. Serialization Proxy Pattern

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

Serializable 인터페이스를 구현하는 순간 정상적인 생성자 이외의 방법으로 인스턴스를 생성할 수 있게 되면서, 버그나 보안 문제에 노출되게 된다. 이런 위험을 크게 줄여주는 기법으로 직렬화 프록시 패턴이 있다.

구현 방법

직렬화 프록시 패턴의 구현 방법은 다음과 같다.

  • 바깥 클래스의 논리적 상태를 표현하는 중첩 클래스를 설계해 private static으로 선언

  • 중첩 클래스의 생성자는 단 하나로 제한하며, 바깥 클래스를 매개변수로 받아 단순히 넘어온 인스턴스 데이터를 복사

  • 바깥 클래스와 직렬화 프록시 모두 Serializable을 구현


class Period implements Serializable {

    // final 키워드 사용
    private final Date start;
    private final Date end;

    public Period(Date start, Date end) {
        this.start = start;
        this.end = end;
    }

    // 직렬화할 객체를 대체할 다른 객체를 반환하는 메서드
    private Object writeReplace() {
        return new SerializationProxy(this);
    }

    // 역직렬화 과정에서 호출
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
        throw new InvalidObjectException("Proxy required");
    }

    private static class SerializationProxy implements Serializable {

        private static final long serialVersionUID = 234098243823485285L; // 아무 값이나 상관 없음
        private final Date start;
        private final Date end;

        SerializationProxy(Period p) {
            this.start = p.start;
            this.end = p.end;
        }

        // 역직렬화된 객체를 대신할 객체를 반환하는 역할을 수행하는 메서드
        private Object readResolve() {
            return new Period(start, end);
        }
    }

위처럼 구현된 각각의 메서드들은 아래와 같은 역할을 수행하면서 보안 문제를 해결하게 된다.

  • writeReplace: 바깥 클래스 인스턴스 대신 직렬화 프록시 객체를 반환하여 직렬화 중에 생성자 호출을 막음

  • readObject: 역직렬화가 직접적으로 이루어지지 않도록 예외를 던져 프록시 객체를 통해서만 역직렬화가 이루어지도록 함

  • readResolve: 역직렬화된 프록시 객체를 바깥 클래스 인스턴스로 대체하여 반환

장점

직렬화 프록시 패턴을 사용하면 아래와 같은 장점을 얻을 수 있다.

  1. 진정한 불변 클래스: 멤버 필드 final 선언하여 불변성 보장 가능

  2. 역직렬화 유효성 검사 불필요: 직렬화 대상이 직접적으로 되지 않기 때문에 역직렬화 과정에서 유효성 검사를 할 필요가 없음

  3. 역직렬화 인스턴스 != 원래의 인스턴스: 역직렬화된 인스턴스가 원래의 인스턴스와 다르게 되어도 문제가 없음

이 중 3번에 대한 장점은 EnumSet 클래스를 사용한 예시를 통해 확인할 수 있다.

EnumSet 클래스의 직렬화 프록시 패턴

EnumSet 클래스 생성 시 RegularEnumSetJumboEnumSet 두 가지 인스턴스를 반환하게 되는데, 이 두 인스턴스의 차이는 다음과 같다.

  • RegularEnumSet 인스턴스: 64개 이하의 원소를 가질 때 사용

  • JumboEnumSet 인스턴스: 64개 이상의 원소를 가질 때 사용

64개짜리 열거 타입을 가진 RegularEnumSet 인스턴스를 직렬화하고 원소를 추가한 후 역직렬화하면 JumboEnumSet 인스턴스가 생성되어야 하는데, 이를 위해 EnumSet 클래스는 직렬화 프록시 패턴을 사용해 적절한 인스턴스를 생성하도록 구현되어 있다.

public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E>
        implements Cloneable, Serializable {

    // ...

    // 
    public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
        Enum<?>[] universe = getUniverse(elementType);
        if (universe == null)
            throw new ClassCastException(elementType + " not an enum");

        if (universe.length <= 64)
            return new RegularEnumSet<>(elementType, universe);
        else
            return new JumboEnumSet<>(elementType, universe);
    }

    private static class SerializationProxy<E extends Enum<E>> implements Serializable {

        private static final long serialVersionUID = 362491234563181265L;
        // 이 EnumSet의 원소 타입
        private final Class<E> elementType;
        // 이 EnumSet의 원소들
        private final Enum[] elements;

        SerializationProxy(EnumSet<E> set) {
            elementType = set.elementType;
            elements = set.toArray(ZERO_LENGTH_ENUM_ARRAY);
        }

        private Object readResolve() {
            EnumSet<E> result = EnumSet.noneOf(elementType);
            for (Enum e : elements) {
                result.add((E) e);
            }
            return result;
        }
    }
}

Last updated