Skip to content

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

@ideasidus

Description

@ideasidus

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

✨ 핵심 내용

  1. 불변식을 위해 인스턴스를 통제해야 한다면 열거타입을 사용하자.
  2. 이가 여의치 않은 상황에서 직렬화와 인스턴스 통제가 모두 필요하면 readResolve 메서드를 사용하고, 그 클래스에서 모든 참조 타입 인스턴스 필드를 transient로 선언 해야한다.

💡 새롭게 알게 된 점

  • 조슈아 블로크는 ENUM을 사랑해
  • ENUM을 사용하면 리플렉션으로 생성자를 호출하는 것 역시 자연스럽게 방지할 수 있다.
  • 직렬화 하지 말자

📚 정리

  1. Singleton에 implements Serializable을 추가하는 순간 더 이상 Singleton이 아니게 된다. 기본 직렬화를 사용하지 않더라도, 명시적인 readObject를 제공해도 소용없다. 이 클래스가 초기화 될 때 만들어진 인스턴스와는 별개인 인스턴스를 반환하게 된다.

  2. readResolve 메서드를 적절히 정의해 뒀다면, 역직렬화 후 새로 생성된 객체를 인수로 이 메서드가 호출되고, 이 메서드가 반환한 객체 참조가 새로 생성된 객체를 대신해 반환된다.

    // 인스턴스 통제를 위한 readResolve
    private Object readResolve() {
        // 진짜 Elvis를 반환하고, 가짜 Elvis는 가비지 컬렉터에 맡긴다.
        return INSTANCE;
    }
    // 이 경우 Elvis 인스턴스의 직렬화 형태는 아무런 실 데이터를 가질 이유가 없으니 모든 인스턴스 필드를 transient 로 선언해야 한다.
    1. readResolve를 인스턴스 통제 목적으로 사용한다면 객체 참조타임 인스턴스 필드는 모두 transient 로 선언해야 한다.
    2. 그렇지 않으면 readResolve 메서드가 수행되기 전에 역직렬화 된 객체의 참조를 공격할 여지가 남는다.
  3. 싱글턴이 transient가 아닌 참조 필드를 가지고 있다면 그 필드의 내용은 readResolve 메서드 수행 전에 역직렬화 된다. 이때 잘 조작된 스트림을 써 해당 참조 필드의 내용이 역직렬화 되는 시점에 그 역직렬화 된 인스턴스의 참조를 훔쳐올 수 있다.

    1. readResolve 메소드와 인스턴스 필드 하나를 포함한 도둑 클래스를 작성. 이 인스턴스 필드는 도둑이 숨길 직렬화된 싱글턴을 참조하는 역할을 한다.
    2. 직렬화된 스트림에서 싱글턴의 비 휘발성 필드를 이 도둑의 인스턴스로 교체한다.
    3. 싱글턴은 도둑을 참조하고 도둑은 싱글턴을 참조하는 순환고리가 만들어 졌다.
    4. 싱글턴이 도둑을 포함하므로 싱글턴이 역직렬화될 때 도둑의 readResolve 메서드가 먼저 호출된다. 그 결과 도둑의 readResolve 메서드가 수행될 때 도둑의 인스턴스 필드에는 역직렬화 도중인(readResolve가 수행되기 전의 싱글턴의 참조가 담겨 있다.)
    5. 도둑의 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;
     }
  4. 필드를 transient로 선언해 이 문제를 고칠 수 있지만, Elvis를 원소 하나짜리 열거 타입으로 바꾸는 편이 더 나은 선택이다

  5. ElvisStealer 공격으로 보여줬듯이 readResolve 메서드를 사용해 '순간적으로' 만들어진 역직렬화된 인스턴스에 접근하지 못하게 하는 방법은 깨지기 쉽고 신경을 많이 써야한다.

  6. 직렬화 가능한 인스턴스 통제 클래스를 열거 타입을 이용해 구현하면 선언한 상수 외의 다른 객체는 존재하지 않음을 자바가 보장해 준다.

    1. 물론 공격자가 임의의 네이티브 코드를 수행할 수 있는 특권을 가로챈 경우 무력화된다.
public enum Elvis {
    INSTANCE;
    private String[] favoriteSongs = { "Hound Dog", "Heartbreak Hotel" };
    public void printFavorites() {
        System.out.println(Arrays.toString(favoriteSongs));
    }
}
  1. 인스턴스 통제를 위해 readResolve를 사용하는 방식이 완전히 쓸모없는 것은 아니다. 컴파일 타임에 어떤 인스턴스가 있는지 알 수 없는 상황이라면 열거 타입으로 표현할 수 없다.
  2. readResolve 메서드의 접근성은 매우 중요하다. final 클래스라면 readResolve 메서드는 private 이어야 한다. final 이 아닌 클래스는 다음을 주의해야 한다.
    1. private로 선언하면 하위 클래스에서 사용할 수 없다.
    2. package-private으로 선언하면 같은 패키지에 속한 하위 클래스에서만 사용할 수 있다.
    3. protected나 public으로 선언하면 이를 재정의 하지 ㅇ낳은 모든 하위 클래스에서 사용할 수 있다.
    4. protected나 public이면서 하위 클래스에서 재정의하지 않았다면, 하위 클래스의 인스턴스를 역직렬화 하면 상위 클래스의 인스턴스를 생성하여 ClassCastException을 일으킬 수 있다.

📢 댓글로 각자의 학습 내용을 공유해주세요!

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions