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;
}