Skip to content

[아이템 86] Serializable을 구현할지는 신중히 결정하라 #104

@jupyter471

Description

@jupyter471

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

✨ 핵심 내용

1. Serializable을 구현하면 릴리스한 뒤에는 수정하기 어렵다

  • 클래스가 Serializable을 구현하면 직렬화된 바이트 스트림 인코딩도 하나의 공개 API가 된다

  • 이 클래스가 사용되는 곳이면 직렬화 형태도 영원히 지원해야한다

  • 만약, 자바 기본 방식을 사용하면 직렬화 형태는 최소 적용 당시 클래스의 내부 구현 방식에 영원히 묶인다

    ⇒ 기본 직렬화 형태에서는 클래스의 private 등 인스턴스 필드들마저 API로 공개되는 사태 발생! 캡슐화 깨진다!

만약 뒤늦게 클래스 내부 구현을 수정하면 원래의 직렬화 형태와 달라지게 된다

  • 한쪽은 구버전 인스턴스 직렬화, 다른쪽은 신버전 클래스로 역직렬화 → 실패

직렬화 가능 클래스를 만들고자 한다면, 길게 보고 감당할 수 있을 만큼 고품질의 직렬화 형태도 주의해서 함께 설계해야한다

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

스트림 고유 식별자, 직렬버전 UID

  • 모든 직렬화된 클래스는 고유 식별 번호를 부여 받는다
  • 명시하지 않으면 시스템이 런타임에 암호 해시 함수를 적용해 자동으로 클래스 안에 생성해 넣는다
  • 이 값을 생성할 때는 클래스 이름, 인터페이스 등 대부분의 클래스 멤버들이 고려됨
  • 그렇기에, 나중에 하나라도 수정한다면 UID값도 변한다

⇒ 자동 생성되는 값에 의존하면 쉽게 호환성이 깨져버려 런타임에 InvalidClassException 발생

2. Serializable 구현은 버그와 보안 구멍이 생길 위험이 높아진다

객체는 생성자를 사용해 만드는게 기본

그러나 직렬화는 언어 기본 메커니즘을 우회하는 객체 생성 기법이다

⇒ “숨은 생성자”

기본 역직렬화를 사용하면 불변식 깨짐과 허가되지 않은 접근에 쉽게 노출된다 (아이템 88)

3. 해당 클래스의 신버전을 릴리스할 때 테스트 요소가 늘어난다

직렬화 가능 클래스가 수정되면 신버전 인스턴스를 직렬화한 후 구버전으로 역직렬화할 수 있는지, 그 반대도 가능한지 검사해야함

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

비용도 크다 그렇기에 이득과 비용을 잘 판단하고 구현해야한다

✏️ 역사적으로 BigInteger와 Instant 같은 값 클래스, 컬렉션 클래스는 Serializable 을 구현하고, 스레드풀 처럼 동작하는 객체를 표현하는 클래스는 구현하지 않았다

💡

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

→ 클래스를 확장하거나 인터페이스르 구현하는 이에게 부담이 크다

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

상속용으로 설계된 클래스 중 Serializable을 구현한 예시

  • Throwable
  • Component

클래스의 인스턴스 필드가 직렬화와 확장이 가능하다면 주의할 점

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

→ finalize 메서드를 자신이 재정의하면서 final로 선언

⚠️ 인스턴스 필드 중 기본값으로 초기화되면 위배되는 불변식이 있다면 클래스에 readObjectNoData 메서드를 반드시 추가해야함

public class User implements Serializable {
    private static final long serialVersionUID = 1L;

    private final String username;
    private final int level;

    public User(String username, int level) {
        if (username == null) throw new NullPointerException("username is required");
        if (level < 1) throw new IllegalArgumentException("level must be >= 1");
        this.username = username;
        this.level = level;
    }

    private void readObjectNoData() throws ObjectStreamException {
        throw new InvalidObjectException("Stream data required to deserialize User object");
    }
}

username은 null이면 안되고 level은 1이상이어야한다는 불변식이 깨지게 된다

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

⚠️ 상속용 클래스가 직렬화를 지원하지 않을 때 하위 클래스에서 직렬화를 지원하려 할 때 부담이 커진다

→ 역직렬화할 때 상위클래스가 매개변수가 없는 생성자 제공해야함

⇒ 하위클래스에서는 어쩔 수 없이 직렬화 프록시 패턴을 사용해야한다 (아이템 90)

⚠️ 내부 클래스는 직렬화를 구현하면 안된다

내부 클래스는 바깥 인스턴스의 참조와 유효 범위 안의 지역변수 값들을 저장하기 위해 컴파일러가 생성한 필드들이 자동으로 추가된다

💡 새롭게 알게 된 점

  • 학습하면서 이전과 다르게 이해한 점이나 새롭게 알게 된 개념을 공유해주세요.

📚 정리

한 클래스의 여러 버전이 상호작용할 일이 없고, 보호된 환경에서만 쓰일 클래스가 아니면 Serializable 구현은 신중해야한다


📢 댓글로 각자의 학습 내용을 공유해주세요!

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions