JAVA/Effective Java

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

꾸준함. 2024. 5. 19. 04:57

앞서 아이템 87, 아이템 88, 그리고 아이템 89에서 언급했던 것처럼 Serializable을 구현하기로 결정한 순간 언어의 생성자 이외의 방법으로도 인스턴스를 생성할 수 있게 됨에 따라 버그 및 보안 문제가 발생할 가능성이 커집니다.

위 문제를 해결하기 위해 커스텀 직렬화, readObject() 또는 readResolve()를 직접 구현하여 위험성을 낮출 수 있지만, 직렬화 프록시 패턴(Serialization Proxy Pattern)을 사용하여 위험을 낮출 수도 있습니다.

  • 직렬화 프록시 패턴은 일반적으로 아이템 88에서 다룬 readObject()의 방어적 복사보다 강력함

 

직렬화 프록시 패턴

  • 바깥 클래스의 논리적 상태를 정밀하게 표현하는 중첩 클래스를 설계해 private static으로 선언
    • 해당 중첩 클래스가 바로 바깥 클래스의 직렬화 프록시
    • 중첩 클래스의 생성자는 단 하나여야 하며 바깥 클래스를 매개변수로 받아야 함
    • 해당 생성자는 단순히 인수로 넘어온 인스턴스의 데이터를 복사하며 이때 추가적인 일관성 검사나 방어적 복사 X
    • 설계상 직렬화 프록시의 기본 직렬화 형태는 바깥 클래스의 직렬화 형태로 쓰기에 이상적
    • 바깥 클래스와 직렬화 프록시 모두 마커 인터페이스인 Serializable을 구현한다고 선언해야 함

 

아이템 88에서 직렬화한 Period 클래스


 

부연 설명

  • Period는 아주 간단하기 때문에 직렬화 프록시도 바깥 클래스와 같은 필드로 구성 
  • 바깥 클래스의 writeReplace() 메서드는 범용적이니 직렬화 프록시를 사용하는 모든 클래스에 그대로 복사해 쓰면 됨
    • 자바의 직렬화 시스템이 바깥 클래스의 인스턴스 대신 SerializationProxy의 인스턴스를 반환하게 하는 역할
    • 직렬화가 이루어지기 전에 바깥 클래스의 인스턴스를 직렬화 프록시로 변환해줌
    • writeReplace()로 인해 직렬화 시스템은 결코 바깥 클래스의 직렬화된 인스턴스를 생성해낼 수 없음

 

  • 바깥 클래스와 논리적으로 동일한 인스턴스를 반환하는 readResolve() 메서드를 SerializationProxy 클래스에 추가
    • 해당 메서드는 역직렬화 시 직렬화 시스템이 직렬화 프록시를 다시 바깥 클래스의 인스턴스로 변환하는 역할
    • readResolve() 메서드는 공개된 API만을 사용해 바깥 클래스의 인스턴스를 생성
    • 직렬화는 생성자를 사용하지 않고도 인스턴스를 생성하는 기능을 제공하는데 해당 패턴은 직렬화의 특성을 상당 부분 제거하여 일반 인스턴스를 만들 때와 똑같이 생성자, 정적 팩토리 혹은 다른 메서드를 사용해 역직렬화된 인스턴스를 생성
    • 이에 따라 역직렬화된 인스턴스가 해당 클래스의 불변식을 만족하는지 검사할 다른 수단을 찾지 않아도 됨

 

직렬화 프록시 패턴의 장단점

 

장점

  • 방어적 복사처럼 가짜 바이트 스트림 공격과 내부 필드 탈취 공격을 프록시 수준에서 차단
  • 직렬화 프록시는 Period의 필드를 final로 선언해도 되므로 Period 클래스를 진정한 불변으로 만들 수 있음 (transient 필드로 선언하지 않아도 됨)
  • 역직렬화할 때 유효성 검사를 수행하지 않아도 됨
  • 역직렬화한 인스턴스와 원래의 직렬화된 인스턴스의 클래스가 달라도 정상 작동

 

단점

  • 클라이언트가 입맛에 따라 확장할 수 있는 클래스에 적용할 수 없음
  • 성능 저하 가능
  • 객체 그래프에 순환이 있는 클래스에는 적용할 수 없음
    • 이런 객체의 메서드를 직렬화 프록시의 readResolve() 내부에서 호출하려고 하면 ClassCastException 예외가 발생함
    • 직렬화 프록시만 가졌을 뿐 실제 객체는 아직 만들어진 것이 아니기 때문

 

직렬화 프록시 적용 예 (EnumSet)

 

EnumSet

 

부연 설명

  • public 생성자 없이 정적 팩토리들만 제공
  • 클라이언트 입장에서는 이 팩토리들이 EnumSet 인스턴스를 반환하는 것으로 보이지만 현재의 OpenJDK를 보면 열거 타입의 크기에 따라 두 하위 클래스 중 하나의 인스턴스를 반환
  • 열거 타입의 원소가 64개 이하면 RegularEnumSet을 사용하고 그보다 크면 JumboEnumSet을 사용

 

EnumSet 내 직렬화 프록시 패턴

 

EnumSet의 직렬화 프록시 패턴

 

부연 설명

  • 원소 64개짜리 열거 타입을 가진 EnumSet을 직렬화한 다음 원소 5개를 추가하고 역직렬화하면
    • 처음 직렬화된 것은 RegularEnumSet 인스턴스
    • 하지만 역직렬화는 JumboEnumSet 인스턴스로 하면 좋을 것
    • 그리고 EnumSet은 직렬화 프록시 패턴을 사용해서 실제로도 위와 같이 동작함

 

정리

  • 제삼자가 확장할 수 없는 클래스라면 가능한 한 중요한 불변식을 안정적으로 직렬화해 주는 쉬운 방법인 직렬화 프록시 패턴을 적용하자

 

참고

이펙티브 자바

반응형