[교재 EffectiveJava] 아이템 90. 직렬화된 인스턴스 대신 직렬화 프록시 사용을 검토하라

반응형
728x90
반응형

직렬화 프록시 패턴(serialization proxy pattern)

바깥 클래스의 논리적 상태를 정밀하게 표현하는 중첩 클래스를 설계하여 private static으로 선언한다. 

이 중첩 클래스가 바로 바깥 클래스의 직렬화 프록시다.

중첩 클래스의 생성자는 단 하나여야 하며, 바깥 클래스를 매개변수로 받아야한다. 이 생성자는 단순히 인수로 넘어온 인스턴스의 데이터를 복사한다. 일관성 검사나 방어적 복사도 필요없다. 설계상 직렬화 프록시의 기본 직렬화 형태는 바깥 클래스의 직렬화 형태로 쓰기에 이상적이다. 그리고 바깥 클래스와 직렬화 프록시 모두 Serializable을 구현한다고 선언해야한다.

 

Period 클래스용 직렬화 프록시
package com.java.effective.item90;

import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.Date;

class Period implements Serializable {
    private final Date start;
    private final Date end;

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

    private static class SerializationProxy implements Serializable {
        // 아무 값이나 상관없다.
        private static final long serialVersionUID = 2123123123;
        private final Date start;
        private final Date end;

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

        /**
         * Period.SerializationProxy용
         */
        private Object readResolve() {
            return new Period(start, end); // public 생성자를 사용한다.
        }
    }


    /**
     * 직렬화 프록시 패턴용
     */
    private Object writeReplace() {
        return new SerializationProxy(this);
    }

    /**
     * 직렬화 프록시 패턴용 readObject 
     */
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
        throw new InvalidObjectException("프록시가 필요합니다.");
    }
}

1) 직렬화 프록시 - SerializationProxy

private static class SerializationProxy implements Serializable {
    // 아무 값이나 상관없다.
    private static final long serialVersionUID = 2123123123;
    private final Date start;
    private final Date end;

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

    /**
     * Period.SerializationProxy용
     */
    private Object readResolve() {
        return new Period(start, end); // public 생성자를 사용한다.
    }
}

 

2) 바깥 클래스의 writeReplace 메서드

/**
 * 직렬화 프록시 패턴용
 */
private Object writeReplace() {
    return new SerializationProxy(this);
}

이 메서드는 직렬화 프록시를 사용하는 모든 클래스에 그대로 복사해서 쓰면 된다. (범용적)

자바의 직렬화 시스템이 바깥 클래스의 인스턴스 대신 SerializationProxy의 인스턴스를 반환하게 하는 역할을 한다. 달리 말해, 직렬화가 이뤄지기 전에 바깥 클래스의 인스턴스를 직렬화 프록시로 변환해준다.

 

writeReplace() 덕분에 직렬화 시스템은 결코 바깥 클래스의 직렬화된 인스턴스를 생성해낼 수 없다. 

 

3) readObject()

readObject 메서드를 바깥 클래스에 추가하여 불변식을 훼손하고자하는 공격을 막을 수 있다.

/**
     * 직렬화 프록시 패턴용 readObject
     */
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
        throw new InvalidObjectException("프록시가 필요합니다.");
    }
}

 

4) readResolve 메서드를 SerializationProxy 클래스에 추가한다.

바깥 클래스와 논리적으로 동일한 인스턴스를 반환한다. 역직렬화시에 직렬화 시스템이 직렬화 프록시를 다시 바깥 클래스의 인스턴스로 변환하게 해준다.

/**
 * Period.SerializationProxy용
 */
private Object readResolve() {
    return new Period(start, end); // public 생성자를 사용한다.
}

readResolve() 메서드는 공개된 API만을 사용하여 바깥 클래스의 인스턴스를 생성한다. 직렬화는 생성자를 이용하지 않고도 인스턴스를 생성하는 기능을 제공하는데, 이 패턴은 직렬화의 이런 언어도단적 특성을 상당 부분 제거한다.

즉, 일반 인스턴스를 만들 때와 똑같은 생성자, 정적 팩터리, 혹은 다른 메서드를 사용해 역직렬화된 인스턴스를 생성하는 것이다. 

 

이렇게되어 역직렬화된 인스턴스가 해당 클래스의 불변식을 만족하는지 검사할 또 다른 수단을 강구하지 않아도 된다. 이 클래스의 정적 팩터리나 생성자가 불변식을 확인해주고 인스턴스 메서드들이 불변식을 잘 지켜준다면, 따로 더 해줘야할 일이 없는 것이다.

 

 

직렬화 프록시 패턴의 장점

1) 직렬화 프록시는 Period 필드를 final로 선언해도 되므로 Period 클래스를 진정한 불변으로 만들 수 있다.

2) 역직렬화 때 유효성 검사를 수행하지 않아도 된다.

3) 역직렬화한 인스턴스와 원래의 직렬화된 인스턴스의 클래스가 달라도 정상 작동한다.

 

 

위 3)번의 예시 - EnumSet

EnumSet 클래스는 public 생성자 없이 정적 팩터리들만 제공한다. 클라이언트 입장에서는 이 팩터리들이 EnumSet 인스턴스를 반환하는 것으로 보이지만, 현재의 openJDK를 보면 열거 타입의 크기에 따라 두 하위 클래스 중 하나의 인스턴스를 반환한다.

  • 열거 타입의 원소가 64개 이하 : RegularEnumSet
  • 열거 타입의 원소가 64개 초과 : JumboEnumSet

이제 원소 64개짜리 열거 타입을 가진 EnumSet을 직렬화한 다음 원소 5개를 추가하고 역직렬화하면 어떤일이 벌어질지 알아보자.

처음 직렬화된 것은 RegularEnumSet이다.
하지만 역직렬화는 JumboEnumSet 인스턴스로 하면 좋을것이다.

그리고 EnumSet은 직렬화 프록시 패턴을 사용해서 실제로 도 이렇게 동작한다. 

 

EnumSet.SerializationProxy
@SuppressWarnings("serial") // No serialVersionUID declared
public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E>
    implements Cloneable, java.io.Serializable
{

    static Enum<?>[] access$000() { return null; }
    private static final java.io.ObjectStreamField[] serialPersistentFields
        = new java.io.ObjectStreamField[0];

    final Class<E> elementType;

    final Enum<?>[] universe;

    EnumSet(Class<E>elementType, Enum<?>[] universe) {
        this.elementType = elementType;
        this.universe    = universe;
    }
    
    ...
    
    Object writeReplace() {
        return new SerializationProxy<>(this);
    }

    private void readObject(java.io.ObjectInputStream s)
        throws java.io.InvalidObjectException {
        throw new java.io.InvalidObjectException("Proxy required");
    }

    ...
    
    /**
     * This class is used to serialize all EnumSet instances, regardless of
     * implementation type.  It captures their "logical contents" and they
     * are reconstructed using public static factories.  This is necessary
     * to ensure that the existence of a particular implementation type is
     * an implementation detail.
     *
     * @serial include
     */
    private static class SerializationProxy<E extends Enum<E>>
        implements java.io.Serializable
    {

        private static final Enum<?>[] ZERO_LENGTH_ENUM_ARRAY = new Enum<?>[0];

        /**
         * The element type of this enum set.
         *
         * @serial
         */
        private final Class<E> elementType;

        /**
         * The elements contained in this enum set.
         *
         * @serial
         */
        private final Enum<?>[] elements;

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

        /**
         * Returns an {@code EnumSet} object with initial state
         * held by this proxy.
         *
         * @return a {@code EnumSet} object with initial state
         * held by this proxy
         */
        @SuppressWarnings("unchecked")
        private Object readResolve() {
            // instead of cast to E, we should perhaps use elementType.cast()
            // to avoid injection of forged stream, but it will slow the
            // implementation
            EnumSet<E> result = EnumSet.noneOf(elementType);
            for (Enum<?> e : elements)
                result.add((E)e);
            return result;
        }

        private static final long serialVersionUID = 362491234563181265L;
    }

    ...
}

 

 

직렬화 프록시 패턴의 한계

  • 1) 클라이언트가 멋대로 확장할 수 있는 클래스에는 적용할 수 없다.
  • 2) 객체 그래프에 순환이 있는 클래스에도 적용할 수 없다.
    • 이런 객체의 메서드를 직렬화 프록시의 readResolve 안에서 호출하려하면 ClassCastException이 발생한다. 직렬화 프록시만 가졌을 뿐, 실제 객체는 아직 만들어진 것이 아니기 때문이다.
  • 3) 방어적 복사때보다 속도가 느릴 수 있다.

 

 

 

반응형

Designed by JB FACTORY