-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Labels
Description
📌 [아이템 89] 인스턴스 수를 통제해야 한다면 readResolve 보다는 열거 타입을 사용하라
✨ 핵심 내용
- 불변식을 위해 인스턴스를 통제해야 한다면 열거타입을 사용하자.
- 이가 여의치 않은 상황에서 직렬화와 인스턴스 통제가 모두 필요하면 readResolve 메서드를 사용하고, 그 클래스에서 모든 참조 타입 인스턴스 필드를 transient로 선언 해야한다.
💡 새롭게 알게 된 점
- 조슈아 블로크는 ENUM을 사랑해
- ENUM을 사용하면 리플렉션으로 생성자를 호출하는 것 역시 자연스럽게 방지할 수 있다.
- 직렬화 하지 말자
📚 정리
-
Singleton에 implements Serializable을 추가하는 순간 더 이상 Singleton이 아니게 된다. 기본 직렬화를 사용하지 않더라도, 명시적인 readObject를 제공해도 소용없다. 이 클래스가 초기화 될 때 만들어진 인스턴스와는 별개인 인스턴스를 반환하게 된다.
-
readResolve 메서드를 적절히 정의해 뒀다면, 역직렬화 후 새로 생성된 객체를 인수로 이 메서드가 호출되고, 이 메서드가 반환한 객체 참조가 새로 생성된 객체를 대신해 반환된다.
// 인스턴스 통제를 위한 readResolve private Object readResolve() { // 진짜 Elvis를 반환하고, 가짜 Elvis는 가비지 컬렉터에 맡긴다. return INSTANCE; } // 이 경우 Elvis 인스턴스의 직렬화 형태는 아무런 실 데이터를 가질 이유가 없으니 모든 인스턴스 필드를 transient 로 선언해야 한다.
- readResolve를 인스턴스 통제 목적으로 사용한다면 객체 참조타임 인스턴스 필드는 모두 transient 로 선언해야 한다.
- 그렇지 않으면 readResolve 메서드가 수행되기 전에 역직렬화 된 객체의 참조를 공격할 여지가 남는다.
-
싱글턴이 transient가 아닌 참조 필드를 가지고 있다면 그 필드의 내용은 readResolve 메서드 수행 전에 역직렬화 된다. 이때 잘 조작된 스트림을 써 해당 참조 필드의 내용이 역직렬화 되는 시점에 그 역직렬화 된 인스턴스의 참조를 훔쳐올 수 있다.
- readResolve 메소드와 인스턴스 필드 하나를 포함한 도둑 클래스를 작성. 이 인스턴스 필드는 도둑이 숨길 직렬화된 싱글턴을 참조하는 역할을 한다.
- 직렬화된 스트림에서 싱글턴의 비 휘발성 필드를 이 도둑의 인스턴스로 교체한다.
- 싱글턴은 도둑을 참조하고 도둑은 싱글턴을 참조하는 순환고리가 만들어 졌다.
- 싱글턴이 도둑을 포함하므로 싱글턴이 역직렬화될 때 도둑의 readResolve 메서드가 먼저 호출된다. 그 결과 도둑의 readResolve 메서드가 수행될 때 도둑의 인스턴스 필드에는 역직렬화 도중인(readResolve가 수행되기 전의 싱글턴의 참조가 담겨 있다.)
- 도둑의 readResolve 메서드는 이 인스턴스 필드가 참조한 값을 정적 필드로 복사하여 readResolve가 끝난 후에도 참조할 수 있도록 한다. 그 다음 이 메소드는 도둑이 숨긴 transient가 아닌 필드의 원래 타입에 맞는 값을 반환한다. 이 과정이 생략되면 직렬화 시스템이 도둑의 참조를 이 필드에 저장하려 할 때 VM 이
ClassCastExceptioin을 던진다.
public class Elvis implements Serializable { public static final Elvis INSTANCE = new Elvis(); private Elvis() {} private String[] favoriteSongs = { "Hound Dog", "Heartbreak Hotel" }; public void printFavorites() { System.out.println(Arrays.toString(favoriteSongs)); } private Object readResolve() { return INSTANCE; } } public class ElvisStealer implements Serializable { static Elvis impersonator; private Elvis payload; private Object readResolve() { // resolve되기 전의 Elvis 인스턴스의 참조를 저장한다. impersonator = payload; // favoriteSongs 필드에 맞는 타입의 객체를 반환한다. return new String[] { "A Fool Such as I"}; } private static final long serialVersionUID = 0; }
-
필드를 transient로 선언해 이 문제를 고칠 수 있지만, Elvis를 원소 하나짜리 열거 타입으로 바꾸는 편이 더 나은 선택이다
-
ElvisStealer 공격으로 보여줬듯이 readResolve 메서드를 사용해 '순간적으로' 만들어진 역직렬화된 인스턴스에 접근하지 못하게 하는 방법은 깨지기 쉽고 신경을 많이 써야한다.
-
직렬화 가능한 인스턴스 통제 클래스를 열거 타입을 이용해 구현하면 선언한 상수 외의 다른 객체는 존재하지 않음을 자바가 보장해 준다.
- 물론 공격자가 임의의 네이티브 코드를 수행할 수 있는 특권을 가로챈 경우 무력화된다.
public enum Elvis {
INSTANCE;
private String[] favoriteSongs = { "Hound Dog", "Heartbreak Hotel" };
public void printFavorites() {
System.out.println(Arrays.toString(favoriteSongs));
}
}- 인스턴스 통제를 위해 readResolve를 사용하는 방식이 완전히 쓸모없는 것은 아니다. 컴파일 타임에 어떤 인스턴스가 있는지 알 수 없는 상황이라면 열거 타입으로 표현할 수 없다.
- readResolve 메서드의 접근성은 매우 중요하다. final 클래스라면 readResolve 메서드는 private 이어야 한다. final 이 아닌 클래스는 다음을 주의해야 한다.
- private로 선언하면 하위 클래스에서 사용할 수 없다.
- package-private으로 선언하면 같은 패키지에 속한 하위 클래스에서만 사용할 수 있다.
- protected나 public으로 선언하면 이를 재정의 하지 ㅇ낳은 모든 하위 클래스에서 사용할 수 있다.
- protected나 public이면서 하위 클래스에서 재정의하지 않았다면, 하위 클래스의 인스턴스를 역직렬화 하면 상위 클래스의 인스턴스를 생성하여 ClassCastException을 일으킬 수 있다.
📢 댓글로 각자의 학습 내용을 공유해주세요!