View

반응형

클래스 선언에 implements Serializable만 붙이면 어떤 클래스의 인스턴스던 직렬화 할 수 있다. 너무 쉽게 적용할 수 있기 때문에 특별히 신경 쓸게 없다고 생각할 수 있지만, 실제로는 훨씬 복잡하고 신경 써야 할 부분이 많다.

 

Serializable 구현 시 주의 사항

릴리스 후 수정이 어려워진다.

Serializable을 구현하면 릴리스 한 뒤에는 수정하기 어려워진다. Serializable을 구현한 클래스는 하나의 공개 API가 되기 때문인데, 만약 이 클래스가 널리 퍼진다면 구현한 직렬화 형태도 영원히 지원해야 한다.

 

자바의 기본 직렬화 방식을 사용하면 직렬화 형태는 적용 당시 클래스의 내부 구현 방식에 종속된다. 달리 말하면, 클래스의 private와 package-private 인스턴스 필드마저 API로 공개되어 캡슐화가 깨진다.

 

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

 

대표적으로 serialVersionUID를 예로 들 수 있다. serialVersionUID는 내부에 직접 명시하지 않는 경우 런타임 시 자동으로 생성된다. 자동 생성될 때는 클래스의 이름, 구현한 인터페이스 등이 고려되기 때문에 이들 중 하나라도 변경되면 UID 값도 달라진다. 따라서 쉽게 호환성이 깨지고 런타임에 InvalidClassException이 발생한다.

 

원래의 직렬화 형태를 유지하면서 내부 구현을 수정할 수도 있지만, 이는 어렵고 소스코드도 지저분해진다. 만약 우리가 직렬화 가능 클래스를 만든다면 고품질의 직렬화 형태도 함께 설계해야 한다.

 

버그와 보안 취약점이 생길 위험이 높아진다.

앞선 아이템(item85)에서 설명했듯, 기본 역직렬화는 숨은 생성자다. 전면에 드러나지 않는 숨은 생성자이기 때문에 불변식 깨짐과 허가되지 않은 접근에 쉽게 노출되어 버그와 보안 취약점이 생길 위험이 높아진다.

 

신버전 릴리스 시 테스트 요소가 많아진다.

직렬화 가능 클래스가 수정되면 신버전 인스턴스를 직렬화한 후 구버전으로 역직렬화할 수 있는지, 그 반대도 가능한지 검사해야 한다. 따라서 테스트해야 할 양이 직렬화 가능 클래스 수와 릴리스 횟수에 비례해 증가한다.

 

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

Serializable을 반드시 구현해야 하는 클래스는 구현에 따르는 이득과 비용을 잘 고려해 설계해야 한다. 예를 들어 BigInteger와 Instant 같은 '값' 클래스와 컬렉션 클래스들은 Serializable을 구현하고, 스레드 풀처럼 '동작'하는 객체를 표현하는 클래스는 대부분 Serializable을 구현하지 않았다.

 

상속용으로 설계된 클래스나 인터페이스는 Serializable을 확장해선 안 된다.

상속용으로 설계된 클래스나 인터페이스가 Serializable을 확장해선 안 되는 이유는 구현하는 대상에게 앞서 살펴본 문제점들이 그대로 전이되기 때문이다.

하지만 Serializable을 구현한 클래스만 지원하는 프레임워크를 사용하는 상황이라면 이 규칙을 지키지 못하는 경우도 생긴다. 대표적으로 Throwable을 예로 들 수 있는데, Throwable은 서버가 RMI를 통해 클라이언트로 예외를 보내기 위해 Serializable을 구현했다.

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

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

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

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

 

Serializable을 구현하지 않는다면..

Serializable을 구현하지 않을 때는 한 가지만 주의하면 된다.

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

 

내부 클래스는 직렬화를 지원하지 말자.

내부 클래스는 바깥 인스턴스의 참조와 유효 범위 안의 지역변수 값들을 저장하기 위해 컴파일러가 생성한 필드들이 자동으로 추가된다. 이 말은 즉, 내부 클래스의 기본 직렬화 형태가 분명하지 않음을 뜻한다. 해당 필드들이 클래스 정의에 어떻게 추가되는지 정의되지 않은 만큼 내부 클래스에서는 직렬화를 지원하면 안 된다.

단, 정적 멤버 클래스는 Serializable을 구현해도 괜찮다.

 

핵심 정리

  • Serializable을 구현할지를 신중히 결정해야 한다. 쉽게 선언할 수 있지만, 구현할 때는 꽤나 신경 써야 할 부분이 많다.
  • 한 클래스의 여러 버전이 상호작용하거나, 신뢰할 수 없는 데이터에 노출될 가능성이 있다면 Serializable은 구현은 아주 신중하게 이뤄져야 한다.
  • 상속할 수 있는(상속용) 클래스라면 주의 사항이 더욱 많아지니 Serializable을 구현하지 않는 게 좋다.
반응형
Share Link

인기 글

최신 글

전체 방문자

Today
Yesterday