item 86) Serializable을 구현할지는 신중히 결정하라

Serializable 구현의 문제점

1) 릴리스 후 수정 어려움

  • Serializable을 구현하면 릴리스 한 뒤에는 수정하기 어려워진다.

  • Serializable을 구현한 클래스는 하나의 공개 API가 되므로 구현한 직렬화 형태도 영원히 지원해야 한다.

  • 직렬화 가능 클래스를 만든다면 고품질의 직렬화 형태도 함께 설계해야 한다.

  • 자바의 기본 직렬화 방식을 사용하면 직렬화 형태는 적용 당시 클래스의 내부 구현 방식에 종속된다.

  • 원래의 직렬화 형태를 유지하면서 내부 구현을 수정할 수도 있지만, 이는 어렵고 소스코드도 지저분해진다.

  • 클래스의 private와 package-private 인스턴스 필드마저 API로 공개되어 캡슐화가 깨진다.

  • 나중에 내부 구현을 수정하면 직렬화 형태가 달라져 직렬화-역직렬화에 실패할 수 있다.

  • ex) serialVersionUID 자동 생성 이슈

    • Serializable 클래스의 버전을 기억해 클래스와 직렬화 된 객체가 호환되는지 확인하는 식별자

    • 직접 번호를 명시하지 않는 경우 런타임 시 자동으로 생성된다.

    • 자동 생성될 때 클래스의 이름, 구현한 인터페이스 등이 고려되기 때문에 이들 중 하나라도 변경되면 UID 값도 달라진다. 따라서 쉽게 호환성이 깨지고 런타임에 InvalidClassException이 발생한다.

2) 버그와 보안 취약점 발생 위험

  • 기본 역직렬화는 전면에 드러나지 않는 숨은 생성자이기 때문에 불변식 깨짐과 허가되지 않은 접근에 쉽게 노출된다.

3) 신버전 릴리스 시 테스트 요소 증가

  • 직렬화 가능 클래스가 수정되면 신버전/구버전 인스턴스 간의 양방향 직렬화/역직렬화가 가능한지 검사해야 한다.

  • 테스트해야 할 양이 직렬화 가능 클래스 수와 릴리스 횟수에 비례해 증가한다.

4) 구현 여부는 쉽게 결정할 사안이 아니다.

  • Serializable을 반드시 구현해야 하는 클래스는 구현에 따르는 이득과 비용을 잘 고려해 설계해야 한다.

  • BigInteger와 Instant 같은 '값' 클래스와 컬렉션 클래스들은 Serializable을 구현하고, 스레드 풀처럼 **'동작'**하는 객체를 표현하는 클래스는 대부분 Serializable을 구현하지 않았다.

    💡 값을 나타내는 클래스는 아무래도 Spring의 entity와 비슷하게 외부와 통신하는 경우가 많을 것 같다.

5) 상속용으로 설계된 클래스/인터페이스는 Serializable 확장 불가

  • 해당 클래스/인터페이스를 구현하는 대상에게 Serializable의 문제점들을 그대로 전이하기 때문

  • 하지만 Serializable을 구현한 클래스만 지원하는 프레임워크를 사용하는 상황이라면 이 규칙을 지키지 못하는 경우도 생긴다.

  • ex) Throwable

    • Throwable은 서버가 RMI를 통해 클라이언트로 예외를 보내기 위해 Serializable을 구현했다.

    RMI(Remote Method Invocation) 자바와 개발환경을 사용하여 서로 다른 컴퓨터들 상에 있는 객체들이 분산 네트워크 내에서 상호 작용하는 객체지향형 프로그램을 작성할 수 있는 방식이다.

  • 직렬화, 확장(extends)이 모두 가능한 클래스에서 불변식을 보장해야 한다면 finalize 메서드를 하위 클래스가 재정의하지 못하도록 자신이 재정의하면서 final로 선언해야 finalizer 공격을 피할 수 있다.

  • 필드 중 원시 값으로 초기화되면 위배되는 불변식이 있다면 readObjectNoData를 반드시 추가해야 한다.

    private void readObjectNoData() throws InvalidObjectException {
    	throw new InvalidObjectException("스트림 데이터가 필요합니다.");
    }

Serializable을 구현하지 않는 상위 클래스

  • 상속용 클래스에서 직렬화를 지원하지 않지만, 하위 클래스에서 직렬화를 지원하려 한다면 부담이 늘어난다.

  • 이런 클래스를 역직렬화하려면 상위 클래스의 매개변수가 없는 생성자를 제공하거나, 하위 클래스에서 직렬화 프록시 패턴을 사용해야 하기 때문이다.

내부 클래스와 직렬화

  • 내부 클래스에는 바깥 인스턴스의 참조와 유효 범위 안의 지역변수 값들을 저장하기 위해 컴파일러가 생성한 필드들이 자동으로 추가되어 기본 직렬화 형태가 유동적이다.

  • 정적 멤버 클래스는 컴파일러에 의해 자동 추가되는 필드가 따로 없으므로 Serializable을 구현해도 괜찮다.

Last updated