Search
Duplicate
🎽

싱글턴 패턴에 serializable 적용하기

태그
Effective Java
Java
Serializable
공개여부
작성일자
2021/07/31
item3 에서 singleton 패턴을 설명할때 생성자를 호출하지 못하게 막는 방식으로 jvm 내에 instance 가 오직 하나만 만들어짐을 보장했다.
public class Elvis { public static final Elvis INSTANCE = new Elvis(); private Elvis() {} public void leaveTheBuilding() { // ... } }
Java
singleton pattern JVM 내에서 딱 하나의 인자만 갖는다
하지만 implements Serializable 을 추가하는 순간 더 이상 싱글턴이 아니다.
readObject 를 사용하든지 이 클래스가 초기화될 때 만들어진 instance 와는 별개인 instance 를 반환한다
대신 readResolve 를 사용하면 readObject 가 만들어낸 instance 를 대체할 수 있는데
대부분의 경우 이때 새로 생성된 객체의 참조는 유지하지 않고 GC 대상에 들어가게 된다.
public class Elvis implements Serializable { public static final Elvis INSTANCE = new Elvis(); private Elvis() {} public void leaveTheBuilding() { ... } // instance 통제를 위한 readReolve - 개선의 여지가 있다. private Object readResolve() { // 진짜 Elvis 를 반환하고, 가짜 Elvis 는 GC 에 맡긴다. return INSTANCE; } }
Java
readResolve 는 deserialize 된 객체는 무시하고 class 가 초기화 때 만들어진 Elvis 를 반환한다.
따라서 모든 필드를 transient 로 선언해야 한다.
readResolve 사용 목적이 instance 개수를 통제하기 위함이라면 모두 transient 로 선언하자
만약 이러한 singleton 이 transient 가 아닌 참조 필드를 갖고 있다면 그 필드의 내용은 readResolve 가 실행되기 전에 역직렬화 된다. 그렇다면 앞선 MutualPeriod 와 같이 공격이 가능해지는데
스트림을 잘 조작하여 역직렬화 되는 시점에 그 역직렬화된 참조를 훔칠 수 있다.
더 상세한 설명을 위해 도둑(Stealer) 라는 클래스를 만든다. 이 클래스는 Elvis 의 직렬화된 singleton 을 참조하는 역할을 하고, 직렬화된 stream 에서 singleton 의 비휘발성 필드를 이 Stealer 의 instance로 교체한다.
public class Elvis implements Serializable { public static final Elvis INSTANCE = new Elvis(); private Elvis() {} private String[] favoriteSongs = {"Hound Dog", "Heartbreak Hotel"} public void printFavorite() { System.out.println(Arrays.toString(favoriteSongs)); } private Object readResolve() { return INSTANCE; } }
Java
잘못된 singleton transient 가 아닌 참조 필드를 갖고 있다.
public class ElvisStealer implements Serializable ( static Elvis impoersonator; private Elvis payload; private Object readResolve() { impersonator = payload; return new String[] {"A Fool Such as I"}; } private static final long serialVersionUID = 0; }
Java
도둑 클래스
StealerElvis 를 참조하고 있고, 역직렬화 될때 StealerreadResolve 가 먼저 호출된다.
Stealer 가 역직렬화 될 때 Elvis 의 역직렬화가 실행되는데 역직렬화 도중인(그리고 readResolve 가 수행되기 전) 싱글턴의 참조가 담겨있게 된다.
Stealer 는 이
public class ElvisImpersonator { // 진짜 Elvis 인스턴스로는 만들어질 수 없는 바이트 스트림 private static final byte[] serializedForm = { -84, -19, 0, 5, 115, 114, 0, 42, 109, 101, 46, 121, 101, 118, 103, 110, 101, 110, 108, 108, 46, 99, 111, 110, 99, 117, 114, 114, 101, 110, 116, 46, 115, 101, 114, 105, 97, 108, 105, 122, 97, 98, 108, 101, 46, 69, 108, 118, 105, 115, -42, -124, -118, 12, 32, 123, 72, 7, 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.printFavorite(); impersonator.printFavorite(); } static Object deserialize(byte[] sf) { try { return new ObjectInputStream(new ByteArrayInputStream(sf)).readObject(); } catch (IOException | ClassNotFoundException e) { throw new IllegalArgumentException(e); } } }
Java
직렬화의 허점을 이용해 싱글턴 객체를 2개 생성한다.
실제 결과
책에서는 이러한 결과가 나온다고 한다.
favoriteSongstransient 로 선언하면 문제를 고칠 수 있다고 한다.
이러한 문제는 직렬화가 가능한 클래스에 참조는 enum 을 선언하는 경우 딱 선언한 상수 이외에 다른 객체가 존재하지 않음을 java 가 보장해준다.
물론 공격자가 AccessibleObject.setAccessible 같은 privileged method 를 사용한다면 이야기가 달라진다. 다음은 Elvis 를 enum 을 사용한 예시이다.
public enum Elvis { INSTANCE; private String[] favoriteSongs = { "Hound Dog", "Heartbreak Hotel" }; public void printFavorites() { System.out.println(Arrays.toString(favoriteSongs)); } }
Java
readResolve 를 사용하는 방식이 완전히 쓸모없는 것은 아니다. 직렬화 가능 instance 를 작성해야 하는데 compile 타임에 어떤 instance 가 있는지 알 수 없는 상황이라면 enum 으로 표현하는 것은 불가능하다
readResolve 의 접근성은 매우 중요하다
final class 라면 readResolve 는 반드시 private 이어야 한다
private 으로 선언하면 하위 클래스에서 사용할 수 없으므로 package-private 으로 선언하여 같은 패키지에 속한 하위 클래스에서만 사용할 수 있게 한다.
protected 나 public 으로 선언하면 이를 재정의하지 않은 모든 하위 클래스에서 사용할 수 있다.
하위 클래스의 instance 를 역직렬화 하면 상위 클래스의 instance 를 생성하여 ClassCastException 을 일으킬 수 있다.
TOP