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

2023. 10. 7. 00:48·Reference/Effective-Java
📌 직렬화 프록시 패턴 (Serialization Proxy Pattern)
  • 디자인 패턴의 프록시 패턴을 응용한 것과 같다. (그래서 장/단점도 비슷하게 따라간다.)
  • 실제 객체를 숨기고 대변인을 통해 직렬화/역직렬화를 수행한다.

 

📌 작성 순서

1️⃣ 프록시 클래스 생성자

private static class SerializationProxy implements Serializable {
    public SerializationProxy(Period p) {
        this.start = p.start;
        this.end = p.end;
    }
}
  • 중첩 클래스를 private static으로 선언한다.
  • 생성자는 단 하나여야 한다.
  • 바깥 클래스를 매개변수로 받아야 한다.
  • 인스턴스를 복사하는 역할만 수행한다.
    • 일관성 검사나 방어적 복사도 필요없다. (바깥 클래스를 불변 클래스로 관리할 수 있게 되었기 때문)

 

2️⃣ Serializable 구현

class Period implements Serializable {
    ...

    private static class SerializationProxy implements Serializable {
        ...
    }
    
    ...
}
  • 바깥 클래스와 직렬화 프록시 모두 Serializable을 구현한다.

 

3️⃣ 프록시 클래스 필드

private static class SerializationProxy implements Serializable {
    private static final long serialVersionUID = 2123123123L; // 아무 값이나 상관없음
    private final Date start;
    private final Date end;
        
    ...
}
  • 직렬화 프록시도 바깥 클래스와 완전히 같은 필드로 구성한다. (단, 이 경우엔 Period가 너무 간단한 객체라서 그렇다.)

 

4️⃣ 바깥 클래스 writeReplace 메서드

class Period implements Serializable {
    ...

    // Serialize -> 프록시 인스턴스 반환
    // 결코 바깥 클래스의 직렬화된 인스턴스를 생성해낼 수 없다
    private Object writeReplace() {
        return new SerializationProxy(this);
    }
    
    ...
}
  • 자바의 직렬화 시스템이 바깥 클래스 인스턴스 대신 SerializationProxy 인스턴스를 반환하게 한다.
  • 직렬화 시스템은 결코 바깥 클래스의 직렬화된 인스턴스 생성이 불가능하다. (직렬화 방지) 

 

5️⃣ 바깥 클래스 readObject 메서드

class Period implements Serializable {
    ...

    // Period 자체의 역직렬화를 방지 -> 역직렬화 시도시, 에러 반환
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
        throw new InvalidObjectException("프록시가 필요합니다.");
    }
}
  • 혹시라도 바깥 클래스의 불변식을 훼손하려는 공격을 막아낼 수 있다. (역직렬화 방지)

 

6️⃣ 프록시 클래스 readResolve 메서드

private static class SerializationProxy implements Serializable {
    ...
    
    // Deserialize -> Object 생성 (바깥 클래스와 논리적으로 동일한 인스턴스 반환)
    private Object readResolve() {
        return new Period(start, end);
    }
}
  • 역직렬화 시, 직렬화 프록시를 다시 바깥 클래스 인스턴스로 변환한다.
  • 숨은 생성자 기능을 가진 직렬화의 특성을 상당 부분 제거한다.
    • 일반 인스턴스 만들 때와 똑같이 생성자, 정적 팩터리, 혹은 다른 메서드로 역직렬화 인스턴스를 생성하면 된다.
  • 역직렬화된 인스턴스가 해당 클래스 불변식을 만족하는지 검사할 수단을 강구할 필요가 없다.
    • 바깥 클래스의 생성자(혹은 정적 팩터리)가 불변식을 확인하고, 인스턴스 메서드들이 불변식을 잘 지켜준다면, 따로 더 할 일이 없다.

 

✨ 최종 구현

class Period implements Serializable {
    // 불변 가능
    private final Date start;
    private final Date end;

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

    // 직렬화 시, Period 인스턴스를 직렬화하지 않고, SerializationProxy 인스턴스를 직렬화한다.
    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);
        }
    }

    // Serialize -> 프록시 인스턴스 반환
    // 결코 바깥 클래스의 직렬화된 인스턴스를 생성해낼 수 없다
    private Object writeReplace() {
        return new SerializationProxy(this);
    }

    // Period 자체의 역직렬화를 방지 -> 역직렬화 시도시, 에러 반환
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
        throw new InvalidObjectException("프록시가 필요합니다.");
    }
}

 

📌 장점
  1. 가짜 바이트 스트림 공격과 내부 필드 탈취 공격을 프록시 수준에서 차단한다.
  2. Period 필드를 final로 선언 가능해지므로, Period 클래스를 진정한 불변 클래스로 만들 수 있다.
  3. 어떤 필드가 기만적 직렬화 공격 목표가 될지 고민하지 않아도 되며, 역직렬화 시 유효성 검사가 필요 없다.
  4. 역직렬화할 인스턴스와 원래 직렬화된 인스턴스의 클래스가 달라도 정상 작동한다. (ex. EnumSet)

 

📌 역직렬화 인스턴스를 선택할 수 있다: EnumSet
public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E>
    implements Cloneable, java.io.Serializable
{
    // declare EnumSet.class serialization compatibility with JDK 8
    @java.io.Serial
    private static final long serialVersionUID = 1009687484059888093L;
    
    ...
    
    public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
        Enum<?>[] universe = getUniverse(elementType);
        if (universe == null)
            throw new ClassCastException(elementType + " not an enum");

        if (universe.length <= 64)
            return new RegularEnumSet<>(elementType, universe);
        else
            return new JumboEnumSet<>(elementType, universe);
    
    ...
    
    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() {
            // 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;
        }

        @java.io.Serial
        private static final long serialVersionUID = 362491234563181265L;
    }
    
    @java.io.Serial
    Object writeReplace() {
        return new SerializationProxy<>(this);
    }
    
    @java.io.Serial
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.InvalidObjectException {
        throw new java.io.InvalidObjectException("Proxy required");
    }

    @java.io.Serial
    private void readObjectNoData()
        throws java.io.InvalidObjectException {
        throw new java.io.InvalidObjectException("Proxy required");
    }
}
  • EnumSet은 기본적으로 원소가 64개 이하면 RegularEnumSet, 그보다 크면 JumboEnumSet을 반환하도록 내부적으로 구현되어 있다. (호출자 입장에선 차이 못 느낌)
  • `원소 64개짜리 EnumSet(RegularEnumSet) → 직렬화 → 원소 5개 추가 → 역직렬화 → EnumSet(JumboEnumSet)`이 가능하다.

 

📌 한계점
  • 클라이언트가 확장 가능한 클래스에는 적용할 수 없다. (불변이 아니므로)
  • 객체 그래프에 순환이 있는 클래스에서 사용할 수 없다.
    • readResolve 호출 시, 이런 객체 메서드로 인해 ClassCastException이 발생할 것이다.
    • 아직 Serialization Proxy만 가졌을 뿐, 실제 객체가 만들어진 상태가 아니기 때문이다.
    • 실행 속도가 저하된다. (프록시 패턴 자체의 단점이기도 하다.)
저작자표시 비영리 (새창열림)
'Reference/Effective-Java' 카테고리의 다른 글
  • [Effective-Java] Chapter12 #89. 인스턴스 수를 통제해야 한다면 readResolve보다는 열거 타입을 사용하라
  • [Effective-Java] Chapter12 #88. readObject 메서드는 방어적으로 작성하라
  • [Effective-Java] Chapter12 #87. 커스텀 직렬화 형태를 고려해보라
  • [Effective-Java] Chapter12 #86. Serializable을 구현할지는 신중히 결정하라
나죽못고나강뿐
나죽못고나강뿐
싱클레어, 대부분의 사람들이 가는 길은 쉽고, 우리가 가는 길은 어려워요. 우리 함께 이 길을 가봅시다.
  • 나죽못고나강뿐
    코드를 찢다
    나죽못고나강뿐
  • 전체
    오늘
    어제
    • 분류 전체보기 (449)
      • Computer Science (59)
        • Git & Github (4)
        • Network (17)
        • Computer Structure & OS (13)
        • Software Engineering (5)
        • Database (9)
        • Security (4)
        • Concept (7)
      • Frontend (21)
        • React (13)
        • Android (4)
        • iOS (4)
      • Backend (74)
        • Spring Boot & JPA (47)
        • Django REST Framework (14)
        • MySQL (8)
        • Nginx (1)
        • FastAPI (4)
      • DevOps (24)
        • Docker & Kubernetes (11)
        • Naver Cloud Platform (1)
        • AWS (2)
        • Linux (6)
        • Jenkins (0)
        • GoCD (3)
      • Coding Test (112)
        • Solution (104)
        • Algorithm (7)
        • Data structure (0)
      • Reference (134)
        • Effective-Java (90)
        • Pragmatic Programmer (0)
        • CleanCode (11)
        • Clean Architecture (2)
        • Test-Driven Development (4)
        • Relational Data Modeling No.. (0)
        • Microservice Architecture (2)
        • 알고리즘 문제 해결 전략 (9)
        • Modern Java in Action (0)
        • Spring in Action (0)
        • DDD start (0)
        • Design Pattern (6)
        • 대규모 시스템 설계 (6)
        • JVM 밑바닥까지 파헤치기 (4)
      • Service Planning (2)
      • Side Project (5)
      • AI (0)
      • MATLAB & Math Concept & Pro.. (1)
      • Review (14)
      • Interview (1)
      • IT News (2)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • 깃
  • 공지사항

    • 취업 전 계획 재조정
    • 취업 전까지 공부 계획
    • 앞으로의 일정에 대하여..
    • 22년 동계 방학 기간 포스팅 일정
    • 중간고사 기간 이후 포스팅 계획 (10.27~)
  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.2
나죽못고나강뿐
[Effective-Java] Chapter12 #90. 직렬화된 인스턴스 대신 직렬화 프록시 사용을 검토하라

개인정보

  • 티스토리 홈
  • 포럼
  • 로그인
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.