자바에 대해 무언가 궁금한 것이 있다면 역시 Effective Java 를 보는 것이 확실한것 같다. 이 item 은 정말 Serializable 을 구현해야 하는지, 무엇이 필요한지, 주의할 것은 무엇인지를 간단히 살펴본다.
직렬화: 데이터를 Stream 으로 전송할 수 있는 상태로 만든다 (ex. 파일)
역직렬화: Stream 으로 전송 받은 데이터를 객체로 만든다
•
어떤 클래스에 직렬화를 구현하고자 하면 Serializable 을 implements Serializable 만 붙이면된다.
◦
매우 간단해 보이지만 그렇지 않다.
◦
짧게 보면 쉽게 만들 수 있지만, 길게 본다면 값비싼 일이다.
•
Serializable 을 구현하면 릴리즈 이후에 수정이 어렵다(여기서 릴리즈는 서버 릴리즈가 아님)
HashMap java 11
HashMap java 1.8
◦
Serializable 을 구현하면 byte stream encoding 도 하나의 공개 API 가 된다.
▪
private, package-private 인스턴스 필드들도 API 로 공개하는 꼴이 된다. (캡슐화 깨짐)
◦
그래서 영원히 관리해야 하는 대상이 된다.
◦
시간이 지나 내부 구현을 수정하게 되면 원래의 직렬화 형태와 달라지게 된다.
▪
만약 한 쪽은 구버전 instance 를 직렬화 하고, 한쪽은 신버젼 클래스로 역직렬화 하는 일이 발생된다면..?
•
직렬화를 구현하고자 한다면 감당할 수 있을 만큼의 고품질의 직렬화 형태로 설계해야 한다.
◦
고생이 크겠지만, 보상은 달콤하다
직렬화가 클래스 개선을 방해하는 예시들
직렬 버젼 UID(serial Version ID)
•
모든 직렬화된 클래스는 serialVersionUID 라는 이름의 static final long 필드의 식별 번호를 부여받는다.
•
이 번호를 명시하지 않으면 runtime 에서 sha-1 을 적용하여 자동으로 클래스에 집어넣는다.
◦
클래스 이름, 구현한 interface, 컴파일러가 자동 생성하여 넣는 것을 포함하며 대부분의 class member 가 대상이다.
•
그래서 나중에 method 를 추가하여 이들중 하나라도 수정되면 UID 도 깨지게 되므로 자동생성보단 직접 넣는것이 좋다.
버그와 보안 구멍이 생길 위험이 높아진다
•
직렬화는 language 의 매커니즘을 우회하는 객체 생성 기법이다.
•
역직렬화는 일반 생성자의 문제가 그대로 적용되는 '숨은 생성자' 이다.
•
이 역직렬화 생성자는 전면에 드러나지 않기 때문에 생성자에 적용되는 규칙을 적용하겠다고 생각하기 어렵게 된다.
◦
생성자에서 구축한 불변식을 모두 보장해야 한다
◦
생성 도중 공격자가 객체 내부를 들여다볼 수 없도록 해야 한다.
•
역직렬화를 이용하면 불변식은 깨지고, 허가되지 않은 접근에 노출된다.
클래스의 신버전을 일리즈 할때 테스트할 것이 늘어난다.
•
신버젼 instance 를 직렬화 하고, 구버젼으로 역직렬화가 되는지 (하위 호환성) 검사가 필요하다
•
이 반대도 가능한지 테스트해야 한다.
•
처음 설계시 이러한 부분까지 고민하지 않는다면 테스트가 부담스러워 진다.
Serializable 구현 여부는 가볍게 결정할 사안이 아니다.
단, 객체를 전송하거나, 직렬화를 이용한 framework 에서는 선택의 여지가 없다.
Serializable 을 구현에 따른 비용이 적지 않으니 그 이득과 비용을 잘 저울질 해야한다
•
BigInteger, Instant 같은 'value' class 와 collection 은 Serializable 을 구현하고
public class ReentrantLock implements Lock, java.io.Serializable {
public class BigInteger extends Number implements Comparable<BigInteger> {
Java
복사
•
thread pool 과 같이 '동작' 하는 class는 serializable 을 구현하지 않는다
public final class DateTimeFormatter {
Java
복사
상속용으로 설계된 클래스는 Serialzable 을 구현해선 안되며, 인터페이스도 대부분 Serializable 을 확장해선 안된다.
예외 상황은 Serializable 을 구현한 클래스만 지원하는 framework 를 사용할 때 이다.
상속용으로 설계된 클래스의 예시는 다음과 같다
public class Throwable implements java.io.Serializable {
Java
복사
Throwable 은 서버가 RMI 를 통해 예외를 보내기 위해 필요하다
public abstract class Component implements ImageObserver, MenuContainer,
Serializable
{
Java
복사
Component 는 GUI 를 전송, 저장, 복원을 위해 사용하는데 swing, AWT 가 많이 사용될 때에도 이런 용도로 사용하진 않았다.
invariant (불변식) 은 재정의 하지 못하게 해야 한다.
class Rational
{
public:
Rational(int n = 0, int d = 1);
~Rational();
public:
void setDenominator(int d)
{
d == 0 ? error_msg("zero divide"): denominator = d;
}
private:
int numerator, denominator;
};
Java
복사
불변식 예시 0으로 나눌 수 없다.
직렬화에 포함되선 안되는 것이 있다면
•
readObjectNoData 를 반드시 추가하라
•
instance field 중 기본값(int → 0, boolean → false, 객체 참조 null)으로 초기화 되었을때 이것이 invariant 인 경우 구현이 필요하다
private void readObjectNoData() throws InvalidObjeectException {
throw new InvalidObjectException("스트림 데이터가 필요합니다");
}
Java
복사
Serializable 을 구현하지 않는다면
•
상속 용도의 class 인데 직렬화를 지원하지 않으면 하위 클래스에서 직렬화를 지원할 때 부담이 늘어난다
•
이러한 경우 상위 클래스는 매개변수가 없는 생성자를 제공해야 한다.
•
이런 생성자를 지원하지 않을 경우 직렬화 proxy pattern 을 사용해야 한다(item90)
inner class 는 직렬화를 구현하지 말아야 한다.
보통 inner 클래스는 바깥 instance 의 참조와 유효 범위 안의 지역변수 값들을 저장하기 위해 compiler 가 생성한 필드들이 자동으로 추가된다.
이것 때문에 직렬화에 대한 앞서 언급한 문제들이 발생할 수 있다.
단, static member class 들은 Serializable 을 구현해도 된다.