Skip to content

[아이템 89] 인스턴스 수를 통제해야 한다면 readResolve 보다는 열거 타입을 사용하라 #91

@meloncha

Description

@meloncha

싱글턴 패턴에 직렬화를 적용하면?

  • 더 이상 싱글턴이 아니게 된다
  • 기본 직렬화를 쓰지 않더라도, 명시적인 readObject를 제공하더라도 소용없다
  • 어떤 readObject를 사용하든 이 클래스가 초기화될 때 만들어진 인스턴스와는 별개인 인스턴스를 반환

해결 방법 - readResolve 이용하기

  • 역직렬화한 객체의 클래스가 readResolve 메서드를 적절히 정의해두면, 역직렬화 후 새로 생성된 객체를 인수로 이 메서드가 호출되고, 이 메서드가 반환한 객체 참조가 새로 생성된 객체를 대신해 반환된다
  • 새로 생성된 객체의 참조는 유지하지 않으므로 바로 가비지 컬렉션 대상이 된다

Untitled

// 인스턴스 통제를 위한 readResolve
private Object readResolve() {
	// 기존에 생성된 인스턴스 반환
	return INSTANCE;
}
  • 이 메서드는 역직렬화한 객체는 무시하고 클래스 초기화 때 만들어진 인스턴스를 반환한다
  • 직렬화 형태는 아무런 실 데이터를 가질 필요가 없으니 모든 인스턴스 필드를 transient로 선언해야 한다
  • transient로 선언하지 않으면 readResolve 메서드가 수행되기 전에 역직렬화된 객체의 참조를 공격할 수 있다

공격 방법

  • 싱글턴이 transient가 아닌 참조 필드를 가지고 있다면, 필드의 내용은 readResolve 메소드가 실행되기 전에 역직렬화된다
  • 역직렬화되는 시점에 그 역직렬화된 인스턴스의 참조를 훔칠 수 있다
  1. readResolve 메서드와 인스턴스 필드 하나를 포함한 도둑 클래스를 만든다.
  2. 도둑 클래스의 인스턴스 필드는 직렬화된 싱글턴을 참조하는 역할을 한다.
  3. 직렬화된 스트림에서 싱글턴의 비휘발성 필드를 도둑의 인스턴스 필드로 교체한다.
  4. 싱글턴이 도둑을 포함하므로 역직렬화시 도둑 클래스의 readResolve가 먼저 호출된다.
  5. 도둑 클래스의 인스턴스 필드에는 역직렬화 도중의 싱글턴의 참조가 담겨있게 된다.
  6. 도둑 클래스의 readResolve 메서드는 인스턴스 필드가 참조한 값을 정적 필드로 복사한다.
  7. 싱글턴은 도둑이 숨긴 transient가 아닌 필드의 원래 타입에 맞는 값을 반환한다.
  8. 이 과정을 생략하면 직렬화 시스템이 도둑의 참조를 이 필드에 저장하려 할 때 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로 선언해야 한다

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions