Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
Mingadinga opened this issue Sep 19, 2023 · 0 comments

Comments

@Mingadinga
Copy link
Member

제3자가 확장할 수 없는 클래스라면 가능한 직렬화 프록시 패턴을 사용하자. 이 패턴이 아마도 중요한 불변식을 안정적으로 직렬화해주는 가장 쉬운 방법일 것이다.

직렬화 프록시 패턴

  • Serializable을 사용하기로 결정하는 순간 언어의 정상 매커니즘인 생성자 외의 방법으로 인스턴스를 생성할 수 있게 되어 버그나 보안 문제가 발생한다.
  • 직렬화 프록시 패턴을 사용하면 정상 매커니즘인 생성자, 정적 팩터리 메서드 등을 사용해 역직렬화된 인스턴스를 반환할 수 있게 되어 위험이 크게 줄어든다.

구현

  • 바깥 클래스와 직렬화 프록시 클래스 모두 Serializable을 구현한다고 선언
  • 바깥 클래스의 논리적 상태를 표현하는 중첩 클래스를 private static으로 선언
  • 프록시 클래스의 생성자 : 딱 하나, 바깥 클래스를 매개변수로 받고 단순히 데이터를 복사한다
  • 바깥 클래스 writeReplace : 직렬화가 이뤄지기 전에 바깥 클래스의 인스턴스를 직렬화 프록시로 변환함. 덕분에 직렬화 시스템은 바깥 클래스의 직렬화된 인스턴스를 생성할 수 없고 오직 프록시로만 접근이 가능하다.
  • 바깥 클래스 readObject : 공격자가 불변식을 훼손하고자 readObject를 호출할때 예외를 던져 방어
  • 프록시 클래스 readResolve : 바깥 클래스의 인스턴스를 생성자나 팩터리 메소드르 생성해 반환
public final class Period implements Serializable {
    private final Date start;
    private final Date end;

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

				// 생성자에 인수 검사 로직 있음 - 직렬화된 스트림을 조작해서 역직렬화하면 불변식 깨질 수도!
        if (this.start.compareTo(this.end) > 0)
            throw new IllegalArgumentException(
                    start + " after " + end);
    }

    public Date start () { return new Date(start.getTime()); }

    public Date end () { return new Date(end.getTime()); }

    public String toString() { return start + " - " + end; }

    // 프록시 클래스
    private static class SerializationProxy implements Serializable {
				// 바깥 클래스를 논리적으로 표현
        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);
        }

        private static final long serialVersionUID = 234098243823485285L; // Any number will do (Item 87)
    }

    // 직렬화가 이뤄지기 전에 바깥 클래스의 인스턴스를 직렬화 프록시로 변환함
		// 덕분에 직렬화 시스템은 바깥 클래스의 직렬화된 인스턴스를 생성할 수 없고 오직 프록시로만 접근 가능
    private Object writeReplace() {
        return new SerializationProxy(this);
    }

    // 공격자가 불변식을 훼손하고자 readObject를 호출할때 예외를 던져 방어
    private void readObject(ObjectInputStream stream)
            throws InvalidObjectException {
        throw new InvalidObjectException("Proxy required");
    }
}

방어적 복사 vs 프록시

  1. 필드를 final로 선언해도 되므로 Period 클래스를 진정한 불변으로 만들 수 있다.
  2. 역직렬화한 인스턴스와 원래의 직렬화된 인스턴스 클래스가 달라도 정상 동작한다.

프록시 패턴을 사용하는 예시 : EnumSet

  • 원소가 64개 이하면 RegularEnumSet 사용
  • 원소가 그보다 크면 JumboEnumSet 사용
  • 원소가 64개짜리 열거 타입을 가진 EnumSet을 직렬화한 다음 원소 5개를 추가하고 역직렬화하면 JumboSet 인스턴스로 사용할 수 있다. EnumSet의 정적 팩터리 메서드를 사용할 수 있기 때문이다!
private static class SerializationProxy<E extends Enum<E>> implements java.io.Serializable {

        private static final Enum<?>[] ZERO_LENGTH_ENUM_ARRAY = new Enum<?>[0];
        private final Class<E> elementType;
        private final Enum<?>[] elements;

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

        @SuppressWarnings("unchecked")
        @java.io.Serial
        private Object readResolve() {
            EnumSet<E> result = EnumSet.noneOf(elementType); // 정적 팩터리 메소드 사용하여 인스턴스 생성
            for (Enum<?> e : elements)
                result.add((E)e);
            return result;
        }

        @java.io.Serial
        private static final long serialVersionUID = 362491234563181265L;
    }

직렬화 프록시 패턴의 한계

  • 클라이언트가 멋대로 확장할 수 있는 클래스는 적용 불가
  • 객체 그래프에 순환이 있는 클래스는 적용 불가
  • 방어적 복사보다 느리다
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant