-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Labels
Description
서론
- Serializable을 implement 하면 직렬화가 가능한 클래스를 만들 수 있다.
- 그렇지만, 기본 직렬화가 커버하지 못하는 것이 있어 직접 private readObject, private writeObject를 구현해야 하는 경우가 있다.
- 먼저 고민해보고 괜찮다고 판단될 때만 기본 직렬화 형태를 사용하라.
기본 직렬화가 적합한 경우
public class Name implements Serializable {
public Name(String lastName, String firstName, String middleName) {
this.lastName = lastName;
this.firstName = firstName;
this.middleName = middleName;
}
/**
* 성, null이 아니어야 한다
* @serial
*/
private final String lastName;
/**
* 이름, null이 아니어야 한다.
* @serial
*/
private final String firstName;
/**
* 중간이름, 중간이륾이 없다면 null
* @serial
*/
private final String middleName;
}- 객체의 물리적 표현과 논리적 내용이 같다면 기본 직렬화 형태라도 무방하다.
@serial태그로 기술한 내용은 API 문서에서 직렬화 형태를 설명하는 특별한 페이지에 기록된다.
직렬화가 적합하지 않은 경우
public class StringList implements Serializable {
private final int size = 0;
private Entry head = null
private static class Entry implements Serializable {
String data;
Entry next;
Entry previous;
}
}- 논리적으로 이 클래스는 일련의 문자열을 표현하는데 물리적으로는 문자열들을 이중 연결 리스트로 연결했다.
- 이 클래스에 기본 직렬화 형태를 사용하면 각 노드의 양방향 연결 정보를 포함해 모든 엔트리(Entry)를 철두철미하게 기록한다.
- 객체의 물리적 표현과 논리적 표현의 차이가 클 때 기본 직렬화를 사용하면 크게 네가지 문제가 생긴다
- 공개 API가 현재의 내부 표현 방식에 영구히 묶인다.
- 앞의 코드에서 StringList.Entry가 공개 API가 되는데 다음 릴리즈에서 내부 표현 방식을 바꾸더라도 StringList는 여전히 연결 리스트로 표현된 입력도 처리할 수 있어야 한다.
- 너무 많은 공간을 차지할 수 있다.
- 기본 직렬화는 객체가 포함한 데이터들과 그 객체에서부터 시작해 접근 할 수 있는 모든 객체를 담아내며 객체들이 연결된 위상까지 기술한다.
- 앞 예시의 직렬화 형태는 연결 리스트의 모든 엔트리와 연결 정보까지 기록하기 때문에 직렬화 형태가 매우 커져 디스크에 저장하거나 네트워크로 전송하는 속도가 느려진다.
- 시간이 너무 많이 걸릴 수 있다.
- 직렬화 로직은 객체 그래프의 위상에 관한 정보가 없으니 그래프를 직접 순회해볼 수밖에 없다.
- 스택 오버플로우를 일으킬 수 있다.
- 기본 직렬화 과정은 객체 그래프를 재귀 순회하는데, 이로인해 자칫 스택오버플로우를 일으킬 수 있다.
- 공개 API가 현재의 내부 표현 방식에 영구히 묶인다.
합리적인 커스텀 직렬화 형태를 갖춘 StringList
public final class StringList implements Serializable {
private transient int size = 0;
private transient Entry head = null;
// 이제는 직렬화되지 않는다.
private static class Entry {
String data;
Entry next;
Entry previous;
}
// 지정한 문자열을 이 리스트에 추가한다.
public final void add(String s) {...}
/**
* 이 {@code StringList} 인스턴스를 직렬화한다.
*
* @serialData 이 리스트의 크기(포함된 문자열의 개수)를 기록한 후
* ({@code int}), 이어서 모든 원소를(각각은 {@code String})
* 순서대로 기록한다.
*/
private void writeObject(ObjectOutputStream s) throws IOException {
//기본 직렬화를 수행한다.
s.defaultWriteObject();
s.writeInt(size);
// 커스텀 역직렬화를 수행한다.
// 모든 원소를 올바른 순서로 기록한다.
for (Entry e = head; e != null; e = e.next)
s.writeObject(e.data);
}
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
//기본 역직렬화를 수행한다.
s.defaultReadObject();
int numElements = s.readInt();
// 커스텀 역직렬화 부분
// 모든 원소를 읽어 이 리스트에 삽입한다.
for(int i = 0; i < numElements; i++) {
add((String) s.readObject());
}
}
}readObject , wirteObject- 자바에서 직렬화 또는 역직렬화 과정에서 별도의 처리가 필요할 때는 readObject와 writeObject 메서드를 클래스 내부에 선언해주면 된다.
- 해당 클래스가 Serializable 인터페이스를 구현한 클래스여야 한다.
- readObject, writeObject 메서드의 접근 지정자를 private으로 선언해야지만 자동으로 호출된다. (https://madplay.github.io/post/what-is-readobject-method-and-writeobject-method)
- 자바에서 직렬화 또는 역직렬화 과정에서 별도의 처리가 필요할 때는 readObject와 writeObject 메서드를 클래스 내부에 선언해주면 된다.
- readObject, writeObject 메서드 각각 가장 먼저
defaultWriteObject, defaultReadObject메서드를 호출한다.- 직렬화 명세에서 이 작업을 무조건 먼저 하라고 요구하고 이렇게 해야 향후 릴리즈에서 transient가 아닌 인스턴스 필드가 추가되더라도 상호(상,하위 모두) 호환되기 때문이다.
- 신버전에서 인스턴스를 직렬화 한 후 구버전으로 역직렬화하면 새로 추가된 필드들은 무시될 것.
- 이때, 구버전 readObject 메서드에서 defaultReadObject를 호출하지 않는다면 역직렬화 시 StreamCorruptedException이 발생할 것.
- 기본 직렬화를 수용하든 수용하지 않든 defaultWriteObject를 호출하면 transient가 아닌 모든 필드가 직렬화 대상이다.
- 직렬화 명세에서 이 작업을 무조건 먼저 하라고 요구하고 이렇게 해야 향후 릴리즈에서 transient가 아닌 인스턴스 필드가 추가되더라도 상호(상,하위 모두) 호환되기 때문이다.
SerialVersionUID
private static final long serialVersionUID = 8540816269016850655L;- 직렬화를 할때에는 serialVersionUID를 직렬화된 데이터에 같이 저장, 직렬화된 객체를 자바 객체로 다시 읽어들일 때는 그 값을 체크하는 용도로 사용.
- 어떤 직렬화 형태를 택하든 직렬화 가능 클래스 모두에 직렬버전 UID를 명시하자.
- 이렇게 하면 직렬 버전 UID가 일으키는 잠재적인 호환성 문제가 사라진다.
- 직접 작성하면 성능도 조금 빨라지는데 직렬 버전 UID를 명시하지 않으면 런타임에 이 값을 생성하느라 복잡한 연산을 수행하기 때문이다.
- 직접 명시하지 않으면 JVM에서 내부 알고리즘에 따라 자동으로 작성함
- 새로운 클래스라면 어떤 long 을 선언해도 상관없다, 생각나는 값을 할당해도 된다.
- 주의점
- 구버전으로 직렬화된 인스턴스와 호환성을 유지한 채 신버전을 릴리즈 하려면 구버전에서 사용한 UID 값을 그대로 사용해야 한다.
- 기본 버전 클래스와의 호환성을 끊고 싶다면 단순히 직렬 버전 UID의 값을 바꿔주면 된다.
- 이렇게 하면 기존 버전의 직렬화된 인스턴스를 역직렬화 할 때 InvalidClassException을 던진다.
- 구버전으로 직렬화된 인스턴스들과의 호환성을 끊으려는 경우를 제외하고는 직렬 버전 UID를 절대 수정하지 말자.
정리
- 클래스를 직렬화하기로 했다면 어떤 직렬화 형태를 사용할지 심사숙고하자.
- 자바의 기본 직렬화 형태는 객체를 직렬화한 결과가 해당 객체의 논리적 표현에 부합할 때만 사용하고, 그렇지 않으면 객체를 적절히 설명하는 커스텀 직렬화 형태를 고려해라.
- 한번 공개된 메서드는 향후 릴리즈에서 제거할 수 없듯이, 직렬화 형태에 포함된 필드도 마음대로 제거할 수 없다.
- 직렬화 호환성을 유지하기 위해 영원히 지원해야 하는 것.