Home [Effective Java] Item90. 직렬화된 인스턴스 대신 직렬화 프록시 사용을 검토하라!
Post
Cancel

[Effective Java] Item90. 직렬화된 인스턴스 대신 직렬화 프록시 사용을 검토하라!

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

Intro

  • Serializable을 구현하기로 결정한 순간 생성자 이외의 방법으로 인스턴스 생성이 가능해진다.
    • 이는 버그와 보안 문제가 일어날 가능성이 커진다는 뜻이다.
  • 이러한 위험을 크게 줄여줄 기법이 바로 직렬화 프록시 패턴(serialization proxy pattern)이다.

직렬화 프록시 패턴

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

    • 이 중첩 클래스가 바로 바깥 클래스의 직렬화 프록시다.
  • 중첩 클래스의 생성자는 단 하나여야 한다. 또 바깥 클래스를 매개변수로 받아야 한다.

    • 이 생성자는 단순히 인수로 넘어온 인스턴스의 데이터를 복사한다.(일관성 검사, 방어적 복사 불필요)
  • 설계상, 직렬화 프록시의 기본 직렬화 형태는 바깥 클래스의 직렬화 형태로 쓰기에 이상적이다.

  • 바깥 클래스와 직렬화 프록시 모두 Serializable을 구현한다고 선언해야 한다.

  • 예시. Period 클래스

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    
    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 = 2123123123L; // 아무 값이나 상관 없음.
            private final Date start;
            private final Date end;
      
            public SerializationProxy(Period p) {
                this.start = p.start;
                this.end = p.end;
            }
      
            // Deserialize -> Object 생성
          	// 역직렬화 시 직렬화 프록시를 바깥 클래스 인스턴스로 변환
            private Object readResolve() {
                return new Period(start, end);
            }
        }
      
      	// 직렬화 프록시 패턴용 writeReplace 메서드
        // 결코 바깥 클래스의 직렬화된 인스턴스를 생성해낼 수 없다.
        private Object writeReplace() {
            return new SerializationProxy(this); // SerializationProxy 인스턴스 반환
        }
      
      	// 직렬화 프록시 패턴용 readObject 메서드
        // Period 자체의 역직렬화를 방지 -> 역직렬화 시도하면 예외 던짐 (불변식 훼손 공격 막음)
        private void readObject(ObjectInputStream stream) throws InvalidObjectException {
            throw new InvalidObjectException("프록시가 필요합니다.");
        }
    }
    
    • readResolve 메서드는 공개된 API 만을 사용해 바깥 클래스의 인스턴스를 생성한다.
      • 즉, 일반 인스턴스를 만들 때와 동일한 생성자, 정적 팩터리, 혹은 다른 메서드를 사용해 역직렬화된 인스턴스를 생성하는 것이다.
      • 그렇기에 역직렬화된 인스턴스가 해당 클래스의 불변식을 만족하는지 검사할 수단을 찾을 필요가 없어진다. (그 클래스의 정적 팩터리나 생성자가 불변식을 확인해주고 인스턴스 메서드들이 불변식을 잘 지켜준다면, 따로 해줘야할 일이 없는 것이다.)
  • 방어적 복사처럼, 직렬화 프록시 패턴은 가짜 바이트 스트림 공격, 내부 필드 탈취 공격을 프록시 수준에서 차단해준다.

  • 직렬화 프록시는 Period 필드를 final로 선언해도 된다. 그러니 Period 클래스를 진정한 불변으로 만들 수 있다.

직렬화 프록시 패턴의 강점

  • readObject의 방어적 복사와 비교해 직렬화 프록시 패턴은 강점을 하나 가진다. 바로 역직렬화한 인스턴스와 원래의 직렬화된 인스턴스의 클래스가 달라도 정상 작동한다는 점이다.

    • A class를 직렬화 -> B class로 역직렬화 -> 정상 작동
  • 이러한 강점을 사용하는 예시. EnumSet

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
    private static class SerializationProxy <E extends Enum<E>> implements java.io.Serializable
    {
    		// 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);
        }
      
        // 새롭게 원소의 크기에 맞는 EnumSet 생성
        private Object readResolve() {
            EnumSet<E> result = EnumSet.noneOf(elementType);
            for (Enum<?> e : elements)
                result.add((E)e);
            return result;
        }
      
        private static final long serialVersionUID = 362491234563181265L;
    }
    
    • EnumSet 클래스는 public 생정자 없이 정적 팩터리들만 제공한다.
      • 현재의 OpenJDK를 살펴보면, 이 팩터리들은 EnumSet 인스턴스를 반환하는 것이 아니라 열거 타입의 원소 개수에 따라서 하위 클래스의 인스턴스를 반환하고 있다. (64개 이하: RegularEnumSet, 64개 초과: JumboEnumSet)
    • 처음에 64개의 열거 타입을 가진 EnumSet을 직렬화하고, 5개의 원소를 추가했다 가정해보자.
      • 처음 직렬화된 것은 RegularEnumSet 인스턴스다. (64 개)
      • 하지만 역직렬화는 JumboEnumSet 인스턴스로 하면 좋을 것이다. (64+5 개)

직렬화 프록시 패턴의 한계

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

직렬화 프록시 사용의 대가

  • 직렬화 프록시 패턴이 주는 강력함과 안전성에는 대가가 따른다. 바로 속도가 느려진다는 점이다.

핵심 정리

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

[Effective Java] Item89. 인스턴스 수를 통제해야 한다면 readResolve보다는 열거 타입을 사용하라!

[Network] 모두의 네트워크 - 1장

Comments powered by Disqus.