item3의 싱글턴 패턴을 보자!
클래스는 바깥에서 생성자를 호출하지 못하게 막는 방식으로 인스턴스가 오직 하나만 만들어짐을 보장한다.
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public void leavingTheBuilding() {...}
}
여기서 class 선언부에 implements Serializable을 추가하는 순간 이 클래스는 무슨 짓을 해도 싱글턴이 아니다.
어떤 readObject를 사용하든, 이 클래스가 초기화될 때 만들어진 인스턴스와는 별개인 인스턴스를 반환하게 된다.
이 때 readResolve 기능을 이용하면 readObject가 만들어낸 인스턴스를 다른 것으로 대체할 수 있다.
그러나, readResolve를 인스턴스 통제 목적으로 사용한다면 객체 참조 타입 인스턴스 필드를 모두 transient로 선언해야 한다.
만약 이를 지키지 않을 경우, readResolve 메서드가 수행되기 전 역직렬화된 객체의 참조를 공격할 여지가 남는다.
그렇다면, 어떻게 해야 할까?
'열거 타입'을 사용하면 된다.
직렬화 가능한 인스턴스 통제 클래스를 열거 타입을 사용해 구현하면, 선언한 상수 외의 다른 객체는 존재하지 않음을 자바가 보장해준다.
물론, 공격자가 AccessibleObject.setAccessible같은 특권 메서드를 악용한다면 이야기가 달라진다. (모든 방어가 무력화된다)
언제 readResolve를 사용해야 할까?
- 직렬화 가능한 인스턴스 통제 클래스를 작성해야 하는데, 컴파일 타임에는 어떤 인스턴스들이 있는지 알 수 없는 상황
(열거 타입 표현 불가능하기 때문)
readResolve 메서드의 접근성
- final class - private
- non-final class - private(하위 클래스에서 사용 불가능), package-private(같은 패키지에 속한 하위 클래스에서만 사용 가능), protected/public(재정의하지 않은 모든 하위 클래스에서 사용 가능)
** 이 때 protected/public 에서 하위 클래스에서 재정의하지 않은 경우,
하위 클래스의 인스턴스를 역직렬화하면 상위 클래스의 인스턴스를 생성하여 ClassCastException을 일으킬 수 있음을 주의하자. **
핵심 정리
불변식을 지키기 위해 인스턴스를 통제해야 한다면 가능한 한 열거 타입을 사용하자.
여의치 않은 상황에서 직렬화와 인스턴스 통제가 모두 필요하다면 readResolve 메서드를 작성해 넣어야 하고,
그 클래스에서 모든 참조 타입 인스턴스 필드를 transient로 선언해야 한다.
'JAVA > Effective Java' 카테고리의 다른 글
Effective Java (7) | 2022.09.15 |
---|---|
item 90. 직렬화된 인스턴스 대신 직렬화 프록시 사용을 검토하라 (0) | 2022.09.15 |
item 88. readObject 메서드는 방어적으로 작성하라 (0) | 2022.09.13 |
item 87. 커스텀 직렬화 형태를 고려해보라 (0) | 2022.09.13 |
item 82. 스레드 안정성 수준을 문서화하라 (0) | 2022.09.01 |
댓글