Serializable을 구현할지는 신중히 결정하라.Serializable을 구현하면 릴리즈한 뒤에는 수정하기 어렵다.직렬화가 클래스 개선을 방해하는 예시스트림 고유 식별자 즉 직렬 버전 UID(Serial Version ID)버그와 보안 구멍이 생길 위험이 높아진다.클래스의 신버전을 릴리즈 할 때 테스트할 것이 늘어난다.직렬화 구현 여부는 가볍게 결정할 사안이 아니다.상속용으로 설계된 클래스는 직렬화를 구현해서는 안되며, 인터페이스도 대부분 직렬화를 확장해서는 안된다.내부 클래스는 직렬화를 구현하지 말아야 한다결론
Serializable을 구현할지는 신중히 결정하라.
클래스를 직렬화하려면Serializable
를 impliments만 하면 되기 때문에 간단하고 신경쓸 부분이 없다고 생각하겠지만, 실제로는 훨씬 더 복잡하다. 직렬화를 지원하는것은 간단할지 몰라도 아주 값비싼 일이다.
Serializable을 구현하면 릴리즈한 뒤에는 수정하기 어렵다.
- 클래스가 직렬화를 구현하면 직렬화된 바이트 스트림 인코딩(직렬화 형태)도 하나의 공개 API가 된다. 그래서 이 클래스가 널리 퍼진다면 그 직렬화 형태도 영원히 지원을 해야한다.
- 커스텀 직렬화 형태를 설계하지 않고 자바의 기본 방식을 사용하면 직렬화 형태는 최소 적용 당시 클래스의 내부 구현 방식에 영원히 묶이게 된다. 즉 기본 직렬화 형태에서는 클래스의 private과 package-private 인스턴스 필드들 마저 API로 공개되는 꼴이 된다. 즉 캡슐화가 깨지게된다.
- 필드로의 접근을 최대한 막아 정보은닉을 하라는 아이템(15)가 무력화되는 꼴이됨
- 뒤늦게 클래스 내부 구현을 손보면 원래의 직렬화 형태와 또 달라지게 된다. 한쪽은 구버전 인스턴스를 직렬화하고 다른 쪽은 신버전 클래스로 역직렬화를 한다면 실패한다.
직렬화를 구현하고자 한다면 감당할 수 있을 만큼 고품질의 직렬화 형태로 설계해야한다.
고생이 그만큼 따르고 개발비용이 발생하지만 보상을 보장한다.
직렬화가 클래스 개선을 방해하는 예시
스트림 고유 식별자 즉 직렬 버전 UID(Serial Version ID)
- 모든 직렬화된 클래스는
SerialVersionUID
라는 이름의 static final long 필드의 식별 번호를 부여받는다. - 이 번호를 명시하지 않으면 시스템이 런타임에 암호해시 함수(SHA-1)를 적용하여 자동으로 클래스 안에 생성해 집어넣게된다.
- 이 값을 생성하는 데는 클래스 이름, 구현한 인터페이스, 컴파일러가 자동으로 생성하여 넣는 것을 포함하며 대부분의 클래스 맴버가 대상이된다.
- 나중에 편의 메서드를 추가하는 식으로 이들 중 하나라도 수정된다면
SerialVersionID
도 변한다. 즉 자동 생성되는 값에 의존하면 쉽게 호환성이 깨져버려 런타임에InvalidClassException
이 발생한다. 따라서 직접 넣는것이 좋다.
버그와 보안 구멍이 생길 위험이 높아진다.
- 객체는 생성자를 사용해 만드는게 기본이다. 즉 직렬화는 언어의 기본 매커니즘을 우회하는 객체 생성 기법이다.
- 역직렬화는 일반 생성자의 문제가 그대로 적용되는 “숨은 생성자”이다.
- 이 생성자는 전면에 드러나지 않아 생성자에서 구축한 불변식을 모두 보장해야 하고 생성 도중 공격자가 객체 내부를 들여다 볼 수 없도록 해야한다.
- 기본 역직렬화를 사용하면 불변식 깨짐과 허가되지 않은 접근에 쉽게 노출된다는 의미이다.
클래스의 신버전을 릴리즈 할 때 테스트할 것이 늘어난다.
- 직렬화 기능 클래스가 수정되면 신버전 인스턴스를 직렬화한 후 구버전으로 역직렬화 할 수 있는지, 그리고 그 반대도 가능한지 테스트가 필요하다.
- 즉 테스트해야 할 양이 직렬화 가능 클래스의 수와 릴리즈 횟수에 비례해 증가하게 된다.
클래스를 처음 제작할 때 커스텀 직렬화 형태를 잘 설계했다면 이러한 테스트 부담을 줄일 수 있긴하다.
직렬화 구현 여부는 가볍게 결정할 사안이 아니다.
- 단, 객체를 전송하거나 저장할 때 자바 직렬화를 이용하는 프레임워크용으로 만든 클래스의 경우는 제외이다.
- 직렬화를 반드시 구현해야 하는 다른 클래스의 컴포넌트로 쓰이는 클래스들도 마찬가지다.
- 하지만 직렬화 구현에 따르는 비용이 적지 않으니, 클래스를 설계할 때마다 그 이득과 비용을 잘 저울질을 해야한다.
- 역사적으로 BigInteger와 Instant같은 “값” 클래스와 컬렉션 클래스들은 직렬화를 구현함
- 스레드 풀 처럼 동작하는 객체를 표현하는 클래스들은 대부분 직렬화를 구현하지 않았음
- .. 뭐가있을까? →
DateTimeFormat


상속용으로 설계된 클래스는 직렬화를 구현해서는 안되며, 인터페이스도 대부분 직렬화를 확장해서는 안된다.
- 이 규칙을 따르지 않으면 그런 클래스를 확장하거나, 인터페이스를 구현하는 이에게 커다란 부담을 주게 된다.
- 이 규칙을 어기는 상황 도 있으며 직렬화를 구현한 클래스만 지원하는 프레임워크를 사용하는 상황이라면 다른 방법이 없다.
- 상속용으로 설계된 클래스 중에 Throwable과 Component가 있다.
- Throwable은 서버가 RMI를 통해 클라이언트로 예외를 보내기 위해 직렬화를 구현했다.
- Component는 GUI를 전송하고 저장하고 복원하기 위해 직렬화를 구현했지만, Swing과 AWT가 널리 쓰이던 시절에도 현업에서 이런 용도로 사용되지는 않았음.
RMI(Remote Method Invocation) 원격 시스템 간의 메시지 교환을 위해서 사용하는 자바에서 지원하는 기술이다. 보통은 원격의 시스템과의 통신을 위해서 IP와 포트를 이용해서 소켓통신을 해야하지만 RMI는 그 부분을 추상화하여 원격에 있는 시스템의 메서드를 로컬 시스템의 메서드인 것처럼 호출할 수 있다. 원격 시스템의 메서드를 호출 시에 전달하는 메시지를 자동으로 직렬화하며 전달받은 시스템에서는 메시지를 역직렬화를 통해 반환하여 사용된다.
- 우리가 작성하는 클래스의 인스턴스 필드가 직렬화와 확장이 모두 가능하다면 주의할 점 몇가지가 있다.
- 인스턴스 필드 값 중 불변식을 보장해야 할 게 있다면 반드시 하위 클래스에서 finalize 메서드를 재정의 하지 못하게 해야한다.
- 즉 finlize메서드를 자신이 재정의 하면서 final로 선언하면 된다. 이렇게 해두지 않으면 finalizer 공격을 당할 수도 있다.
- 인스턴스 필드 중 기본값으로 초기화되면 위배되는 불변식이 있다면 클래스에 다음의
readObjectNoData
메서드를 반드시 추가해야한다.
private void readObjectNoData() throws InvalidObjeectException { throw new InvalidObjectException("스트림 데이터가 필요합니다"); }
- 직렬화를 구현하지 않는다면 한가지만 주의하면 된다.
- 상속용 클래스인데 직렬화를 지원하지 않으면 그 하위 클래스에서 직렬화를 지원하려 할 때 부담이 늘어난다.
- 보통은 이런 클래스를 역직렬화 하려면 그 상위 클래스는 매개변수가 없는 생성자를 제공해야 하는데 우리가 이런 생성자를 제공하지 않으면 하위 클래스에서는 어쩔 수 없이 직렬화 프록시 패턴을 사용해야 한다.
내부 클래스는 직렬화를 구현하지 말아야 한다
- 내부 클래스에는 바깥 인스턴스의 참조와 유효 범위 안의 지역변수 값들을 저장하기 위해 컴파일러가 생성한 필드들이 자동으로 추가된다. 때문에 위에 언급한 직렬화에 대한 문제가 발생할 수 있다.
- 단, 정적 멤버 클래스는 직렬화를 구현해도 된다.
결론
- 직렬화를 구현한다고 선언하기는 아주 쉽지만 실제로는 굉장히 복잡하고 고려해야 할 부분이 많다.
- 보호된 환경에서만 쓰일 클래스가 아니라면 직렬화 구현은 아주 신중하게 이루어져야 한다. 또한 상속할 수 있는 클래스라면 주의사항이 더 많아지니 잘 생각하고 사용하도록 하자.