Skip to content

Latest commit

 

History

History
92 lines (70 loc) · 4.02 KB

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

File metadata and controls

92 lines (70 loc) · 4.02 KB

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

  • Serializable을 구현한 순간 언어 정상 매커니즘인 생성자 이외의 방법으로 인스턴스를 생성할 수 있게된다.
    • 이것은 버그와 보안 문제가 일어날 가능성이 크다.
  • 하지만 직렬화 프록시 패턴이 이 위험을 줄여줄 수 있다.

직렬화 프록시 패턴 이란?

  • 바깥 클래스의 논리적 상태를 표현하는 중첩 클래스를 설계하여 private static으로 선언한다.
  • 여기서 중첩 클래스가 바깥 클래스의 직렬화 프록시다.
  • 중첩 클래스의 생성자는 단 하나여야 하며, 바깥 클래스를 매개변수로 받아야한다. 일관성 검사나 방어적 복사가 필요없다.
public final class Period implements Serializable {
    private final Date start;
    private final Date end;

    /**
     * @param  start 시작 시각
     * @param  end 종료 시각; 시작 시각보다 뒤여야 한다.
     * @throws IllegalArgumentException 시작 시각이 종료 시각보다 늦을 때 발생한다.
     * @throws NullPointerException start나 end가 null이면 발행한다.
     */
    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 + "가 " + end + "보다 늦다.");
    }

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

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

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


    // 코드 90-1 Period 클래스용 직렬화 프록시 (479쪽)
    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;
        }

        /**
         * Deserialize 할 때 호출된다. 바깥 클래스의 인스턴스로 변환한다.
         * 장점으로 일반 인스턴스를 만들 때와 똑같은 생성자, 정적 팩터리를 사용해 인스턴스를 생성할 수 있다.
         */
        private Object readResolve() {
            return new Period(start, end);
        }

        private static final long serialVersionUID = 34098243823485285L; // 아무 값이나 상관없다. (아이템 87)
    }

    // 직렬화 프록시 패턴용 writeReplace 메서드 
    // 바깥 클래스의 직렬화된 인스턴스를 생성할 수 없게한다.
    // 직렬화할 때 호출되는데, 프록시를 반환하게 하고 있다.
    private Object writeReplace() {
        return new SerializationProxy(this);
    }

    // 직렬화 프록시 패턴용 readObject 메서드 (480쪽)
    // 추가해서 불변식 훼손 공격을 막아낼 수 있다.
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
        throw new InvalidObjectException("프록시가 필요합니다.");
    }
}

직렬화 프록시 패턴 장점

  • 직렬화 프록시는 Period의 필드를 final로 선언해도 되므로 불변으로 만들 수 있다.
  • 또한 직렬화 프록시 패턴은 역직렬화한 인스턴스와 원래의 직렬화된 클래스가 달라도 정상적으로 동작한다.
    • 예를들어 직렬화는 RegularEnumSet을 사용하고, 역직렬화는 JumboEnumSet사용

직렬화 프록시 패턴 한계

  • 클라이언트가 마음대로 확장할 수 있는 클래스에는 적용할 수 없다.
  • 객체가 서로 참조하는 상황, 그러니까 객체 그래프에 순환이 있는 클래스에도 적용할 수 없다. 이런 객체의 메서드를 직렬화 프록시의 readResolve 안에서 호출하려 하는 경우 예외가 발생할 것이다. 직렬화 프록시만 가진 것이지 실제 객체는 아직 만들어지지 않았기 때문이다.
  • 방어적 복사보다 상대적으로 속도도 느리다.