[교재 EffectiveJava] 아이템 86. Serializable을 구현할지는 신중히 결정하라

반응형
728x90
반응형

Serializable

어떤 클래스의 인스턴스를 직렬화할 수 있게 하려면 클래스 선언에 implements Serializable만 덧붙히면 된다. 너무 쉽게 적용할 수 있는것에 비해, 직렬화는 아주 값비싼 일이다.

 

Serializable을 구현하면 릴리스한 뒤에는 수정하기 어렵다. 클래스가 Serializable을 구현하면 직렬화된 바이트 스트림 인코딩(직렬화 형태)도 하나의 공개 API가 된다. 그래서 이 클래스가 널리 퍼진다면 그 직렬화 형태도 영원히 지원해야한다. 커스텀 직렬화 형태를 설계하지 않고 자바의 기본 방식을 사용하면 직렬화 형태는 최소 적용 당시 클래스의 내부 구현 방식에 영원히 묶여버린다.

기본 직렬화 형태에서는 클래스의 private, package-private 인스턴스 필드들마저 API로 공개되버린다. (캡슐화가 깨진다.)

 

 

직렬화 형태 설계 

뒤늦게 클래스 내부 구현을 수정하면 원래의 직렬화 형태와 달라지게 된다. 한쪽은 구버전 인스턴스를 직렬화하고 다른 쪽은 신버전 클래스로 역직렬화한다면 실패할 것이다. 원래의 직렬화 형태를 유지하면서 내부 표현을 바꾸는것은 어렵고 소스코드에 지저분한 혹을 남겨놓게 된다. 따라서 직렬화 기능 클래스를 만들고자 한다면, 길게 보고 감당할 수 있을 만큼 고품질의 직렬화 형태도 주의해서 함께 설계해야한다. 

직렬화 형태를 잘 설계하더라도 클래스를 개선하는데 제약이 될 수 있으므로, 잘못 설계한 경우라면 더 큰일이다. 

 

 

식별 버전 UID(serial version UID)

대표적으로는 스트림 고유 식별자, 즉 식별 버전 UID(serial version UID)를 들 수 있다. 모든 직렬화된 클래스는 고유 식별 번호를 부여받는다. serialVersionUID라는 이름의 static final long 필드로, 이 번호를 명시하지 않으면 시스템이 런타임에 암호 해시 함수(SHA-1)를 적용해 자동으로 클래스 안에 생성해 넣는다. 이 값을 생성하는 데는 클래스 이름, 구현한 인터페이스들, 컴파일러가 자동으로 생성해 넣은 것을 포함한 대부분의 클래스 멤버들이 고려된다. 그래서 나중에 편의 메서드를 추가하는 식으로 이들 중 하나라도 수정한다면 직렬 버전 UID 값도 변한다. 다시말해, 자동 생성되는 값에 의존하면 쉽게 호환성이 깨져버려 런타임에 InvalidClassException이 발생할 것이다.

 

 

버그와 보안의 위험성 증가

Serializable 구현은 버그와 보안 구멍이 생길 위험이 높아진다. 객체는 생성자를 사용하여 만드는게 기본이다. 즉, 직렬화는 언어의 기본 메커니즘을 우회하는 객체 생성 기법인 것이다. 역직렬화의 '숨은 생성자'는 전면에 드러나지 않으므로 "생성자에서 구축한 불변식을 모두 보장해야 하고 생성 도중 공격자가 객체 내부를 들여다볼 수 없도록 해야한다."는 사실을 떠올리기 어렵다. 기존 역직렬화를 사용하면 불변식 깨짐과 허가되지 않은 접근에 쉽게 노출된다.

 

 

신버전 릴리스의 경우 테스트 증가

Serializable의 구현은 클래스의 신버전을 릴리스할 때 테스트할 것이 늘어난다. 직렬화 기능 클래스가 수정되면 신버전 인스턴스를 직렬화한 후 구버전으로 역직렬화할 수 있는지, 반대도 가능한지 검사해야한다. 따라서 테스트해야할 양이 직렬화 가능 클래스의 수와 릴리스 횟수에 비례하여 증가한다. 양방향 직렬화/역직렬화가 모두 성공하고 원래의 객체를 충실히 복제해내는지를 반드시 확인해야한다. 

 

 

Serializable 구현 선택

Serializable 구현 여부는 쉽게 결정하면 안된다. 객체를 전송하거나 저장할때 자바 역직렬화를 이용하는 프레임워크용으로 만든 클래스라면 Serializable을 구현해야 한다. Serializable을 반드시 구현해야하는 다른 클래스의 컴포넌트로 쓰일 클래스들도 마찬가지다. 하지만 Serializable 구현에 따르는 비용은 적지 않으므로, 클래스를 설계할 때마다 그 이득과 비용을 잘 저울질해야 한다.

 

 

상속용으로 설계된 클래스는 Serializable 구현하지 않기

상속용으로 설계된 클래스는 대부분 Serializable을 구현하면 안되며, 인터페이스도 대부분 Serializable를 확장해서는 안된다. 그렇지 않으면, 그런 클래스를 확장하거나 그런 인터페이스를 구현하는 이에게 커다란 부담을 주게된다. 이 규칙을 어겨야하는 상황은 Serializable을 구현한 클래스만 지원하는 프레임워크를 사용하는 상황이다.

 

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

 

 

클래스의 인스턴스 필드가 직렬화와 확장이 모두 가능할때 주의할점

1) 인스턴스 필드 값 중 불변식을 보장해야할게 있다면 반드시 하위 클래스에서 finalize 메서드를 재정의하지 못하게 해야한다.

즉, finalize 메서드를 자신이 재정의하면서 final로 선언하면 된다. 

이렇게 해두지 않으면 finalizer 공격을 당할 수 있다.

 

2) 인스턴스 필드 중 기본값(정수형은 0, boolean은 false, 객체 참조 타입은  null)으로 초기화되면 위배되는 불변식이 있다면 클래스에 다음의 readObjectNoData 메서드를 반드시 추가해야한다.

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

위 메서드는 기존의 직렬화 가능 클래스에 직렬화 가능 상위 클래스를 추가하는 드문 경우를 위한 메서드다.

 

 

Serializable을 구현하지 않기로 할때의 주의점

상속용 클래스인데 직렬화를 지원하지 않으면 그 하위 클래스에서 직렬화를 지원하려고할 때 부담이 늘어난다. 보통은 이런 클래스를 역직렬화하려면 그 상위 클래스는 매개변수가 없는 생성자를 제공해야하는데, 이런 생성자를 제공하지 않으면 하위 클래스에서는 어쩔 수 없이 직렬화 프록시를 사용해야한다.

 

직렬화 프록시

https://devfunny.tistory.com/864

 

[교재 EffectiveJava] 아이템 90. 직렬화된 인스턴스 대신 직렬화 프록시 사용을 검토하라

직렬화 프록시 패턴(serialization proxy pattern) 바깥 클래스의 논리적 상태를 정밀하게 표현하는 중첩 클래스를 설계하여 private static으로 선언한다. 이 중첩 클래스가 바로 바깥 클래스의 직렬화 프

devfunny.tistory.com

 

 

내부 클래스는 직렬화를 구현하지 말자

내부 클래스에는 바깥 인스턴스의 참조와 유효 범위 안의 지역변수 값들을 저장하기 위해 컴파일러가 생성한 필드들이 자동으로 추가된다. 익명 클래스와 지역 클래스의 이름을 짓는 규칙이 언어 명세에 나와 있지 않듯, 이 필드들이 클래스 정의에 어떻게 추가되는지도 정의되지 않았다. 다시말해 내부 클래스에 대한 기본 직렬화 형태는 분명하지가 않다. 

 

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

 

 

반응형

Designed by JB FACTORY