클래스 인스턴스를 직렬화하기 위해서는 단순히 마커 인터페이스인 Serializable을 구현하면 됩니다.
이처럼 너무 쉽게 적용할 수 있기 때문에 프로그래머가 특별히 신경 쓸 것이 없다는 오해가 생길 수 있지만 실제로는 직렬화를 적용하는 것은 아주 값비싼 일입니다.
Serializable을 구현할 경우 릴리즈한 뒤에는 수정하기 어려움
- 클래스가 Serializable 구현체일 경우 직렬화 형태인 바이트 스트림 인코딩도 하나의 공개 API가 됨
- 자바는 하위 호환성을 지원하는 것을 추구하기 때문에 직렬화된 클래스가 널리 쓰이는 라이브러리에 포함될 경우 릴리즈 이후에는 직렬화 형태도 영원히 지원해야 함
- 클래스의 private과 package-private 인스턴스 필드마저 API로 공개하는 꼴이 되어 객체지향의 핵심인 캡슐화가 깨짐
- 뒤늦게 클래스 내부 구현을 손보게 되면 기존의 직렬화 형태와 달라짐
- 한쪽은 구버전의 인스턴스를 직렬화하고 다른 쪽은 신버전 클래스로 역직렬화할 경우 실패를 맛보게 됨
- 원래의 직렬화 형태를 유지하면서 내부 표현을 바꿀 수도 있지만 어렵기도 하고 소스코드가 지저분해짐
- 따라서 직렬화 가능 클래스를 생성할 경우 길게 보고 감당할 수 있을 만큼 고품질의 직렬화 형태도 주의해서 함께 설계해야 함
직렬화/역직렬화 예제
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class MyClass implements Serializable { | |
private String data; | |
public MyClass(String data) { | |
this.data = data; | |
} | |
public String getData() { | |
return data; | |
} | |
} | |
@SpringBootTest | |
class MyClassTest { | |
private static String DATA = "data"; | |
private String base64MyClass; | |
@BeforeEach | |
void serializable() throws IOException { | |
MyClass myClass = new MyClass(DATA); | |
byte[] bytes; | |
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { | |
try (ObjectOutputStream oos = new ObjectOutputStream(baos)) { | |
oos.writeObject(myClass); | |
bytes = baos.toByteArray(); | |
} | |
} | |
base64MyClass = Base64.getEncoder().encodeToString(bytes); | |
System.out.println(base64MyClass); | |
} | |
@Test | |
void deserializable() throws IOException, ClassNotFoundException { | |
byte[] serializedMyClass = Base64.getDecoder().decode(base64MyClass); | |
MyClass myClass; | |
try (ByteArrayInputStream bais = new ByteArrayInputStream(serializedMyClass)) { | |
try (ObjectInputStream ois = new ObjectInputStream(bais)) { | |
myClass = (MyClass)ois.readObject(); | |
} | |
} | |
assertThat(myClass.getData()).isEqualTo(DATA); | |
} | |
} |

MyClass에 필드 추가할 경우 에러 발생
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class MyClass implements Serializable { | |
private String data; | |
private String newData; | |
public MyClass(String data) { | |
this.data = data; | |
} | |
public String getData() { | |
return data; | |
} | |
public String getNewData() { | |
return newData; | |
} | |
} | |
@Test | |
void deserializableOld() throws IOException, ClassNotFoundException { | |
byte[] serializedMyClass = Base64.getDecoder() | |
.decode( | |
"rO0ABXNyADZjb20udGlzdG9yeS5qYWltZW1pbi5lZmZlY3RpdmVqYXZhLmNoMTIuaXRlbTg2Lk15Q2xhc3Ozd6KUHsS+yAIAAUwABGRhdGF0ABJMamF2YS9sYW5nL1N0cmluZzt4cHQABGRhdGE="); | |
MyClass myClass; | |
try (ByteArrayInputStream bais = new ByteArrayInputStream(serializedMyClass)) { | |
try (ObjectInputStream ois = new ObjectInputStream(bais)) { | |
myClass = (MyClass)ois.readObject(); | |
} | |
} | |
assertThat(myClass.getData()).isEqualTo(DATA); | |
} |

부연 설명
- 예상한 결과는 newData가 null인 상태로 역직렬화
- 하지만 MyClass에 필드가 추가되면서 인코딩 결과가 달라짐
- 이에 따라 serialVersionUID도 달라지면서 역직렬화하면서 에러 발생
- serialVersionUID 를 클래스 내에 static fianl long 필드
- 명시적으로 선언하지 않으면 시스템이 런타임에 암호해시 함수(SHA-1)를 적용해 자동으로 클래스 안에 생성
- 명시적으로 serialVersionUID를 선언할 경우 위와 같은 에러는 피할 수 있음
- 하지만 변수의 자료형이 바뀌면서 기존의 데이터가 현재 버전의 데이터 타입과 호환되지 않을 경우 여전히 오류 발생
- 이처럼 초기 버전에서 Serializable 구현체를 선언할 경우 추후 버전에서 이전 버전에 영향을 끼치지 않으면서 수정하기 매우 어려워짐
Serializable 구현은 버그와 보안 구멍이 생길 위험이 높아짐
- 객체는 생성자를 사용해 생성하는 것이 기본이지만 직렬화는 생성자가 숨겨져있기 때문에 자바 언어의 기본 메커니즘을 우회하는 객체생성 기법
- 기본 방식을 따르든 재정의해 사용하든 역직렬화는 일반 생상자의 문제가 그대로 적용되는 "숨은 생성자"
- 기본 역직렬화 사용시 불변식이 깨지기 쉽고 허가되지 않은 접근에 쉽게 노출됨
MyClass 생성자에 제약 조건 추가
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public MyClass(String data) { | |
if (data.equals("data")) { | |
throw new IllegalArgumentException("data can't be data"); | |
} | |
this.data = data; | |
} |
앞서 작성한 테스트 코드를 그대로 돌리면 @BeforeEach에 존재하는 생성자 때문에 IllegalArgumentException 에러 발생

하지만 생성자에 제약조건이 걸리기 전에 만든 인코딩 문자열을 기반으로 역직렬화할 경우 숨은 생성자에 보안 구멍이 생겨 정상적으로 역직렬화가 됨

Serializable 구현은 해당 클래스의 새로운 버전을 릴리즈할 때마다 테스트할 것이 늘어남
- 직렬화 가능 클래스가 수정될 경우 신버전 인스턴스를 직렬화한 후 구버전에서도 역직렬화가 가능한지
- 그리고 구버전에서 직렬화한 후 신버전에서도 역직렬화 가능한지 확인해야 함
- 이 때문에 버전을 릴리즈할 수록 테스트 커버리지가 기하급수적으로 늘어남
- 커스텀 직렬화 형태를 잘 설계했을 경우 이러한 테스트 부담을 줄일 수 있음 (아이템 87, 90 참고)
Serializable 구현 여부는 가볍게 결정할 사안이 아님
- 객체를 전송하거나 저장할 때 반드시 자바 직렬화를 이용해야 하는 프레임워크용으로 만든 클래스라면 선택의 여지가 없지만 웬만해서는 아이템 85에서 언급한 크로스-플랫폼 구조화된 데이터 표현을 사용하는 것을 권장
- Serializable 구현에 따른 비용이 적지 않으므로 클래스 설계마다 직렬화 적용했을 때 이득과 비용을 비교해서 사용 여부를 판단해야 함
- 습관성으로 Serializable 마커 인터페이스 구현체로 만드는 것은 지양!
- 역사적으로 BigInteger 와 Instant 같은 '값' 클래스와 컬렉션 클래스들은 Serializable을 구현
- 쓰레드 풀처럼 '동작' 하는 객체를 표현하는 클래스 들은 대부분 Serializable을 구현하지 않음
상속용으로 설계된 클래스는 대부분 Serializable을 구현하면 안 되며 인터페이스도 대부분 Serializable을 확장해서는 안됨
- 상속용 클래스가 직렬화를 지원하지 않은 상태인데 하위 클래스에서 직렬화를 지원하려고 하면 부담이 늘어남
- 단, Serializable을 구현한 클래스만 지원하는 프레임워크를 사용하는 상황이라면 다른 방도가 없음
- ex) Throwable은 서버가 RMI를 통해 클라이언트로 예외를 보내기 위해 Serializable 구현
클래스의 인스턴스 필드가 직렬화와 확장이 모두 가능하다면 주의할 점
- 인스턴스 필드 값 중 불변식을 보장해야 할 것이 있다면 반드시 하위 클래스에서 finalize 메서드를 재정의하지 못하게 해야 함
- 아이템 8에서 언급한 finalizer attack을 방지하기 위해 finalize 메서드를 자신이 재정의하면서 final로 선언
내부 클래스는 직렬화를 구현하지 말아야 함
- 내부 클래스에는 바깥 인스턴스의 참조와 유효 범위 내 지역변수 값들을 저장하기 위해 컴파일러가 생성한 필드들이 자동으로 추가됨
- 익명 클래스와 지역 클래스의 이름을 짓는 규칙이 언어 명세에 나와 있지 않듯이 내부 클래스에 대한 기본 직렬화 형태는 불분명하기 때문에 직렬화를 구현하지 않는 것을 권장
- 단, 정적 멤버 클래스는 Serializable 구현체여도 됨
정리
- 직렬화 가능 클래스는 마커 인터페이스인 Serializable을 구현하기만 하면 돼서 쉬워 보이지만 실상은 그렇지 않음
- 한 클래스의 여러 버전이 상호작용할 일이 없고 서버가 신뢰할 수 없는 데이터에 노출될 가능성이 없는 등, 보호된 환경에서만 쓰일 클래스가 아니라면 Serializable 구현 여부는 신중히 판단하고 적용해야 됨
- 특히, 상속 가능 클래스로 설계했다면 더더욱 신중히 고려해야함
참고
이펙티브 자바
반응형
'JAVA > Effective Java' 카테고리의 다른 글
[아이템 88] readObject 메서드는 방어적으로 작성하라 (0) | 2024.05.18 |
---|---|
[아이템 87] 커스텀 직렬화 형태를 고려해보라 (2) | 2024.05.14 |
[아이템 85] 자바 직렬화의 대안을 찾으라 (1) | 2024.05.12 |
[아이템 84] 프로그램의 동작을 쓰레드 스케줄러에 기대지 말라 (0) | 2024.04.22 |
[아이템 83] 지연 초기화는 신중히 사용하라 (0) | 2024.04.11 |