싱글톤과 직렬화
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() {
return INSTANCE;
}
- 한편 여기서 살펴본
Elvis
인스턴스의 직렬화 형태는 아무런 실 데이터를 가질 필요가 없으니 모든 인스턴스 필드는 transient
로 선언해야 한다.
- 그러니까
readResolve
메서드를 인스턴스의 통제 목적으로 이용한다면 모든 필드는 transient
로 선언해야 한다.
- 만일 그렇지 않으면 역직렬화(Deserialization) 과정에서 역직렬화된 인스턴스를 가져올 수 있다. 즉, 싱글턴이 깨지게 된다.
Transient
- 객체의 필드 중에 직렬화 하지 않을 것들을 지정하기 위해 사용한다.
- 비밀번호 암호등이 예시가 될 수 있다.(민감한 정보)
보안
- readResolve를 인스턴스 통제 목적으로 사용한다면 객체 참조 타입 인스턴스 필드는 모두 transient로 선언하자.
- 이렇게 하지 않으면 역직렬화된 객체 참조를 공격할 여지가 남는다.
- readResolve 메서드와 인스턴스 필드 하나를 포함한 도둑 클래스를 만든다.
- 도둑 클래스의 인스턴스 필드는 직렬화된 싱글턴을 참조하는 역할을 한다.
- 직렬화된 스트림에서 싱글턴의 비휘발성 필드를 도둑의 인스턴스 필드로 교체한다.
- 싱글턴이 도둑을 포함하므로 역직렬화시 도둑 클래스의 readResolve가 먼저 호출된다.
- 도둑 클래스의 인스턴스 필드에는 역직렬화 도중의 싱글턴의 참조가 담겨있게 된다.
- 도둑 클래스의 readResolve 메서드는 인스턴스 필드가 참조한 값을 정적 필드로 복사한다.
- 싱글턴은 도둑이 숨긴 transient가 아닌 필드의 원래 타입에 맞는 값을 반환한다.
- 이 과정을 생략하면 직렬화 시스템이 도둑의 참조를 이 필드에 저장하려 할 때 ClassCastException 이 발생한다.
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() {
impersonator = payload;
return new String[] {"There is no cow level"};
}
}
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) {
Elvis elvis = (Elvis) deserialize(serializedForm);
Elvis impersonator = ElvisStealer.impersonator;
elvis.printFavorites();
impersonator.printFavorites();
}
}
Enum 사용
enum
을 사용하면 모든 것이 해결된다.
- 자바가 선언한 상수 외에 다른 객체가 없음을 보장해주기 때문이다.
AccessibleObject.setAccessible
메서드와 같은 리플렉션을 사용했을 때는 예외다.
public enum Elvis {
INSTANCE;
...필요한 데이터들
}
- 인스턴스 통제를 위해
readResolve
메서드를 사용하는 것이 중요할 때도 있다.
- 직렬화 가능 인스턴스 통제 클래스를 작성해야 할 때, 컴파일 타임에는 어떤 인스턴스들이 있는지 모를 수 있는 상황이라면 열거 타입으로 표현하는 것이 불가능하기 때문에
readResolve
메서드를 사용할 수 밖에 없다.
readResolve 메서드의 접근성
final
클래스라면 readResolve
메서드는 private
이어야 한다.
final
이 아닌 경우는 주의해야한다.
private
선언시 하위 클래스에서 사용할 수 없다.
package-private
으로 선언시 같은 패키지에 속한 하위 클래스에서만 사용할 수 있다.
protected
, public
은 재정의하지 않은 모든 하위 클래스에서 사용할 수 있다.
protected
, public
이면서 하위클래스에서 재정의 하지 않으면, 하위 클래스의 인스턴스를 역직렬화하면 상위 클래스의 인스턴스를 생성하여 ClassCastException
을 일으킬 수 있다.