Search
Duplicate

Serializable 정말 구현해야 할까? 신중히 결정하라 Effective java item. 86

태그
Effective Java
Java
Serializable
공개여부
작성일자
2021/07/25
자바에 대해 무언가 궁금한 것이 있다면 역시 Effective Java 를 보는 것이 확실한것 같다. 이 item 은 정말 Serializable 을 구현해야 하는지, 무엇이 필요한지, 주의할 것은 무엇인지를 간단히 살펴본다.
💡
직렬화: 데이터를 Stream 으로 전송할 수 있는 상태로 만든다 (ex. 파일) 역직렬화: Stream 으로 전송 받은 데이터를 객체로 만든다
어떤 클래스에 직렬화를 구현하고자 하면 Serializable 을 implements Serializable 만 붙이면된다.
매우 간단해 보이지만 그렇지 않다.
짧게 보면 쉽게 만들 수 있지만, 길게 본다면 값비싼 일이다.
Serializable 을 구현하면 릴리즈 이후에 수정이 어렵다(여기서 릴리즈는 서버 릴리즈가 아님)
HashMap java 11
HashMap java 1.8
Serializable 을 구현하면 byte stream encoding 도 하나의 공개 API 가 된다.
private, package-private 인스턴스 필드들도 API 로 공개하는 꼴이 된다. (캡슐화 깨짐)
그래서 영원히 관리해야 하는 대상이 된다.
시간이 지나 내부 구현을 수정하게 되면 원래의 직렬화 형태와 달라지게 된다.
만약 한 쪽은 구버전 instance 를 직렬화 하고, 한쪽은 신버젼 클래스로 역직렬화 하는 일이 발생된다면..?
직렬화를 구현하고자 한다면 감당할 수 있을 만큼의 고품질의 직렬화 형태로 설계해야 한다.
고생이 크겠지만, 보상은 달콤하다

직렬화가 클래스 개선을 방해하는 예시들

직렬 버젼 UID(serial Version ID)

모든 직렬화된 클래스는 serialVersionUID 라는 이름의 static final long 필드의 식별 번호를 부여받는다.
이 번호를 명시하지 않으면 runtime 에서 sha-1 을 적용하여 자동으로 클래스에 집어넣는다.
클래스 이름, 구현한 interface, 컴파일러가 자동 생성하여 넣는 것을 포함하며 대부분의 class member 가 대상이다.
그래서 나중에 method 를 추가하여 이들중 하나라도 수정되면 UID 도 깨지게 되므로 자동생성보단 직접 넣는것이 좋다.

버그와 보안 구멍이 생길 위험이 높아진다

직렬화는 language 의 매커니즘을 우회하는 객체 생성 기법이다.
역직렬화는 일반 생성자의 문제가 그대로 적용되는 '숨은 생성자' 이다.
이 역직렬화 생성자는 전면에 드러나지 않기 때문에 생성자에 적용되는 규칙을 적용하겠다고 생각하기 어렵게 된다.
생성자에서 구축한 불변식을 모두 보장해야 한다
생성 도중 공격자가 객체 내부를 들여다볼 수 없도록 해야 한다.
역직렬화를 이용하면 불변식은 깨지고, 허가되지 않은 접근에 노출된다.

클래스의 신버전을 일리즈 할때 테스트할 것이 늘어난다.

신버젼 instance 를 직렬화 하고, 구버젼으로 역직렬화가 되는지 (하위 호환성) 검사가 필요하다
이 반대도 가능한지 테스트해야 한다.
처음 설계시 이러한 부분까지 고민하지 않는다면 테스트가 부담스러워 진다.

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

단, 객체를 전송하거나, 직렬화를 이용한 framework 에서는 선택의 여지가 없다.
Serializable 을 구현에 따른 비용이 적지 않으니 그 이득과 비용을 잘 저울질 해야한다
BigInteger, Instant 같은 'value' class 와 collection 은 Serializable 을 구현하고
public class ReentrantLock implements Lock, java.io.Serializable { public class BigInteger extends Number implements Comparable<BigInteger> {
Java
thread pool 과 같이 '동작' 하는 class는 serializable 을 구현하지 않는다
public final class DateTimeFormatter {
Java

상속용으로 설계된 클래스는 Serialzable 을 구현해선 안되며, 인터페이스도 대부분 Serializable 을 확장해선 안된다.

예외 상황은 Serializable 을 구현한 클래스만 지원하는 framework 를 사용할 때 이다.
상속용으로 설계된 클래스의 예시는 다음과 같다
public class Throwable implements java.io.Serializable {
Java
Throwable 은 서버가 RMI 를 통해 예외를 보내기 위해 필요하다
public abstract class Component implements ImageObserver, MenuContainer, Serializable {
Java
Component 는 GUI 를 전송, 저장, 복원을 위해 사용하는데 swing, AWT 가 많이 사용될 때에도 이런 용도로 사용하진 않았다.

invariant (불변식) 은 재정의 하지 못하게 해야 한다.

class Rational { public: Rational(int n = 0, int d = 1); ~Rational(); public: void setDenominator(int d) { d == 0 ? error_msg("zero divide"): denominator = d; } private: int numerator, denominator; };
Java
불변식 예시 0으로 나눌 수 없다.

직렬화에 포함되선 안되는 것이 있다면

readObjectNoData 를 반드시 추가하라
instance field 중 기본값(int → 0, boolean → false, 객체 참조 null)으로 초기화 되었을때 이것이 invariant 인 경우 구현이 필요하다
private void readObjectNoData() throws InvalidObjeectException { throw new InvalidObjectException("스트림 데이터가 필요합니다"); }
Java

Serializable 을 구현하지 않는다면

상속 용도의 class 인데 직렬화를 지원하지 않으면 하위 클래스에서 직렬화를 지원할 때 부담이 늘어난다
이러한 경우 상위 클래스는 매개변수가 없는 생성자를 제공해야 한다.
이런 생성자를 지원하지 않을 경우 직렬화 proxy pattern 을 사용해야 한다(item90)

inner class 는 직렬화를 구현하지 말아야 한다.

보통 inner 클래스는 바깥 instance 의 참조와 유효 범위 안의 지역변수 값들을 저장하기 위해 compiler 가 생성한 필드들이 자동으로 추가된다.
이것 때문에 직렬화에 대한 앞서 언급한 문제들이 발생할 수 있다.
단, static member class 들은 Serializable 을 구현해도 된다.

직렬화 시리즈

직렬화(Serializ), 역직렬화에 대한 개념 정리

Effective Java 86 Serializable 을 구현할지 신중히 결정하자

Effective Java 87 커스텀 직렬화 형태를 고려해보자

Effective Java 88 readObject 는 방어적으로 작성하라

TOP