DEV ℧ Developer Diary

[EffectiveJava] item86 - Serializable을 구현할지는 신중히 결정하라

어떤 클래스의 인스턴스를 직렬화할 수 있게 하려면 클래스 선언에 implements Serializable만 덧붙이면 된다.

직렬화를 지원하기란 짧게 보면 손쉬워 보이지만, 길게보면 아주 값비싼 일이다.

Serializable

Serializable을 구현하면 릴리스한 뒤에는 수정하기 어렵다.
Serializable을 구현하면 직렬화된 바이트 스트림 인코딩도 하나의 공개 API가 된다. 그래서 이 클래스가 널리 퍼진다면 그 직렬화 형태도 영원히 지원해야 한다.

또 기본 직렬화 형태에서는 클래스의 private와 package-private 인스턴스 필드들 마저 API로 공개되는 꼴이 된다.(캡슐화가 깨진다.)

뒤늦게 한쪽은 구버전 인스턴스를 직렬화하고 다른 쪽은 신버전 클래스로 역직렬화한다면 실패를 맛보게된다.

Serializable 구현의 문제점

직렬화의 클래스 개선 방해

직렬화가 클래스 개선을 방해하는 간단한 예를 보자.

직렬 버전 UID(serial version UID)를 들 수 있다. 모든 직렬화된 클래스는 고유 식별 번호(static final long serialVersionUID)를 부여 받는다.
나중에 편의 메서드를 추가하는 식으로 클래스이름, 인터페이스 등등을 하나라도 수정한다면 직렬 버전 UID 값도 변해서 호환성이 깨져 InvalidClassException이 발생할 것이다.

버그와 보안 구멍의 위험

Serializable 구현은 버그와 보안 구멍이 생길 위험이 높아진다.

객체는 생성자를 사용해 만드는게 기본이다. 직렬화는 언어의 기본 메커니즘을 우회하는 객체 생성 기법으로, 역직렬화는 일반 생성자의 문제가 그대로 적용되는 ‘숨은 생성자’다.

기본 역직렬화를 사용하면 불변식 깨짐과 허가되지 않은 접근에 쉽게 노출되게 된다.

클래스의 신버전 릴리스때 테스트의 증가

Serializable 구현은 해당 클래스의 신버전을 릴리스할 때 테스트할 것이 늘어난다는 점이다.

클래스가 수정되면 신버전 인스턴스를 직렬화한 후 구버전으로 역직렬화할 수 있는지, 반대가 가능한지 검사해야한다.
따라서 테스트해야 할 양이 직렬화 가능 클래스의 수와 릴리스 횟수에 비례해 증가한다.

클래스를 처음 제작할 때 커스텀 직렬화 형태를 잘 설계해야 이러한 부담을 줄일 수 있다.

Serializable 구현여부

Serializable 구현 여부는 가볍게 결정할 사안이 아니다. 단, 객체를 전송하거나 저장할 때 자바 직렬화를 이용하는 프레임워크용으로 만든 클래스라면 대안이 없다.

하지만 Serializable구현에 따르는 비용이 적지 않으니, 클래스를 설계할 때마다 그 이득과 비용을 잘 저울질 해야한다.

스레드 풀처럼 ‘동작’하는 객체를 표현하는 클래스들은 대부분 Serializable을 구현하지 않았다.

상속용으로 설계된 클래스

상속용으로 설계된 클래스는 대부분 Serializable을 구현하면 안 되며, 인터페이스 대부분 Serializable을 확장해서는 안된다.

이 규칙을 따르지 않으면, 그런 클래스를 확장하거나 그런 인터페이스를 구현하는 이에게 커다람 부담이 된다.

주의할 점

작성하는 클래스의 인스턴스 필드가 직렬화와 확장이 모두 가능할때 주의할 점이 몇가지 있다.

finalize

인스턴스 필드 값 중 불변식을 보장해야 할 게 있다면 반드시 하위 클래스에서 finalize 메서드를 재정의하지 못하게 해야 한다.

finalize 메서드를 자신이 재정의하면서 final로 선언하면된다. 이렇게 하지 않으면 finalizer 공격을 당할 수 있다.

readObjectNoData

인스턴스 필드 중 기본값으로 초기화되면 위배되는 불변식이 있다면 클래스에 다음의 readObjectNoData 메서드를 반드시 추가해야 한다.

상태가 있고, 확장 가능하고, 직렬화 가능한 클래스용 readObjectNoData 메서드

private void readObjectNoData() throws InvalidObjetException {
    throw new InvalidObjectException("스트림 데이터가 필요합니다.");
}

Serializable을 구현하지 않을시.

Serializable을 구현하지 않기로 할때는 상속용 클래스인데 직렬화를 지원하지 않으면 그 하위 클래스에서 직렬화를 지원하려 할 때 부담이 늘어날 수 있으니, 직렬화 프록시 패턴 아이템을 사용해야 한다.

내부 클래스의 직렬화

내부 클래스는 직렬화를 구현하지 말아야 한다. 내부 클래스에는 바깥 인스턴스의 참조와 유효 범위 안의 지역변수 값들을 저장하기 위한 컴파일러가 생성한 필드들이 자동으로 추가된다.

내부 클래스에 대한 기본 직렬화 형태는 분명하지 않다. 단 정적 멤버 클래스는 Serializable을 구현해도 된다.