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

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

Open
kmswlee opened this issue Jul 3, 2022 · 0 comments
Assignees
Labels
12장 직렬화

Comments

@kmswlee
Copy link

kmswlee commented Jul 3, 2022

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

  • Serializable을 구현하기로 결정한 순간 언어의 정상 메커니즘인 생성자 이외의 방법으로 인스턴스를 생성할 수 있게 된다.
  • 버그와 보안 문제가 일어날 가능성이 커진다는 뜻
  • 이를 줄여줄 기법이 프록시 패턴(serialization proxy pattern)이다.

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

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

ex) Period 클래스용 직렬화 프록시

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 static final long serialVersionUID = 
      452376890235478394258L; // 아무값이나 상관없다.
}

다음으로, 바깥 클래스에 writeReplace 메서드를 추가한다.

private Object writeReplace() {
  return new SerializationProxy(this);
}
  • 이 메서드는 바깥 클래스의 인스턴스 대신 SerializationProxy의 인스턴스를 반환하게 하는 역할을 한다.
  • 달리 말해, 직렬화가 이뤄지기 전에 바깥 클래스의 인스턴스를 직렬화 프록시로 변환해준다.
  • writeReplace 덕분에 직렬화 시스템은 결코 바깥 클래스의 직렬화된 인스턴스를 생성해낼 수 없다.
  • 하지만 허점이 있기 때문에 readObject 메서드를 바깥 클래스에 추가하면 좋다.
private void readObject(ObjectInpuStream stream) 
    throws InvalidObjectException {
      throw new InvalidObjectException("프록시가 필요합니다.");
    }
  • 마지막으로, 바깥 클래스와 논리적으로 동일한 인스턴스를 반환하는 readResolve 메서드를 SerializationProxy 클래스에 추가한다.
  • 이 메서드는 역직렬화 시에 직렬화 시스템이 직렬화 프록시를 다시 바깥 클래스의 인스턴스로 변환하게 해준다.
  • readResolve 메서드는 공개된 API만을 사용해 바깥 클래스의 인스턴스를 생성하는데,
  • 이 패턴이 아름다운 이유는, 직렬화는 생성자를 이용하지 않고도 인스턴스를 생성하는 기능을 제공하는데, 이 패턴은 직렬화의 이런 언어도단적 특성을 상당 부분 제거한다.
  • 즉, 인스턴스를 만들 때와 똑같은 생성자, 정적 팩터리, 혹은 다른 메서드를 사용해 역직렬화된 인스턴스를 생성하는 것이다.
  • 따라서 불변식을 만족하는지 따로 검사할 필요가 없다.

ex) readResolve 메서드

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

EnumSet의 직렬화 프록시

private static class SerializationProxy <E extends Enum<E>>
    implements Serializable {
      private final Class<E> elementType;
      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 = 
          4578194361234063217489L;
    }

직렬화 프록시 패턴의 한계

  • 첫째, 클라이언트가 멋대로 확장할 수 있는 클래스에는 적용할 수 없다.
  • 둘째, 객체 그래프에 순환이 있는 클래스에도 적용할 수 없다.
  • 셋째, 직렬화 프록시 패턴이 주는 강력함과 안정성에도 대가는 따른다.
    예를 들어, Period를 내 컴퓨터에서 실행해보니 방어적 복사 때보다 14% 느렸다.

핵심정리

  • 제3자가 확장할수 없는 클래스라면 가능한 한 직렬화 프록시 패턴을 사용하자.
  • 이 패턴이 아마도 중요한 불변식을 안정적으로 직렬화해주는 가장 쉬운 방법일 것이다.
@kmswlee kmswlee added the 12장 직렬화 label Jul 3, 2022
@kmswlee kmswlee self-assigned this Jul 3, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
12장 직렬화
Projects
None yet
Development

No branches or pull requests

1 participant