Home [Effective Java] Item86. Serializable을 구현할지는 신중히 결정하라!
Post
Cancel

[Effective Java] Item86. Serializable을 구현할지는 신중히 결정하라!

Item86. Serializable을 구현할지는 신중히 결정하라!

Intro

  • 어떤 클래스의 인스턴스를 직렬화 하려면 클래스 선언에 implements Serializable만 덧붙이면 된다.
    • 너무나도 쉽게 적용 가능하다.
  • 직렬화 적용이 쉽기에 직렬화를 지원하기란 손쉬워 보인다. 하지만 그렇지 않다. 아주 값비싼 일이란걸 알아두자.

Serializable 구현의 문제점

  1. 릴리즈 이후 수정하기 어렵다.
    • 클래스가 Serializable 구현 시, 직렬화된 바이트 스트림 인코딩도 하나의 공개 API가 된다. 그래서 이러한 클래스가 널리 퍼진다면, 그 직렬화 형태도 영원히 지원해야만 한다.
    • 커스텀 직렬화 형태를 설계하지 않고, 자바가 기본적으로 지원하는 기본 직렬화 형태에서는 클래스의 private과 package-private 인스턴스 필드들마저 API로 공개된다. 즉 캡슐화가 깨진다.
    • 뒤늦게 클래스 내부 구현을 손보면 직렬화 형태는 달라질 것이고, 한쪽은 구버전 인스턴스 직렬화 하고 다른 한쪽은 신버전 클래스로 역직렬화할 것이다. 이러면 실패를 맛볼 것이다. 원래의 직렬화 형태 유지를 하면서 내부 표현 변경을 할 수도 있으나, 어렵고 소스코드가 지저분해진다.
      • 그러니 직렬화 가능 클래스를 만들고자 한다면, 길게 보고 감당할 수 있을 만큼 고품질의 직렬화 형태도 주의해서 함께 설계해야 한다.
    • 직렬화가 클래스 개선을 방해하는 대표적인 예로 고유 식별자, 즉 serial version UID를 들 수 있다. 모든 직렬화된 클래스는 고유 식별 번호를 받는다. 이 값을 생성할 때 클래스 이름, 구현 인터페이스들, 컴파일러가 자동으로 생성해 넣은 것을 포함한 대부분의 클래스들이 고려된다. 그렇기에 수정이 일어나면 serial version UID에 변화가 생기고, 호환 의존성이 깨지게 되어 InvalidClassException이 발생할 것이다.
  2. 버그와 보안 구멍이 생길 위험이 높아진다.

    • 객체 생성은 생성자를 통하는 것이 기본인데, 직렬화는 이러한 기본 매커니즘을 우회하는 객체 생성 기법이다.
    • 역직렬화는 일반 생성자의 문제가 그대로 적용되는 ‘숨은 생성자’이고, 이 생성자는 전면에 드러나지 않으므로 ‘생성자에서 구축한 불변식을 모두 보장해야 하고, 생성 도중 공격자가 객체 내부를 들여다 볼 수 없게 해야 한다’는 사실을 떠올리기 어렵다.
    • 그러니, 기본 역직렬화를 사용하면 불변식 깨짐과 허가되지 않은 접근에 쉽게 노출된다.
  3. 해당 클래스의 신버전을 릴리즈할 때 테스트할 것이 늘어난다.

    • 직렬화 가능 클래스 수정 시, 신버전과 구버전의 직렬화, 역직렬화 과정을 양방향으로 수행해보고, 객체를 잘 복제해내는지 확인하는 테스트가 필요하다. 이는 테스트 횟수가 직렬화 가능 클래스 수와 릴리즈 횟수에 비례한다는 것을 말한다.
    • 클래스를 처음 제작할 때 직렬화 형태를 잘 설계해놨다면 이러한 테스트 부담이 줄어들 것이다.

Serializable 구현 여부

  • Serializable 구현 여부는 가볍게 결정할 사안이 아니다.
    • 객체를 전송하거나 저장할 때 자바 직렬화를 이용하는 프레임워크용으로 만든 클래스라면 선택의 여지가 없다. Serializable을 반드시 구현해야 하는 다른 클래스의 컴포넌트로 쓰일 클래스도 마찬가지다.
      • Serializable 구현에 따르는 비용이 적지 않으니, 클래스를 설계할 때마다 그 이득과 비용을 잘 계산하자.
    • 역사적으로 BigInteger와 Instant 같은 ‘값’ 클래스와 컬렉션 클래스들은 Serializable을 구현하고, 스레드 풀처럼 ‘동작’하는 객체를 표현하는 클래스는 대부분 구현하지 않았다.
  • 상속용으로 설계된 클래스는 대부분 Serializable을 구현하면 안 되며, 인터페이스도 대부분 Serializable을 확장해서는 안된다.

    • 이 규칙을 따르지 않는다면, 이러한 클래스를 확장하거나 인터페이스를 구현하는 데에 있어 큰 부담을 지우게 된다.

    • 하지만 Serializable을 구현한 클래스만 지원하는 프레임워크를 사용하는 상황이라면 이를 어길 수밖에 없을 것이다.

    • 상속용으로 설계된 클래스 중 Serializable을 구현한 예로는 Throable, Component가 있다.

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

      • 인스턴스 필드 중 불변식을 보장할 것에는 하위 클래스에서 finailize 메서드를 재정의하지 못하도록, finalize 메서드를 자신이 재정의하면서 final로 선언하자.

      • 인스턴스 필드 중 기본값으로 초기화되면 위배되는 불변식이 존재할 경우, 클래스에 readObjectNoData 메서드를 반드시 추가하자.

        1
        2
        3
        
        private void readObjectNoData() throws InvalidObjectException {
        	throw new InvalidObjectException("스트림 데이터가 필요합니다");
        }
        
    • 상속용 클래스는 Seralizable을 지원하지 않고, 하위 구현 클래스가 이를 구현하려 한다면 부담이 늘어난다. 보통 이러한 경우, 역직렬화를 위해 상위 클래스는 매개변수가 없는 생성자를 제공해야 하는데, 만약 이런 생성자를 제공하지 않는다면 하위 클래스는 직렬화 프록시 패턴을 사용해야만 한다.

  • 내부 클래스는 직렬화를 구현하지 말아야 한다.
    • 내부 클래스에는 바깥 인스턴스 참조와 유효 범위 안의 지역변수 값의 저장을 위해 컴파일러가 생성한 필드들이 자동으로 추가된다. 자동으로 추가되는 데에 어떤 규칙이 있는지 아무도 모른다. 다시 말해 내부 클래스에 대한 기본 직렬화 형태는 불분명하다.
    • 단, 정적 멤버 클래스는 Serializable 구현이 가능하다.

핵심 정리

  • Serializable은 구현한다고 선언하기는 아주 쉽지만, 눈속임일 뿐이다.
  • 한 클래스의 여러 버전이 상호작용할 일이 없고, 서버가 신뢰할 수 없는 데이터에 노출되지 않는 등의 보호된 환경에서만 쓰이는 것이 아니라면, Serializable 구현은 아주 신중이 이뤄져야 한다.
  • 상속 가능 클래스라면 더욱 더 주의하자.
This post is licensed under younghwani by the author.

[Env] 사용중인 포트 찾아서 죽이기

[Effective Java] Item87. 커스텀 직렬화 형태를 고려해보라!

Comments powered by Disqus.