cleanUrl: /posts/for-instance-control-prefer-enum-types-to-readResolve
item3 에서 singleton 패턴을 설명할때 생성자를 호출하지 못하게 막는 방식으로 jvm 내에 instance 가 오직 하나만 만들어짐을 보장했다.
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() {}
public void leaveTheBuilding() {
// ...
}
}
하지만 implements Serializable
을 추가하는 순간 더 이상 싱글턴이 아니다.
readObject
를 사용하든지 이 클래스가 초기화될 때 만들어진 instance 와는 별개인 instance 를 반환한다
대신 readResolve
를 사용하면 readObject
가 만들어낸 instance 를 대체할 수 있는데
대부분의 경우 이때 새로 생성된 객체의 참조는 유지하지 않고 GC 대상에 들어가게 된다.
public class Elvis implements Serializable {
public static final Elvis INSTANCE = new Elvis();
private Elvis() {}
public void leaveTheBuilding() { ... }
// instance 통제를 위한 readReolve - 개선의 여지가 있다.
private Object readResolve() {
// 진짜 Elvis 를 반환하고, 가짜 Elvis 는 GC 에 맡긴다.
return INSTANCE;
}
}
readResolve
는 deserialize 된 객체는 무시하고 class 가 초기화 때 만들어진 Elvis
를 반환한다.
따라서 모든 필드를 transient
로 선언해야 한다.
readResolve
사용 목적이 instance 개수를 통제하기 위함이라면 모두 transient
로 선언하자
만약 이러한 singleton 이 transient
가 아닌 참조 필드를 갖고 있다면 그 필드의 내용은 readResolve
가 실행되기 전에 역직렬화 된다. 그렇다면 앞선 MutualPeriod
와 같이 공격이 가능해지는데
스트림을 잘 조작하여 역직렬화 되는 시점에 그 역직렬화된 참조를 훔칠 수 있다.
더 상세한 설명을 위해 도둑(Stealer) 라는 클래스를 만든다. 이 클래스는 Elvis
의 직렬화된 singleton 을 참조하는 역할을 하고, 직렬화된 stream 에서 singleton 의 비휘발성 필드를 이 Stealer 의 instance로 교체한다.
public class Elvis implements Serializable {
public static final Elvis INSTANCE = new Elvis();
private Elvis() {}
**private String[] favoriteSongs = {"Hound Dog", "Heartbreak Hotel"}**
public void printFavorite() {
System.out.println(Arrays.toString(favoriteSongs));
}
private Object readResolve() {
return INSTANCE;
}
}
public class ElvisStealer implements Serializable (
static Elvis impoersonator;
private Elvis payload;
private Object readResolve() {
impersonator = payload;
return new String[] {"A Fool Such as I"};
}
private static final long serialVersionUID = 0;
}
Stealer
는 Elvis
를 참조하고 있고, 역직렬화 될때 Stealer
의 readResolve
가 먼저 호출된다.