-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Labels
Description
싱글턴 패턴에 직렬화를 적용하면?
- 더 이상 싱글턴이 아니게 된다
- 기본 직렬화를 쓰지 않더라도, 명시적인 readObject를 제공하더라도 소용없다
- 어떤 readObject를 사용하든 이 클래스가 초기화될 때 만들어진 인스턴스와는 별개인 인스턴스를 반환
해결 방법 - readResolve 이용하기
- 역직렬화한 객체의 클래스가 readResolve 메서드를 적절히 정의해두면, 역직렬화 후 새로 생성된 객체를 인수로 이 메서드가 호출되고, 이 메서드가 반환한 객체 참조가 새로 생성된 객체를 대신해 반환된다
- 새로 생성된 객체의 참조는 유지하지 않으므로 바로 가비지 컬렉션 대상이 된다
// 인스턴스 통제를 위한 readResolve
private Object readResolve() {
// 기존에 생성된 인스턴스 반환
return INSTANCE;
}- 이 메서드는 역직렬화한 객체는 무시하고 클래스 초기화 때 만들어진 인스턴스를 반환한다
- 직렬화 형태는 아무런 실 데이터를 가질 필요가 없으니 모든 인스턴스 필드를 transient로 선언해야 한다
- transient로 선언하지 않으면 readResolve 메서드가 수행되기 전에 역직렬화된 객체의 참조를 공격할 수 있다
공격 방법
- 싱글턴이 transient가 아닌 참조 필드를 가지고 있다면, 필드의 내용은 readResolve 메소드가 실행되기 전에 역직렬화된다
- 역직렬화되는 시점에 그 역직렬화된 인스턴스의 참조를 훔칠 수 있다
- readResolve 메서드와 인스턴스 필드 하나를 포함한 도둑 클래스를 만든다.
- 도둑 클래스의 인스턴스 필드는 직렬화된 싱글턴을 참조하는 역할을 한다.
- 직렬화된 스트림에서 싱글턴의 비휘발성 필드를 도둑의 인스턴스 필드로 교체한다.
- 싱글턴이 도둑을 포함하므로 역직렬화시 도둑 클래스의 readResolve가 먼저 호출된다.
- 도둑 클래스의 인스턴스 필드에는 역직렬화 도중의 싱글턴의 참조가 담겨있게 된다.
- 도둑 클래스의 readResolve 메서드는 인스턴스 필드가 참조한 값을 정적 필드로 복사한다.
- 싱글턴은 도둑이 숨긴 transient가 아닌 필드의 원래 타입에 맞는 값을 반환한다.
- 이 과정을 생략하면 직렬화 시스템이 도둑의 참조를 이 필드에 저장하려 할 때 ClassCastException 이 발생한다.
- 죄송하지만 잘 이해가 안갑니다..
참고 https://stackoverflow.com/questions/37660696/elvisstealer-from-effective-java
잘못된 싱글턴 - transient가 아닌 참조 필드를 가지는 경우
- 예시 코드
public class Elvis implements Serializable {
public static final Elvis INSTANCE = new Elvis();
private Elvis() {}
// transient 가 아님
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"};
}public static void main(String[] args) {
// ElvisStealer.impersonator 를 초기화한 다음,
// 진짜 Elvis(즉, Elvis.INSTANCE)를 반환
Elvis elvis = (Elvis) deserialize(serializedForm);
Elvis impersonator = ElvisStealer.impersonator;
elvis.printFavorites(); // [Hound Dog, Heartbreak Hotel]
impersonator.printFavorites(); // [A Fool Such as I]
}본질적인 해결 방법 - Enum 활용하기
- 열거 타입을 이용해 구현하면 선언한 상수 외의 다른 객체는 존재하지 않음을 자바가 보장해준다
- reflection은 예외이다
- 예시 코드
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 이 아닌 클래스의 경우 readResolve 의 접근성에 따른 영향을 받는다 (private, default, protected, public)
- protected나 public 이며 하위 클래스에서 재정의하지 않은 경우, 하위 클래스의 인스턴스를 역직렬화하면 상위 클래스의 인스턴스를 생성하여 ClassCastException 을 일으킬 수 있다
정리
- 불변식을 지키기 위 해 인스턴스를 통제해야 한다면 가능한 열거 타입을 사용하기
- 열거 타입이 안되면 readResolve 메서드를 작성해서 넣고 모든 참조 타입 인스턴스 필드를 transient로 선언해야 한다
