728x90
커스텀 직렬화 형태를 고려해보라.
먼저 고민해보고 괜찮다고 판단될 때만 기본 직렬화 형태를 사용하라.
- 기본 직렬화 형태는 유연성, 성능, 정확성 측면에서 신중히 고민한 후 합당할 때만 사용해야한다.
- 기본 직렬화 형태와 거의 같은 결과가 나올 경우에만 기본 형태를 사용해야 한다.
객체의 물리적 표현과 논리적 내용이 같다면 기본 직렬화 형태라도 무방하다.
- 성명은 논리적으로 이름, 성, 중간이름이라는 3개의 문자열로 구성되며, 앞 코드의 인스턴스 필드들은 이 논리적 구성요소를 정확히 반영했다.
- 기본 직렬화 형태가 적합하다고 결정했더라도 불변식 보장과 보안을 위해 readObject메서드를 제공해야 할 때가 많다.
public class Name implements Serializable {
/**
* 성, null이 아니여야 함.
* @serial
* */
private final String lastName;
/**
* 이름, null이 아니여야 함.
* @serial
*/
private final String firstName;
/**
* 중간이름, 중간이름이 없다면 null.
* @serial
*/
private final String middleName;
... // 나머지 코드는 생략
}
기본 직렬화 형태에 적합하지 않은 클래스
- 아래코드를 직렬화 형태를 사용하면 각 노드 양방향 연결 정보를 포함해 모든 엔트리를 철두철미하게 기록해야한다.
public final class StringList implements Serializable {
private int size = 0;
private Entry head = null;
private static class Entry implements Serializable {
String data;
Entry next;
Entry previous;
}
... // 나머지 코드 생략
}
객체의 물리적 표현과 논리적 표현의 차이가 클 때 기본 직렬화를 사용하면 안되는 이유
- 공개 API가 현재의 내부 표현 방식에 영구히 묶인다. 앞의 예에서 private 내부 클래스인 StringList.Entry가 공개 API가 되어 버린다.
- 너무 많은 공간을 차지할 수 있다. 내부 구현은 직렬화 형태에 포함할 가치가 없다.
- 시간이 너무 많이 걸릴 수 있다. 직렬화 로직은 객체 그래프의 위상에 관한 정보가 없어 그래프를 직접 순회해볼 수 밖에 없다.
- 스택 오버플로를 일으킬 수 있다. 그래프를 순회하는 과정에서 스택오버플로우를 일으킬 수 있다.
코드 수정
- transient사용하기 : 해시값처럼 JVM마다 달라지는 필드들은 transient를 붙여야 한다. 향후 릴리스에서 transient가 아닌 인스턴스 필드가 추가되더라도 상호 호환된다. 해당 객체의 논리적 상태와 무관한 필드라고 확실할 때만 transient 한정사를 생략해야 한다. ( 해시테이블의 경우 value에 대한 해시코드가 달라질 수 있다. )
- defaultReadObject, defaultWriteObject : 필드가 모두 transient여도 사용하도록 명세에 나와있다. 먼저 필드들을 초기값으로 만든다음 해당필드를 원하는 값으로 복원해야 한다.
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());
}
... // 나머지 코드는 생략
}
위의 코드는 속도도 빨라지고 스택오버플로우가 나타나지는 않는다. 유연성을 떨어지더라도 원래 객체를 불변식까지 포함해 제대로 복원해낸다는 점에서 정확하다고 할 수 있다.
동기화 메커니즘은 직렬화에도 적용해야 한다.
모든 메서드를 synchronized로 선언하여 스레드 안전하게 만든 객체에 기본 직렬화를 사용하려면 writeObject도 다음 코드처럼 synchronized로 선언해야 한다.
private synchronized void writeObject(ObjectOutputStream s)
throws IOException {
s.defaultWriteObject();
}
어떤 직렬화 형태를 택하든 직렬화 가능 클래스 모두에 직렬버전 UID를 명시적으로 부여하자.
UID를 명시하면 속도가 빨라지고 없으면 이 값을 생성하느라 복잡한 연산을 수행한다. 구버전으로 직렬화된 인스턴스들과의 호환성을 끊으려는 경우를 제외하고는 직렬 버전 UID를 절대 수정하지 말자.
private static final long serialVersionUID = <무작위로 고른 long 값>;
readObject 메서드는 방어적으로 작성하라.
물리적 표현과 논리적 표현이 부합하더라도 불변식을 보장하지 못할 수 있다. readObject메서드가 실질적으로 또 다른 public생성자이기 때문이다.
아래코드는 Period클래스의 불변식을 깨뜨리기 때문에 잘못된 결과를 내뱉는다.
public class BogusPeriod {
public static final byte[] serializedForm = {
(byte)0xac, (byte)0xed, 0x00, 0x05, 0x73, 0x72, 0x00, 0x06,
0x50, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x40, 0x7e, (byte)0xf8
};
public static void main(String[] args) {
Period p = (Period) deserialize(serializedForm);
System.out.println(p);
}
static Object deserialize(byte[] sf) {
try {
return new ObjectInputStream(
new ByteArrayInputStream(sf)).readObject();
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
readObject를 통해 해결하는 방법
- 유효성 검사를 통해 잘못된 역직렬화가 일어나는 것을 막을 수 있다.
- Override을 하면 안된다. 역직렬화 되기 전에 하위 readObject가 실행되어서 오작동으로 이어진다.
- 객체를 역직렬화할 때는 클라이언트가 소유해서는 안되는 객체 참조를 갖는 필드를 모두 반드시 방어적으로 복사해야 한다.
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {
s.defaultReadObject();
// 방어적 복사
start = new Date(start.getTIme());
end = new Date(end.getTime());
// 유효성 검증
if(start.compareTo(end) > 0)
throw new InvalidObjectException(start + " after " + end);
}
728x90
댓글