Item86.Serializable을 구현할지는 신중히 결정하라
어떤 클래스의 인스턴스를 직렬화 하려면 클래스 선언에 implements Serializable만 덧 붙이면 된다.
너무 쉽게 적용할 수 있기 때문에 직렬화를 지원하기란 짧게 보면 손쉬워 보이지만, 길게 보면 아주 값비싼 일이다.
Serializable의 문제점
Serializable을 구현하면 릴리스한 뒤에는 수정하기 어렵다.
클래스가 Serializable을 구현하면 직렬화된 바이트 스트림 인코딩(직렬화 형태)도 하나의 공개 API가 된다.
이 클래스가 널리 퍼진다면 그 직렬화 형태도 공개 API와 마찬가지로 영원히 지원해야 한다.
커스텀 직렬화 형태를 설계하지 않고 자바의 기본 방식을 사용하면 직렬화 형태는 클래스의 private과 package-private 인스턴스 필드마저 API로 공개되어 캡슐화가 깨지게 된다.
뒤늦게 클래스 내부 구현을 손보면 원래의 직렬화 형태와 달라지게 된다.
한쪽은 구버전 인스턴스를 직렬화하고 다른쪽은 신버전 클래스로 역직렬화 한다면 실패를 맛볼 것이다.
Serializable 인터페이스를 구현한 User 클래스
객체를 생성하고 직렬화 하여 파일로 내보낸다.
User 객체가 직렬화된 파일
직렬화 파일을 다시 역직렬화 하면 User 객체를 얻을 수 있다.
초기 릴리스 버전에서는 아무런 문제가 없으나 만약 User 객체에 필드값이 추가가 된다면
serialVersionUID 값이 달라져 InvalidClassException이 발생한다.
모든 직렬화된 클래스는 고유 식별 번호를 부여받는다. serialVersionUID라는 이름의 static final long 필드로 이 번호를 명시하지 않으면 시스템이 런타임에 암호 해시 함수(SHA-1)를 적용해 자동으로 클래스 안에 생성해 넣는다. 이 값을 생성하는데는 클래스 이름, 구현한 인터페이스들, 컴파일러가 자동으로 생성해 넣은 것을 포함한 대부분의 클래스 멤버들이 포함되는데 필드나 메서드가 추가되거나 수정된다면 UID 값도 변경되어 InvalidClassException이 발생한다.
UID를 추가하고 직렬화 파일로 내보낸 후 필드를 추가하고 역직렬화 하니 같은 객체로 인식하여 sex 값은 비워진 채로 역직렬화하여 객체로 생성되었다.
문제가 다 해결된 것은 아니다. 타입변환을 하게 된다면 역시 역직렬화가 실패하게 된다.
이처럼 직렬화 객체를 잘 설계하기란 여간 어려운게 아니다.
Serializable 구현은 버그와 보안 구멍이 생길 위험이 높아진다.
객체는 생성자를 사용해 만들어진다. 직렬화는 언어의 기본 메커니즘을 우회하는 객체 생성 기법이다.
기본 방식을 따르든 재정의해 사용하든 역직렬화는 일반 생성자의 문제가 그대로 적용되는 '숨은 생성자'다.
이 생성자는 전면에 드러나지 않으므로 "생성자에서 구축한 불변식을 모두 보장해야 하고 생성 도중 공격자가 객체 내부를 들여다 볼 수 없도록 해야 한다"는 정보은닉, 캡슐화를 떠올리기 쉽지 않다. 기본 역직렬화를 사용하면 불변식 깨짐과 허가되지 않은 접근에 쉽게 노출된다.
30살 미만의 User 객체만 생성하고 싶지만 역직렬화 하면 여지 없이 객체가 생성된다.
Serializable 구현은 클래스의 신버전을 릴리스할 때 테스트할 것이 늘어난다.
직렬화 가능 클래스가 수정되면 신버전 인스턴스를 직렬화한 후 구버전으로 역직렬화할 수 있는지, 그리고 그 반대도 가능한지 검사해야 한다. 따라서 테스트해야 할 양이 직렬화 가능 클래스의 수와 릴리스 횟수에 비례해 증가한다.
양방향 직렬화/역직렬화가 모두 성공하고, 원래의 객체를 충실히 복제해내는지 반드시 확인해야 한다.
Serializable 구현 여부는 가볍게 결정할 사안이 아니다.
객체를 전송하거나 저장할 때 자바 직렬화를 이용하는 프레임워크용으로 만든 클래스라면 선택의 여지가 없다.
하지만 Serializable 구현에 따르는 비용이 적지 않으니 클래스 설계시 이득과 비용을 잘 따져봐야 한다.
BigInteger나 Instant 같은 '값'클래스와 컬렉션 클래스들은 Serializable을 구현하고, 스레드 풀처럼 '동작'하는 객체를 표현하는 클래스는 대부분 Serializable을 구현하지 않았다.
상속용으로 설계된 클래스는 대부분 Serializable을 구현하면 안되며, 인터페이스도 대부분 Serializable을 확장해서는 안된다.
예외적으로 상속용으로 설계된 클래스 중 Serializable을 구현한 객체는 Throwable과 Component가 있다.
Throwable은 서버가 RMI를 통해 클라이언트로 예외를 보내기 위해 Serializable을 구현했다.
Component는 GUI를 전송하고 저장하고 복원하기 위해 Serializable을 구현했다.
프로그래머가 작성하는 클래스의 인스턴스 필드가 직렬화와 확장이 모두 가능하고 불변식을 보장해야 한다면 반드시 하위 클래스에서 finaliza 메서드를 재정의하지 못하게 해야한다.
즉, finalize 메서드를 자신이 재정의 하면서 final로 선언하면 된다. 그렇지 않으면 finalizer 공격을 당할 수 있다.
인스턴스 필드 중 기본값(정수형: 0, boolean: false, 참조형: null)으로 초기화되면 위배되는 불변식이 있다면 클래스에 readObjectNoData 메서드를 반드시 추가해야 한다.
private void readObjectNoDate() throws InvalidObjectException {
throw new InvalidObjectException("스트림 데이터가 필요합니다.");
}
이 메서드는 자바4에 추가된 것으로, 기존의 직렬화 가능 클래스에 직렬화 가능 상위 클래스를 축하는 드문 경우를 위한 메서드다.
Serializable을 구현하지 않기로 할 때는 상속용 클래스인데 직렬활르 지원하지 않으면 그 하위 클래스에서 직렬화를 지우너하려 할 때 부담이 늘어난다.
보통 이런 클래스를 역직렬화하려면 그 상위 클래스는 매개변수가 없는 기본 생성자를 제공해야 하는데 기본생성자가 없다면 하위 클래스에서 직렬화 프록시 패턴을 사용해야 한다.
내부 클래스는 직렬화를 구현하지 말아야 한다.
내부 클래스에는 바깥 인스턴스의 참조와 유효 범위 안의 지역변수 값들을 저장하기 위해 컴파일러가 생성한 필드들이 자동으로 추가된다.
익명 클래스와 지역 클래스의 이름을 짓는 규칙이 언어 명세에 나와있지 않듯, 이 필드들이 클래스 정의에 어떻게 추가되는지 정의되지 않았다.
내부 클래스에 대한 기본 직렬화 형태는 분명하지 않다.
단, 정적 멤버 클래스는 Serializable을 구현해도 된다.
핵심 정리
Serializable을 구현한다고 선언하기 아주 쉽지만 그것은 눈속임일 뿐이다. 한 클래스의 여러 버전이 상호작용할 일이 없고 서버가 신뢰할 수 없는 데이터에 노출될 가능성이 없는 등, 보호된 환경에서만 쓰일 클래스가 아니라면 Serializable 구현은 아주 신중하게 이뤄져야 한다. 상속할 수 있는 클래스라면 주의사항이 더욱 많아진다.
https://codingwell.tistory.com/170