[이펙티브자바] item89. 인스턴스 수를 통제해야 한다면 readResolve 보다는 열거 타입을 사용하라

wally·2022년 11월 28일
0

싱글톤과 직렬화

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

    ...
}
  • 위 코드는 싱글톤 패턴이다.(바깥에서 생성자를 호출하지 못하게 막아 인스턴스가 1개만 만들어짐을 보장한다)
  • 위 코드에 implements Serializable을 추가하는 순간 더이상 싱글턴이 아니게 된다.
  • readObject를 제공해도 소용이 없으면 어떤 readObject를 사용하든 이 클래스가 초기화 될 떄 만들어진 인스턴스와는 별개인 인스턴스를 반환하게 된다.
  • 실제 싱글톤 인스턴스를 직렬화/역직렬화 하면 == 연산자와 equals 연산자가 false 가 나오는 것을 확인할 수 있다.
    실제 코드 링크

readResolve

  • 이때 readResolve 메서드를 이용하면 readObject 메서드가 만든 인스턴스를 다른 것으로 대체할 수 있다.
  • 즉 역직렬화 과정에서 만들어진 인스턴스 대신에 기존에 생성된 싱글톤 인스턴스를 반환하게 한다.
  • 실제로 역직렬화 과정에서 자동으로 호출되는 readObject 메서드가 있더라도 readResolve 메서드에서 반환한 인스턴스로 대체된다.
  • 이때 readObject 가 만들어낸 인스턴스는 가비지 컬렉션의 대상이 된다.
  • 그 후 ==연산자와 equals 메서드로 비교하면 true 가 뜬다.
private Object readResolve() {
    // 기존에 생성된 진짜 인스턴스를 반환하고 가짜 인스턴스는 GC 에 맡긴다
    return INSTANCE;
}
  • 한편 여기서 살펴본 Elvis 인스턴스의 직렬화 형태는 아무런 실 데이터를 가질 필요가 없으니 모든 인스턴스 필드는 transient 로 선언해야 한다.
  • 그러니까 readResolve 메서드를 인스턴스의 통제 목적으로 이용한다면 모든 필드는 transient로 선언해야 한다.
  • 만일 그렇지 않으면 역직렬화(Deserialization) 과정에서 역직렬화된 인스턴스를 가져올 수 있다. 즉, 싱글턴이 깨지게 된다.

Transient

  • 객체의 필드 중에 직렬화 하지 않을 것들을 지정하기 위해 사용한다.
  • 비밀번호 암호등이 예시가 될 수 있다.(민감한 정보)

보안

  • readResolve를 인스턴스 통제 목적으로 사용한다면 객체 참조 타입 인스턴스 필드는 모두 transient로 선언하자.
  • 이렇게 하지 않으면 역직렬화된 객체 참조를 공격할 여지가 남는다.
  1. readResolve 메서드와 인스턴스 필드 하나를 포함한 도둑 클래스를 만든다.
  2. 도둑 클래스의 인스턴스 필드는 직렬화된 싱글턴을 참조하는 역할을 한다.
  3. 직렬화된 스트림에서 싱글턴의 비휘발성 필드를 도둑의 인스턴스 필드로 교체한다.
  4. 싱글턴이 도둑을 포함하므로 역직렬화시 도둑 클래스의 readResolve가 먼저 호출된다.
  5. 도둑 클래스의 인스턴스 필드에는 역직렬화 도중의 싱글턴의 참조가 담겨있게 된다.
  6. 도둑 클래스의 readResolve 메서드는 인스턴스 필드가 참조한 값을 정적 필드로 복사한다.
  7. 싱글턴은 도둑이 숨긴 transient가 아닌 필드의 원래 타입에 맞는 값을 반환한다.
  8. 이 과정을 생략하면 직렬화 시스템이 도둑의 참조를 이 필드에 저장하려 할 때 ClassCastException 이 발생한다.
// tranient가 아닌 참조 필드를 가지는 싱글턴
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 {
  private static final long serialVersionUID = 0;
  static Elvis impersonator;
  private Elvis payload;

  private Object readResolve() {
    // resolve되기 전의 Elvis 인스턴스의 참조를 저장
    impersonator = payload;
    // favoriteSongs 필드에 맞는 타입의 객체를 반환
    return new String[] {"There is no cow level"};
  }
}
// 직렬화의 약점을 이용해 싱글턴 객체를 2개 생성한다.
public class ElvisImpersonator {
  private static final byte[] serializedForm = new byte[]{
      (byte) 0xac, (byte) 0xed, 0x00, 0x05, 0x73, 0x72, 0x00, 0x05,
      // 코드 생략
  };

  private static Object deserialize(byte[] sf) {
        try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(sf)) {
      try (ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream)) {
        return objectInputStream.readObject();
      } catch (IOException | ClassNotFoundException e) {
        throw new IllegalArgumentException(e);
      }
  }

  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(); // [There is no cow level]
  }
}

Enum 사용

  • enum을 사용하면 모든 것이 해결된다.
  • 자바가 선언한 상수 외에 다른 객체가 없음을 보장해주기 때문이다.
  • AccessibleObject.setAccessible 메서드와 같은 리플렉션을 사용했을 때는 예외다.
public enum Elvis {
    INSTANCE;
    
    ...필요한 데이터들
}
  • 인스턴스 통제를 위해 readResolve 메서드를 사용하는 것이 중요할 때도 있다.
  • 직렬화 가능 인스턴스 통제 클래스를 작성해야 할 때, 컴파일 타임에는 어떤 인스턴스들이 있는지 모를 수 있는 상황이라면 열거 타입으로 표현하는 것이 불가능하기 때문에 readResolve 메서드를 사용할 수 밖에 없다.

readResolve 메서드의 접근성

  • final 클래스라면 readResolve 메서드는 private 이어야 한다.
  • final 이 아닌 경우는 주의해야한다.
  • private 선언시 하위 클래스에서 사용할 수 없다.
  • package-private 으로 선언시 같은 패키지에 속한 하위 클래스에서만 사용할 수 있다.
  • protected, public 은 재정의하지 않은 모든 하위 클래스에서 사용할 수 있다.
  • protected, public 이면서 하위클래스에서 재정의 하지 않으면, 하위 클래스의 인스턴스를 역직렬화하면 상위 클래스의 인스턴스를 생성하여 ClassCastException 을 일으킬 수 있다.
profile
클린코드 지향

0개의 댓글