앞서 아이템 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)
부연 설명
- public 생성자 없이 정적 팩토리들만 제공
- 클라이언트 입장에서는 이 팩토리들이 EnumSet 인스턴스를 반환하는 것으로 보이지만 현재의 OpenJDK를 보면 열거 타입의 크기에 따라 두 하위 클래스 중 하나의 인스턴스를 반환
- 열거 타입의 원소가 64개 이하면 RegularEnumSet을 사용하고 그보다 크면 JumboEnumSet을 사용
EnumSet 내 직렬화 프록시 패턴
부연 설명
- 원소 64개짜리 열거 타입을 가진 EnumSet을 직렬화한 다음 원소 5개를 추가하고 역직렬화하면
- 처음 직렬화된 것은 RegularEnumSet 인스턴스
- 하지만 역직렬화는 JumboEnumSet 인스턴스로 하면 좋을 것
- 그리고 EnumSet은 직렬화 프록시 패턴을 사용해서 실제로도 위와 같이 동작함
정리
- 제삼자가 확장할 수 없는 클래스라면 가능한 한 중요한 불변식을 안정적으로 직렬화해 주는 쉬운 방법인 직렬화 프록시 패턴을 적용하자
참고
이펙티브 자바
반응형
'JAVA > Effective Java' 카테고리의 다른 글
[아이템 89] 인스턴스 수를 통제해야 한다면 readResolve보다는 열거 타입을 사용하라 (1) | 2024.05.18 |
---|---|
[아이템 88] readObject 메서드는 방어적으로 작성하라 (0) | 2024.05.18 |
[아이템 87] 커스텀 직렬화 형태를 고려해보라 (2) | 2024.05.14 |
[아이템 86] Serializable을 구현할지는 신중히 결정하라 (1) | 2024.05.12 |
[아이템 85] 자바 직렬화의 대안을 찾으라 (1) | 2024.05.12 |