JAVA/Effective Java

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

꾸준함. 2024. 5. 18. 03:49

싱글턴이란?

  • 클래스의 인스턴스가 오직 1개만 생성되는 것을 보장하는 디자인 패턴

 

싱글턴 객체를 직렬화할 때 발생하는 문제점

  • 클래스에 마커 인터페이스인 Serializable을 구현하는 순간 더 이상 싱글턴 객체가 아님
  • 아이템 87에서 언급한 커스텀 직렬화를 사용하더라도, 아이템 88에서 언급한 커스텀 readObject() 메서드를 사용하더라도 소용없음
    • 어떤 readObject() 메서드를 선언하든 해당 클래스가 초기화될 때 만들어진 인스턴스와는 별개인 인스턴스를 반환

 

readResolve() 기능을 이용할 때

  • readObject()가 생성한 인스턴스를 다른 것으로 대체 가능
  • 역직렬화한 객체의 클래스가 readResolve() 메서드를 적절히 구현했다면 역직렬화 후 새로 생성된 객체를 인수로 해당 메서드가 호출되고 해당 메서드가 반환한 객체 참조가 새로 생성된 객체를 대신해 반환
  • 대부분의 경우 이때 새로 생성된 객체의 참조는 유지하지 않으므로 바로 GC 대상이 됨

 

readResolve() 메서드를 통해 싱글턴 속성을 유지하는 방법

  • readResolve() 메서드가 역직렬화한 객체를 무시하고 클래스 초기화 때 생성된 인스턴스 반환
    • 이때 인스턴스의 직렬화 형태는 아무런 실 데이터를 가질 이유가 없기 때문에 모든 인스턴스 필드를 transient로 선언
    • 정리하자면 객체 참조 타입 인스턴스 필드는 모두 transient로 선언
    • 위 조건이 충족되지 않을 경우 아이템 88에서 언급한 MutablePeriod 공격과 유사하게 readResolve() 메서드를 호출하기 전 역직렬화된 객체의 참조를 공격할 여지가 남음

 

역직렬화 공격 파헤치기

  • 싱글턴 객체가 transient가 아닌 참조 필드를 지니고 있다면 해당 필드 내용은 readResolve() 메서드가 호출되기 전에 역직렬화됨
    • 이때 잘 조작된 스트림을 써서 해당 참조 필드의 내용이 역직렬화되는 시점에 그 역직렬화된 인스턴스의 참조를 훔쳐올 수 있음 (ElvisStealer 참고)

 

public class Elvis implements Serializable {
private static final long serialVersionUID = -8870240565519414478L;
public static final Elvis INSTANCE = new Elvis();
private String[] favoriteSongs = {"Hound Dog", "Heartbreak Hotel"};
private Elvis() {
}
public void printFavorites() {
System.out.println(Arrays.toString(favoriteSongs));
}
private Object readResolve() throws ObjectStreamException {
return INSTANCE;
}
}
public class ElvisStealer implements Serializable {
private static final long serialVersionUID = 0;
static Elvis impersonator;
private Elvis payload;
private Object readResolve() {
// Save a reference to the "unresolved" Elvis instance
impersonator = payload;
// Return an object of correct type for favorites field
return new String[] {"A Fool Such as I"};
}
}
public class ElvisImpersonator {
// Byte stream could not have come from real Elvis instance!
private static final byte[] serializedForm = new byte[] {(byte)0xac, (byte)0xed, 0x00, 0x05, 0x73, 0x72, 0x00,
0x05, 0x45, 0x6c, 0x76, 0x69, 0x73, (byte)0x84, (byte)0xe6, (byte)0x93, 0x33, (byte)0xc3, (byte)0xf4,
(byte)0x8b, 0x32, 0x02, 0x00, 0x01, 0x4c, 0x00, 0x0d, 0x66, 0x61, 0x76, 0x6f, 0x72, 0x69, 0x74, 0x65, 0x53,
0x6f, 0x6e, 0x67, 0x73, 0x74, 0x00, 0x12, 0x4c, 0x6a, 0x61, 0x76, 0x61, 0x2f, 0x6c, 0x61, 0x6e, 0x67, 0x2f,
0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x3b, 0x78, 0x70, 0x73, 0x72, 0x00, 0x0c, 0x45, 0x6c, 0x76, 0x69, 0x73,
0x53, 0x74, 0x65, 0x61, 0x6c, 0x65, 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01,
0x4c, 0x00, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x74, 0x00, 0x07, 0x4c, 0x45, 0x6c, 0x76, 0x69,
0x73, 0x3b, 0x78, 0x70, 0x71, 0x00, 0x7e, 0x00, 0x02};
public static void main(String[] args) {
// Initializes ElvisStealer.impersonator and returns
// the real Elvis (which is Elvis.INSTANCE)
Elvis elvis = (Elvis)deserialize(serializedForm);
Elvis impersonator = ElvisStealer.impersonator;
elvis.printFavorites();
impersonator.printFavorites();
}
// Returns the object with the specified serialized form
private static Object deserialize(byte[] sf) {
try (InputStream is = new ByteArrayInputStream(sf);
ObjectInputStream ois = new ElvisImpersonator.CustomObjectInputStream(is)) {
return ois.readObject();
} catch (Exception e) {
throw new IllegalArgumentException(e);
}
}
private static class CustomObjectInputStream extends ObjectInputStream {
public CustomObjectInputStream(InputStream in) throws IOException {
super(in);
}
@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
String name = desc.getName();
if ("ElvisStealer".equals(name)) {
return com.tistory.jaimemin.effectivejava.ch12.item89.ElvisStealer.class;
}
if ("Elvis".equals(name)) {
return com.tistory.jaimemin.effectivejava.ch12.item89.Elvis.class;
}
return super.resolveClass(desc);
}
}
}
view raw .java hosted with ❤ by GitHub

 

부연 설명

  • readResolve() 메서드와 인스턴스 필드 하나를 포함한 ElvisStealer 클래스 작성
  • 해당 클래스의 인스턴스 필드는 도둑이 `숨길` 직렬화된 싱글턴을 참조하는 역할
  • 직렬화된 스트림에서 싱글턴의 비휘발성 필드를 ElvisStealer의 인스턴스로 교체
  • 이제 싱글턴 객체는 ElvisStealer를 참조하고 ElvisStealer는 싱글턴 객체를 참조하는 순환고리가 만들어짐
  • 싱글턴 객체가 도둑을 포함하므로 싱글턴 객체가 역직렬화될 때 ElvisStealer의 readResolve() 메서드가 먼저 호출됨
  • 그 결과 ElvisStealer의 readResolve() 메서드가 수행될 때 ElvisStealer의 인스턴스 필드에는 역직렬화 도중인 그리고 readResolve() 메서드가 수행되기 전인 싱글턴 객체의 참조가 담겨 있음
  • ElvisStealer의 readResolve() 메서드는 해당 인스턴스 필드가 참조한 값을 정적 필드로 복사하여 readResolve() 메서드가 끝난 후에도 참조할 수 있도록 하고 해당 메서드는 ElvisStealer가 숨긴 transient가 아닌 필드의 원래 타입에 맞는 값을 반환
    • 위 과정을 생략하면 직렬화 시스템이 ElvisStealer의 참조를 해당 필드에 저장하려 할 때 JVM이 ClassCastException 예외를 던짐

 

  • 결론적으로 직렬화의 허점을 이용해 싱글턴 객체를 2개 생성한 꼴
    • 도둑 클래스 공격으로 보여줬듯이 readResolve() 메서드를 사용해 `순간적으로` 만들어진 역질렬화된 인스턴스에 접근하지 못하게 하는 방법은 깨지기 쉽고 신경을 많이 써야 하는 작업
    • favoriteSongs 필드를 transient로 선언하여 위 문제를 고칠 수 있지만 해당 클래스를 원소 하나짜리 열거 타입으로 바꾸는 편이 더 나은 선택

 

싱글턴을 보장하는 Enum 클래스


public enum Elvis {
INSTANCE;
private String[] favoriteSongs = {"Hound Dog", "Heartbreak Hotel"};
public void printFavorites() {
System.out.println(Arrays.toString(favoriteSongs));
}
}
view raw .java hosted with ❤ by GitHub

 

부연 설명

  • 직렬화 가능한 인스턴스 통제 클래스를 열거 타입을 이용해 구현할 경우 선언한 상수 외의 다른 객체는 존재하지 않음을 자바가 보장
  • 공격자가 AccessibleObject.setAccessible과 같은 priviledged 메서드를 악용하지 않는 이상 직렬화 공격으로부터 방어할 수 있음
  • 인스턴스 통제를 위해 readResolve() 메서드를 사용하는 방식이 완전히 쓸모없는 것은 아님
    • 직렬화 가능한 인스턴스를 통제하는 클래스를 작성해야 하는데, 컴파일 타임에는 어떤 인스턴스들이 있는지 알 수 없는 상황이라면 열거 타입으로 표현할 수 없음

 

readResolve() 메서드의 접근 제한자

  • final 클래스인 경우 readResolve() 메서드는 private 접근 제한자여야 함
  • final이 아닌 클래스의 경우 readResolve() 메서드의 접근 제한자가
    • private일 경우 하위 클래스에서 사용할 수 없음
    • package-private으로 선언할 경우 같은 패키지에 속한 하위 클래스에서만 사용 가능
    • protected나 public으로 선언할 경우 이를 재정의하지 않은 모든 하위 클래스에서 사용 가능하지만 하위 클래스의 인스턴스를 역직렬화하면 상위 클래스의 인스턴스를 생성하여 ClassCastException 예외 발생시킴

 

정리

  • 불변식을 지키기 위해 인스턴스 개수를 하나 즉 싱글턴 객체로 통제해야 한다면 가능한 한 열거 타입을 사용하자
  • 여의치 않은 상황에서 직렬화와 인스턴스 통제가 모두 필요할 경우 readResolve() 메서드를 재정의해야 하고 해당 크래스 내 모든 참조 타입 인스턴스 필드를 transient 필드로 선언하자

 

참고

이펙티브 자바

반응형