아이템 86. Serializable을 구현할지는 신중히 결정하라. - ksw6169/effective-java GitHub Wiki
Serializable은 신중하게 사용해야 한다.
어떤 클래스의 인스턴스를 직렬화할 수 있게 하려면 클래스 선언에 Serializable
을 붙여주면 된다. 이렇듯 직렬화를 지원하기란 손쉬워 보이지만, 길게 보면 아주 값비싼 일이다.
Serializable을 구현했을 때의 단점
1. 클래스가 Serializable을 구현하면 릴리즈한 뒤에는 수정하기 어렵다.
- 클래스가
Serializable
을 구현하면 직렬화된 바이트 스트림 인코딩(직렬화 형태)도 하나의 공개 API가 된다. 그래서 이 클래스가 널리 퍼진다면 그 직렬화 형태도 다른 공개 API와 마찬가지로 영원히 지원해야 한다. - 커스텀 직렬화 형태를 설계하지 않고 자바의 기본 방식을 사용하면 직렬화 형태는 적용 당시 클래스의 내부 구현 방식에 영원히 묶여 버린다.
- 달리 말하면, 기본 직렬화 형태에서는 클래스의 private과 package-private 인스턴스 필드들마저 API로 공개하는 꼴이 된다. (즉, 캡슐화가 깨진다.)
- 뒤늦게 클래스 내부 구현을 손보면 원래의 직렬화 형태와 달라지게 된다. 예를 들어 한 쪽은 구버전 인스턴스를 직렬화하고 다른 쪽은 신버전 클래스로 역직렬화한다면 실패를 맛볼 것이다.
- 그러므로 직렬화 가능 클래스를 만들고자 한다면 길게 보고 감당할 수 있을 만큼 고품질의 직렬화 형태도 주의해서 함께 설계해야 한다.
- 직렬화 형태를 잘 설계하더라도 클래스를 개선하는 데 제약이 될 수 있다. (ex. serialVersionUID)
serialVersionUID
모든 직렬화된 클래스는 고유 식별 번호를 부여받는다.
serialVersionUID
라는 이름의 static final long 필드로, 이 번호를 명시하지 않으면 시스템이 런타임에 암호 해시 함수(SHA-1)를 적용해 자동으로 클래스 안에 생성해 넣는다. 이 값을 생성하는 데는 클래스 이름, 구현한 인터페이스들, 컴파일러가 자동으로 생성해 넣은 것을 포함한 대부분의 클래스 멤버들이 고려된다.그래서 나중에 편의 메소드를 추가하는 식으로 이들 중 하나라도 수정한다면 직렬 버전 UID 값도 변경된다. 따라서 자동 생성되는 값에 의존하면 쉽게 호환성이 깨져버려 런타임에
InvalidClassException
이 발생할 것이다.
2. 버그와 보안 구멍이 생길 위험이 높아진다.
- 역직렬화는 일반 생성자의 문제가 그대로 적용되는 ‘숨은 생성자’다.
- 이 생성자는 전면에 드러나지 않으므로 생성자에서 구축한 불변식을 모두 보장해야 하고 생성 도중 공격자가 객체 내부를 들여다 볼 수 없도록 해야 한다는 사실을 떠올리기 어렵다.
- 따라서 기본 역직렬화를 사용하면 불변식 깨짐과 허가되지 않은 접근에 쉽게 노출된다.
3. 해당 클래스의 신버전을 릴리즈할 때 테스트할 것이 늘어난다.
- 직렬화 가능 클래스가 수정되면 신버전 인스턴스를 직렬화한 후 구버전으로 역직렬화할 수 있는지, 그리고 그 반대도 가능한지를 검사해야 한다.
- 따라서 테스트해야 할 양이 직렬화 가능 클래스의 수와 릴리즈 횟수에 비례해 증가한다.
- 양방향 직렬화/역직렬화가 모두 성공하고, 원래의 객체를 충실히 복제해내는지를 반드시 확인해야 한다.
- 클래스를 처음 제작할 때 커스텀 직렬화 형태를 잘 설계해놨다면 이러한 테스트 부담을 줄일 수 있다.
Serializable 구현을 하면 안되는 경우
상속용으로 설계된 클래스나 인터페이스
- 상속용으로 설계된 클래스는 대부분
Serializable
을 구현하면 안 되며, 인터페이스도 대부분Serializable
을 확장해서는 안된다. - 이 규칙을 따르지 않으면 그런 클래스를 확장하거나 인터페이스를 구현하는 이에게 커다란 부담을 지우게 된다.
클래스의 인스턴스 필드가 직렬화와 확장이 모두 가능하다면 주의할 점
1. finalize 메소드를 재정의하지 못하게 하라.
- 인스턴스 필드 값 중 불변식을 보장해야 할 게 있다면 반드시 하위 클래스에서
finalize
메소드를 재정의하지 못하게 해야 한다. - 즉,
finalize
메소드를 자신이 재정의하면서 final로 선언하면 된다. 이렇게 해두지 않으면 finalizer 공격을 당할 수 있다.
2. 인스턴스 필드 중 기본값(정수형 0, boolean false, 객체 참조 타입은 null)으로 초기화되면 위배되는 불변식이 있다면 클래스에 다음의 readObjectNoData 메소드를 반드시 추가해야 한다.
- 다음은 상태가 있고, 확장 가능하고, 직렬화 가능한 클래스용
readObjectNoData
메소드다.
private void readObjectNoData() throws InvalidObjectException {
throw new InvalidObjectException("스트림 데이터가 필요합니다.");
}
- 이 메소드는 자바 4에 추가된 것으로 기존의 직렬화 가능 클래스에 직렬화 가능 상위 클래스를 추가하는 드문 경우를 위한 메소드다.
기타
내부 클래스는 직렬화를 구현하지 말아야 한다.
- 내부 클래스에는 바깥 인스턴스의 참조와 유효 범위 안의 지역변수 값들을 저장하기 위해 컴파일러가 생성한 필드들이 자동으로 추가된다.
- 이 필드들이 클래스 정의에 어떻게 추가되는지는 언어 명세에 정의되지 않았다. 즉, 내부 클래스에 대한 기본 직렬화 형태는 분명하지가 않으므로 내부 클래스는 직렬화를 구현하지 말아야 한다.
- 단, 정적 멤버 클래스는
Serializable
을 구현해도 된다.
핵심 정리
Serializable
은 구현한다고 선언하기는 아주 쉽지만, 눈속임일 뿐이다.- 한 클래스의 여러 버전이 상호작용할 일이 없고, 서버가 신뢰할 수 없는 데이터에 노출될 가능성이 없는 등, 보호된 환경에서만 쓰일 클래스가 아니라면 Serializable 구현은 아주 신중하게 이뤄져야 한다.
- 상속할 수 있는 클래스라면 주의사항이 더욱 많아진다.
참고 자료
- Effective Java 3/E