아이템 74. Serializable 인터페이스를 구현할 때는 신중하라
74. Serializable 인터페이스를 구현할 때는 신중하라
Serializable 구현시 생각해야 하는 것들
- Serializable 구현과 관련된 가장 큰 문제는 일단 클래스를 릴리스하고 나면 클래스 구현을 유연하게 바꾸기 어려워 진다.
- 모든 직렬화 가능 클래스에는 고유한 식별 번호(serialVersionUID)가 붙는다. 자동 생성되는 이 식별 번호(serialVersionUID는 클래스 이름, 해당 클래스가 구현하는 모든 인터페이스의 이름, 그리고 해당 클래스의 모든 public, protected 멤버에 영향을 받아 생성된다.
- 버그나 보안 취약점(security hole)이 발생할 가능성이 높아진다.
- 직렬화는 언어 외적인(extralinguistic) 객체 생성 메커니즘이다.
- 역직렬화는 생성자와 동일한 이슈를 갖고 있는 “숨은 생성자(hidden constructor)”이다.(생성자가 만족하는 모든 불변식을 보장해야 함)
- 기본 역직렬화 메커니즘을 그대로 사용할 경우, 객체는 불변식 훼손(invariant corruption)이나 불범 접근(illegal access) 문제에 쉽게 노출됨
- 새 버전 클래스를 내놓기 위한 테스트부담이 늘어난다.
- 새 릴리즈에서 만들고 직렬화한 객체를 예전 릴리스에서 역직렬화할 수 있는지, 그리고 그 역도 가능한지 검사하는 것이 중요함
- 직렬화-역직렬화가 정상적인지 확인해야 하고, 그 결과로 만들어지는 객체가 원래 객체이 충실한 사본(faithful copy)인지도 확인해야 함
- 이진 호환성(binary compatibility)와 의미 호환성(semantic compatibility)까지 테스트 해야 하기 때문에, 자동 생성된 테스트가 불가능함
- 계승을 염두에 두고 설계하는 클래스는
Serializable
을 구현하지 않는 것이 바람직하다. 또한, 인터페이스는 가급적Serializable
을 계승하지 말아야 한다.- 하지만, 프레임워크 상에서 클래스나 인터페이스가
Serializable
을 구현하거나 계승하도록 해야 하는 경우도 있다.- 계승을 고려해
Serializable
이 적용되어 설게된 클래스 :Throwable
,Component
,HttpServlet
- 계승을 고려해
- 하지만, 프레임워크 상에서 클래스나 인터페이스가
객체 필드 직렬화시 유의 사항
객체 필드를 갖는 클래스를 직렬화 가능하고 계승 가능한 클래스로 구현할 때는 반드시 조심해야 할 것이 하나 있다. 객체 필드가 기본값으로 초기화되면 위배되는 불변식이 있는 경우 (정수 : 0, boolean : false, 일반 객체 : null등과 같이 default 초기화가 일어나면 안되는 경우)에는 아래의 readObjectNoData 메서드를 클래스에 반드시 추가해야 한다.
// 상태유지 계승 가능 직렬화 가능
// 클래스에 대한 readObjectNoData 메서드
private void readObjectNoData() throws InvalidObjectException {
throw new InvalidObjectException("Stream data requied");
}
계승을 고려해 설계한 직렬화 불가능 클래스에는 무인자 생성자를 제공하는 것이 어떨지 따져봐야 한다.
// 직렬화가 불가능한 상태유지(stateful) 클래스.
// 하지만 직렬화가 가능한 하위 클래스를 만들 수 있다.
public abstract class AbstractFoo{
private int x, y; // 상태
// 아래 enum과 필드는 초기화 과정을 추적하기 위한 것이다.
private enum State { NEW, INITIALIZING, INITIALIZED };
/**
* 가령 어떤 스레드가 객체에 initialize를 호출하는 순간에 두 번째 스레드가
* 그 객체를 사용하려 한다고 해 보자. 그 두 번째 스레드는 상태가 깨진 객체를 이용하게 될 수 있다.
* compareAndSet을 사용해 enum에 대한 참조를 원자적으로 조작하는 이 패턴은,
* 다목적(general-purpose) 스레드 안전 상태 기계(thread safe state machine)를 구현하기 좋다.
*/
private final AtomicReference<State> init =
new AtomicReference<State>(State.NEW);
public AbstractFoo(int x, int y) { initialize(x, y); }
// 이 생성자와 그 아래 메서드는 하위 클래스의 readObject 메서드가
// 상태를 초기화할 수 있도록 하기 위한 것이다.
protected AbstractFoo() { }
protected final void initialize(int x, int y){
if (!init.compareAndSet(State.NEW, State.INITIALIZING))
throw new IllegalStateException("Already initialized");
this.x = x;
this.y = y;
...//원래 생성자가 하던 나머지 작업
init.set(State.INITIALIZED);
}
// 이 메서드들은 하위 클래스의 writeObject 메서드에 의해 객체가
// 수동적으로 직렬화될 수 있도록 내부 상태 정보를 제공하는 역할을 한다.
protected final int getX() { checkInit(); return x; }
protected final int getY() { checkInit(); return y; }
// 모든 public 및 protected 객체 메서드에서 반드시 호출해야 하는 메서드
private void checkInit(){
if(init.get() != State.INITIALIZED)
throw new IllegalStateException("Uninitialized");
}
... // 이하 생략
}
다목적(general-purpose) 스레드 안전 상태 기계(thread safe state machine)를 구현하면, 직렬화 가능 하위 클래스를 구현하기가 쉽다.
// 직렬화 불가능 상태유지(stateful) 클래스의 직렬화 가능 하위 클래스
public class Foo extends AbstractFoo implements Serializable{
private void readObject(ObjectInputStream s)
throw IOException, ClassNotFountException{
s.defaultReadObject();
// 상위 클래스 상태를 수동으로 역직렬화 한 다음 초기화
int x = s.readInt();
int y = s.readInt();
initialize(x, y);
}
private void writeObject(ObjectOutputStream s) throws IOException {
s.defaultWriteObject();
// 상위 클래스 상태를 수동으로 직렬화
s.writeInt(getX());
s.writeInt(getY());
}
// 생성자는 이 메커니즘과 상관없음
public Foo(int x, int y) { super(x, y); }
private static final long serialVersionUID = 2213124123213123L;
}
내부 클래스(inner class)는 Serializable을 구현하면 안된다.
내부 클래스에는 바깥 객체에 대한 참조를 보관하고 바깥 유효범위의 지역 변수 값을 보관하기 위해 컴파일러가 자동으로 생성하는 인위생성 필드(synthetic field)가 있다. 익명 클래스나 지역 클래스 이름과 마찬가지로, 자바 명세서에는 이런 필드가 클래스 정의에 어떻게 들어맞는지 나와 있지 않다. 따라서 내부 클래스의 직렬화 형식은 정의될 수 없다. 하지만 정적 멤버 클래스는 Serializable을 구현해도 된다.
결론
- 잠깐 쓰고 버릴 클래스가 아니라면,
Serializable
을 구현한 다는 것은 심각하게 받아 들여야 하는 약속 생각하고 신중하게 설계해야 한다. - 계승을 고려하여 설계하는 클래스는 더욱 더 조심해서 설계하자.
- 하위 클래스에서
Serializable
구현을 금하고 싶지 않다면, 무인자 생성자를 제공하는 것을 고려하자.그래야 하위 클래스에서Serializable
을 구현할지 말지를 선택할 수 있다.