들어가며
enum이 아닌 싱글톤을 사용하게 될 때 이 싱글톤을 직렬화 하게 되면 인스턴스가 싱글톤이 아니게 되는 문제가 있다. 다음 싱글톤 클래스의 직렬화 코드를 보자.
1 | private Object readResolve() { |
위 코드는 개선의 여지가 있다. 그리고 readResolve를 인스턴스 통제 목적으로 사용한다면 객체 참조 타입 인스턴스 필드는 모두 transient로 선언해야 한다. 다음은 위 코드에 대한 공격 방법이다. 싱글턴이 non-transient 참조 필드를 가지고 있다면 그 필드의 내용은 readResolve 메소드가 실행되기 전에 역직렬화 된다. 그렇다면 잘 조작된 스트림을 써서 해당 참조 필드의 내용이 역직렬화되는 시점에 그 역직렬화된 인스턴스의 참조를 훔쳐올 수 있다.
가령 예를들어 도둑클래스 안에 인스턴스 필드 하나와 readResolve 메소드가 있다고 가정하자. 이 인스턴스 필드는 도둑이 숨길 직렬화된 싱글턴을 참조하는 역할을 한다. 직렬화된 스트림에서 싱글턴의 비휘발성 필드를 이 도둑의 인스턴스로 교체한다. 이제 싱글턴과 도둑은 상호 참조를 하게 된다. 싱글톤이 도둑을 포함하므로 싱글톤이 역직렬화될 때 도둑의 readResolve 메소드가 먼저 호출된다. 그 결과 도둑의 readResolve 메소드가 수행될 때 도둑의 인스턴스 필드에는 역직렬화 도중이면서 readResolve가 수행되기 전인 싱글턴의 참조가 담겨있게 된다. 도둑의 readResolve 메소드는 이 인스턴스 필드가 참조한 값을 정적 필드로 복사해 readResolve가 끝난 후에도 참조할 수 있도록 한다. 그 뒤엔 도둑이 숨긴 transient가 아닌 필드의 원래 타입에 맞는 값을 반환한다.이 과정을 생략하면 VM이 ClassCastException을 던진다.
이런 문제는 비휘발성 필드를 transient로 선언하면 고칠 수 있지만 열거타입 싱글톤을 사용하는 편이 더 낫다. 직렬화 가능한 인스턴스 통제 클래스를 열거 타입을 이용해 구현하면 선언한 상수 외의 다른 객체는 존재하지 않음을 자바가 보장해준다. 단, 공격자가 privileged 메소드를 악용한다면 모든 방어가 무력화된다.
인스턴스 통제를 위해 readResolve를 사용하는 방식이 완전히 쓸모없는 것은 아니다. 직렬화 가능 인스턴스 통제 클래스를 작성해야 하는데 컴파일 타임에는 어떤 인스턴스들이 있는지 알 수 없는 상황이라면 열거 타입으로 표현하는것이 불가능하기 때문이다.
readResolve 메소드의 접근성은 매우 중요하다. final 클래스라면 readResolve는 private여야 한다. final이 아닌 클래스에서는 다음을 고려하자. private로 선언하면 하위 클래스에서 사용할 수 없다. package-private로 선언하면 같은 패키지에 속한 하위 클래스에서만 사용할 수 있다. protected나 public으로 선언하면 이를 재정의하지 않은 모든 하위 클래스에서 사용할 수 있다. protected나 public이면서 하위 클래스에서 재정의하지 않았는데 하위 클래스의 인스턴스를 역직렬화 하면 상위 클래스의 readResolve 메소드가 수행되어 상위 클래스의 인스턴스를 생성하므로 ClassCastException을 던지게 된다.