86. Serializable을 구현할지는 신중히 결정하라
"implements Serializable" 한 줄이 시스템 전체의 발목을 잡을 수 있다!
📌 핵심 요약
⚠️
Serializable
구현은 신중히 결정해야 한다.📡 한 번 직렬화 형태가 공개되면 캡슐화 파괴, 호환성 유지 비용 증가, 보안 취약점 등 장기적인 부작용 발생.
🛡️ 꼭 필요한 경우가 아니라면 구현을 피하고, 해야 한다면 불변식 보장 및 필터링 등 조치가 필요하다.
🧠 필수 개념 요약
직렬화 (Serialization)
객체를 바이트 스트림으로 변환하는 과정
직렬화 형태
직렬화된 바이트 스트림의 구조 → 공개 API처럼 취급됨
serialVersionUID
클래스 버전 식별자, 명시하지 않으면 자동 생성됨
캡슐화 깨짐
private 필드까지 외부에 노출될 수 있음
readObjectNoData()
스트림에 데이터가 없을 때 불변식 보장을 위해 호출
🔍 주요 문제점
1. ✏️ 변경의 어려움
직렬화 형태도 공개 API가 되기 때문에 이후 구조 변경이 매우 어렵다.
자바 기본 직렬화 방식은 private 필드까지 포함하여 캡슐화 원칙을 위반한다.
2. 🧮 serialVersionUID 호환성 문제
자동 생성된
serialVersionUID
는 작은 변경에도 달라짐.값을 명시하지 않으면 구버전과 역직렬화 시 InvalidClassException 발생 가능.
3. 🔓 보안 취약점
생성자 없이 객체를 생성 → 불변식이 깨질 수 있고, 공격에 취약.
역직렬화는 사실상 숨겨진 생성자.
4. 🧪 테스트 부담 증가
버전 간 직렬화/역직렬화 호환 테스트가 필요.
릴리스가 많아질수록 테스트 비용도 증가.
✅ Serializable 적절/부적절 판단 기준
값 클래스, 컬렉션 클래스 (예: BigInteger
, Instant
)
✅ 구현 OK
프레임워크 사용을 위한 DTO 등
✅ 구현 필요
동작 객체 (예: 스레드풀, GUI 등)
❌ 지양
상속용 클래스 / 인터페이스
❌ 지양
내부 클래스 (정적 멤버 클래스 제외)
❌ 지양
🛡️ 구현 시 주의사항 체크리스트
serialVersionUID
명시적으로 선언 ✅불변식을 깨뜨릴 수 있는 필드 →
readObjectNoData()
메서드 구현 ✅finalizer 공격 방지 →
finalize()
메서드 final로 선언 ✅문서화 및 직렬화 형태 관리 ✅
양방향 직렬화/역직렬화 테스트 계획 ✅
🧪 예제 코드
예제 1: readObjectNoData()
방어 구현
readObjectNoData()
방어 구현private void readObjectNoData() throws InvalidObjectException {
throw new InvalidObjectException("스트림 데이터가 필요합니다");
}
예제 2: Serializable을 안전하게 구현한 클래스
import java.io.InvalidObjectException;
import java.io.Serializable;
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private final String name;
private final int age;
// 불변식을 보장하는 생성자
public Person(String name, int age) {
// 불변식 검증 코드
if (name == null) throw new NullPointerException("이름은 null이 될 수 없습니다");
if (age < 0) throw new IllegalArgumentException("나이는 음수가 될 수 없습니다");
this.name = name;
this.age = age;
}
// 스트림 데이터가 없을 때 불변식이 깨지는 것을 방지
private void readObjectNoData() throws InvalidObjectException {
throw new InvalidObjectException("스트림 데이터가 필요합니다");
}
// readObject 메서드도 구현하여 역직렬화 시 불변식을 보장할 수 있음
// (아이템 88에서 다룰 내용)
// 일반적인 접근자 메서드
public String getName() { return name; }
public int getAge() { return age; }
}
예제 3: 상속용 Serializable 클래스 방어 코드
// 상속용으로 설계된 클래스지만 부득이하게 Serializable을 구현해야 하는 경우
public abstract class AbstractVehicle implements Serializable {
private static final long serialVersionUID = 1L;
private String make, model;
protected AbstractVehicle(String make, String model) {
this.make = make;
this.model = model;
}
// finalize 메서드를 final로 선언하여 하위 클래스가 재정의할 수 없게 함
// 이는 finalizer 공격을 방지
@Override
protected final void finalize() throws Throwable {
try {
// Finalizer 방어 로직
} finally {
super.finalize();
}
}
// 일반적인 접근자 메서드
public String getMake() { return make; }
public String getModel() { return model; }
}
// AbstractVehicle을 상속하는 클래스
// 부모 클래스가 Serializable을 구현하기 때문에 자동으로 Serializable을 상속
class Car extends AbstractVehicle {
private int numDoors;
public Car(String make, String model, int numDoors) {
super(make, model);
this.numDoors = numDoors;
}
public int getNumDoors() { return numDoors; }
}
🧱 내부 클래스에서의 Serializable
❌ 익명, 지역, 비정적 내부 클래스 → 직렬화 금지
✅ 정적 멤버 클래스는 예외적으로 허용 가능
컴파일러가 자동 생성한 필드와 참조가 존재하므로 예측 불가능한 직렬화 형태가 생성됨.
🧾 결론
Serializable
은 단순히 한 줄짜리 선언이 아니라 전체 API 설계에 영향을 미치는 결정이다.반드시 필요한 상황이 아니면 구현하지 않는 것이 원칙.
필요한 경우에는 철저한 방어와 테스트 전략을 수립하고, 클래스 변경 시마다 호환성 유지에 신경 써야 한다.
📌 자바 버전별 주요 변화
Java 4
readObjectNoData()
메서드 도입
Java 9
ObjectInputFilter
통한 역직렬화 필터링 지원 (참고: 아이템 85)
💬 느낀점
과거엔 단순히
implements Serializable
만 붙이면 끝이라 생각했지만, 이제는 이것이 클래스의 미래 유지보수, 보안, 구조까지 바꿔버릴 수 있는 중대한 설계 결정임을 깨달았다. 앞으로는 꼭 필요한 상황이 아니면 피하고, 구현 시에는serialVersionUID
, 불변식 보장,readObjectNoData()
등을 꼼꼼히 챙기자.
📋 Serializable 구현 결정 체크리스트
1. 필요성 검토
외부 시스템과 통신/저장 용도인가?
다른 직렬화 방식(JSON, Protobuf 등)으로 대체할 수 있는가?
2. 구현 결정 시
serialVersionUID
명시 여부readObjectNoData()
등 불변식 방어 로직 포함직렬화 형태 문서화 및 테스트 전략
3. 구조적 고려사항
상속 구조와 충돌하지 않는가?
finalizer 공격에 대비했는가?
내부 클래스는 사용하지 않았는가?
🧾결론
Serializable은 구현한다고 선언하기는 아주 쉽지만, 그것은 눈속임일 뿐이다. 한 클래스의 여러 버전이 상호작용할 일이 없고 서버가 신뢰할 수 없는 데이터에 노출될 가능성이 없는 등, 보호된 환경에서만 쓰일 클래스가 아니라면 Serializable 구현
은 아주 신중하게 이뤄져야 한다. 상속할 수 있는 클래스라면 주의사항이 더욱 많아진다.
Last updated