Backend/Java

[Effective Java : Item 86] Serializable을 구현할지는 신중히 결정하라

김세진 2024. 5. 8. 15:27
반응형

 

 

 

 

 

개요

 

특정 클래스의 인스턴스를 직렬화하고 싶다면 Serializable을 구현하도록 클래스 선언에 implements Serializable만 붙이면 된다. Serializable 내부적으로 구현이 강제되는 메서드 또한 존재하지 않아 쉽게 적용할 수 있다고 생각될 수 있으나, 실상은 직렬화를 구현한다는 것은 훨씬 복잡하고 값비싼 일이다.

 

 


Serializable 구현 시 주의할 점

 

1. 릴리스한 뒤에는 수정하기 어렵다.

Serializable을 구현하면 직렬화된 바이트 스트림 인코딩도 하나의 공개 API가 되는 셈이다. 따라서 릴리스가 된 뒤에는 이 직렬화 형태도 영원히 지원해야 한다.

 

또한 커스텀 직렬화 형태를 사용하지 않고 자바의 기본 직렬화를 사용한다면 최초 직렬화 당시 클래스의 내부 구현 방식에 묶인다. 이 경우 private과 package-private 인스턴스 필드들도 공개 API가 되어 캡슐화가 깨진다.

 

클래스에 신규 필드를 추가하는 등 뒤늦게 내부 구현을 손본다면 원래의 직렬화 형태와 달라지게 된다. 구버전의 인스턴스를 직렬화한 것을 신버전 클래스로 역직렬화할 경우 실패 가능성이 생기게 되는 것이다. 그 예시 중 하나로, 모든 직렬화 클래스는 serialVersionUID 라는 static final long 필드가 있는데, 이 필드를 직접 명시하지 않으면 클래스 내부의 다양한 구현을 고려하여 해시 함수를 통해 런타임에 UID를 생성해 채워넣게 된다. 이러한 자동 생성 방식에 의존할 때 만약 클래스 구현이 달라질 경우 UID 값이 변해, 역직렬화할 경우 호환성이 깨져 InvalidClassException이 발생하게 되는 것이다.

 

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

객체는 생성자를 통해 생성하는게 기본 메커니즘이지만, 직렬화는 이를 우회하는 객체 생성 기법, 달리 말해 '숨은 생성자'이다. 명시적으로 드러나있는 생성자가 아니기 때문에 불변식이 깨지기 쉽고, 허가되지 않은 접근에 쉽게 노출될 수 있다.

 

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

릴리스 버전 간 직렬화 가능 클래스의 직렬화와 역직렬화가 실패 없이 되는지 검사해야 한다. 따라서 테스트해야 할 양이 직렬화 가능 클래스의 수와 릴리스 횟수에 비례하여 증가한다. 이러한 부담을 덜기 위해서는 클래스를 처음 설계할 때 커스텀 직렬화 형태를 잘 설계해야 한다.

 

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

만약 아래 두 가지 사항에 해당한다면 선택의 여지 없이 Serializable을 구현해야 한다.

 

- 객체를 전송하거나 저장할 때 자바 직렬화를 이용하는 프레임워크용으로 만든 클래스

- Serializable을 반드시 구현해야 하는 다른 클래스의 컴포넌트로 쓰일 클래스

 

그 외의 경우는 Serializable 구현에 따르는 비용이 적지 않으니, 클래스를 설계할 때마다 그 이득과 비용을 잘 비교해야 한다. 일반적으로 BigIntegerInstant 같은 '값' 클래스와 컬렉션 클래스들은 Serializable을 구현하고, 스레드 풀처럼 '동작'하는 객체를 표현하는 클래스들은 대부분 Serializable을 구현하지 않는다.

 

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

4번의 예외와 마찬가지로 Serializable을 구현하도록 강제하는 프레임워크를 사용하는 상황이라면, 어쩔 수 없이 Serializable을 구현하거나 확장해야 한다.

 

그렇지 않을 경우, 위 규칙을 따르지 않으면 이러한 클래스를 구현하거나 확장하려는 클라이언트에게 커다란 부담을 지우게 된다.

 

하지만 반대로 상속용 클래스인데 Serializable을 구현하지 않기로 결정한 경우, 하위 클래스에서 직렬화를 지원하려고 할 때 부담이 늘어나게 된다는 점도 주의해야 한다. 이러한 경우 상위 클래스에서 매개변수가 없는 생성자를 제공해야 하위 클래스를 역직렬화 할 수 있다. 만약 이런 생성자를 제공하지 않을 경우, 직렬화 프록시 패턴(아이템 90)을 사용해야 한다.

 

6. 내부 클래스(아이템 24)는 직렬화를 구현하지 말아야 한다.

내부 클래스에 대한 기본 직렬화 형태는 분명하지가 않으므로 Serializable을 구현하면 안 된다. 내부 클래스는 외부 클래스에 대한 참조를 사용할 수 있는데, 이는 내부 클래스의 상태가 외부 클래스의 상태에 의존적임을 의미한다.

 

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

 

 


정리

 

1. Serializable은 구현한다고 선언하기는 쉽지만, 실상은 아주 큰 부담을 지우는 것이다.

2. 한 클래스의 여러 버전이 상호작용할 일이 없고, 서버가 신뢰할 수 없는 데이터에 노출될 가능성이 없는 것이 아니라면 Serializable의 구현은 신중하게 이루어져야 한다.

3. 상속할 수 있는 클래스라면 주의사항이 더욱 많아진다.

 

 

 

 

 

반응형