Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ Item89 ] 인스턴스 수를 통제해야 한다면 readResolve 보다는 열거 타입을 사용하라 #86

Open
SooKim1110 opened this issue Jul 2, 2022 · 0 comments
Assignees
Labels
12장 직렬화

Comments

@SooKim1110
Copy link

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

싱글턴 패턴과 Serializable

아래 예시처럼 바깥에서 생성자를 호출하지 못하게 막는 방식으로 인스턴스가 하나만 만들어지도록 할 수 있다

public class Elvis {
  public static final Elvis INSTANCE = new Elvis();
  private Elvis(){
    
  }

  public void leaveTheBuilding(){...}
}

하지만 이 클래스의 선언에 implements Serializable을 추가하면 더 이상 싱글턴이 아니게 된다!
(어떤 readObject를 사용하든 이 클래스가 초기화될 때 만들어진 인스턴스와는 별개인 인스턴스를 반환하게 된다)

이 때, readResolve 메소드를 이용하면 readObject가 만든 인스턴스를 다른 것으로 대체하는 것이 가능하다. readResolve 메서드를 정의했다면, 역직렬화 후 새로 생성된 객체를 인수로 정의된 readResolve 메서드가 호출되어 객체 참조를 반환한다.

public class Elvis implements Serializable {
  public static final Elvis INSTANCE = new Elvis();
  private Elvis(){
    
  }

  public void leaveTheBuilding(){...}
  private Object readResolve() {
      // 역직렬화된 객체는 무시하고(가비지 컬렉션 대상이 된다), 클래스 초기화 때 만들어진 Elvis 인스턴스를 반환한다
      return INSTANCE;
  }
}

그러면 Elvis 인스턴스의 직렬화 형태는 실데이터를 가질 이유가 없어지기 때문에 모든 인스턴스 필드는 transient로 선언해야 한다. (readResolve를 인스턴스 통제 목적으로 쓸 때는 모든 필드를 transient로 선언해야하는 것이다)

❗️ transient로 선언하지 않을 경우, 역직렬화 과정에서 역직렬화된 인스턴스의 참조를 가져올 수 있다는 문제가 있다. 아래의 예시를 보자.

readResolve가 수행되기 전 역직렬화된 객체의 참조를 공격하는 법

싱글턴이 non-transient 참조 필드를 가지고 있으면 그 필드의 내용은 readResolve메서드가 실행되기 전에 역직렬화된다. 그러면 잘 조작된 스트림을 쓰면 해당 필드가 역직렬화되는 시점에 역직렬화된 인스턴스의 참조를 훔쳐올 수 있다.

잘못된 싱글턴(transient 선언을 하지 않았다)

public class Elvis implements Serializable {
    public static final Elvis INSTANCE = newElvis();

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

싱글턴 객체를 2개 생성하는 프로그램

public class ElvisImpersonator {
    // 진짜 Elvis 인스턴스로는 만들어질 수 없는 바이트 스트림
    private static final byte[] serializedForm = {
            -84, -19, 0, 5, 115, 114, 0, 20, 107, 114, 46, 115,
            101, 111, 107, 46, 105, 116, 101, 109, 56, 57, 46, 69,
            108, 118, 105, 115, 98, -14, -118, -33, -113, -3, -32,
            70, 2, 0, 1, 91, 0, 13, 102, 97, 118, 111, 114, 105, 116,
            101, 83, 111, 110, 103, 115, 116, 0, 19, 91, 76, 106, 97,
            118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110,
            103, 59, 120, 112, 117, 114, 0, 19, 91, 76, 106, 97, 118,
            97, 46, 108, 97, 110, 103, 46, 83, 116, 114, 105, 110, 103,
            59, -83, -46, 86, -25, -23, 29, 123, 71, 2, 0, 0, 120, 112,
            0, 0, 0, 2, 116, 0, 9, 72, 111, 117, 110, 100, 32, 68, 111,
            103, 116, 0, 16, 72, 101, 97, 114, 116, 98, 114, 101, 97, 107,
            32, 72, 111, 116, 101, 108
    };

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

Elvis는 싱글톤으로 설계했는데 지금 서로 다른 2개의 Elvis 인스턴스가 생성되었음을 알 수 있다. 아래와 같은 과정으로 공격이 가능한 것이다.

    1. favoriteSongs 라는 non-transient 참조 필드가 있다
    1. 바이트 코드에서 Elvis의 favoriteSongs 부분을 ElvisStealer 인스턴스로 교체한다
    1. 그러면 readResolve를 호출할 때 Elvis(new).favoriteSongs = ElvisStealer(new).readResolve() 처럼 코드가 실행된다
    1. 그러면 Elvis 역직렬화를 할 때 favoriteSongs이 ElvisStealer를 참조하고 있기 때문에 ElvisStealer의 readResolve 코드를 호출한다
    1. 그래서 ElvisStealer의 readResolve에서 반환하는 String[]이 반환된다

이해하는데 참고할만한 글:
https://stackoverflow.com/questions/37660696/elvisstealer-from-effective-java
https://stackoverflow.com/questions/72583310/effective-in-java-item-89-for-instance-control-prefer-enum-types-to-readresolv

💡 favoriteSongs 필드를 transient로 선언하여 문제를 해결할 수 있지만, Elvis를 원소 하나짜리 열거타입으로 바꾸는 것이 나은 선택이다.

열거 타입 싱글턴

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

public enum Elvis {
    INSTANCE;
    private String[] favoriteSongs = {"Hound Dog", "Heartbreak Hotel"};

    public void printFavorites() {
        System.out.printtn(Arrays.toString(favoriteSongs));
    }
}

다만, 컴파일타임에는 어떤 인스턴스들이 있는지 알 수 없는 상황일 때는 열거 타입으로 표현할 수 없으므로 readResolve를 써야한다. 이 때 readResolve 메서드의 접근성에 주의해야한다.

핵심 정리

  • 불변식을 지키기 위해 인스턴스를 통제해야 할 때는 열거 타입을 사용하자
  • 컴파일타임에 어떤 인스턴스들이 있는지 알 수 없을 때는 readResolve 메소드를 작성해야한다. 이 때 클래스에서 모든 참조 타입 인스턴스 필드를 transient로 선언해야한다.
@SooKim1110 SooKim1110 added the 12장 직렬화 label Jul 2, 2022
@SooKim1110 SooKim1110 self-assigned this Jul 2, 2022
@ruthetum ruthetum changed the title # [ Item89 ] 인스턴스 수를 통제해야 한다면 readResolve 보다는 열거 타입을 사용하라 [ Item89 ] 인스턴스 수를 통제해야 한다면 readResolve 보다는 열거 타입을 사용하라 Jul 3, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
12장 직렬화
Projects
None yet
Development

No branches or pull requests

1 participant