Search
Duplicate
🏀

serialized 인스턴스 대신 직렬화 프록시를 고려하자

태그
Effective Java
Java
Serializable
공개여부
작성일자
2021/07/31
앞선 chapter 에서 버그와 보안 문제가 일어날 가능성이 있음을 확인하였는데 이 위험을 크게 줄일 방법이 serialization proxy pattern 이다.
proxy 를 만들 때 중첩 클래스와 같이 내부에 private static 으로 class 를 선언한다.
이때 내부 클래스의 생성자는 오로지 하나여야만 하고, 바깥 클래스를 매개변수로 받아야 한다.
이 생성자는 parameter 로 넘어온 instance 를 복사한다
일관성 검사, 방어적 복사도 필요없다.
외부 클래스, 내부 클래스 모두 implements Serializable 를 붙여야 한다.
Period 를 예시로 든다.
private static class SerializationProxy implements Serializable { private static final long serialVersionUID = -2785633062946028119L; private final Date start; private final Date end; SerializationProxy(Period period) { start = period.start; end = period.end; } }
Java
Period 용 proxy serializable 클래스
이 다음에 외부 클래스(Period)에 writeReplace 를 추가한다. 이건 그냥 복사해서 어디에서든지 사용해도 괜찮다.
private Object writeReplace() { return new SerializationProxy(this); }
Java
Period 에 추가해야 한다.
목적은 Period 의 직렬화를 호출하면 결국 내부의 SerializationProxy 를 반환하는 역할을 한다.
직렬화가 이뤄지기 전에 바깥 클래스의 instance 를 직렬화 proxy 로 변환한다
writeReplace 때문에 직렬화 시스템은 결코 바깥 클래스의 직렬화 instance 를 생성할 수 없다.
다음의 readObject 를 바깥 클래스에 추가하면 공격자가 불변식을 훼손하고자 하는 시도 역시 막을 수 있다
// 직렬화 proxy pattern private void readObject(ObjectInputStream stream) throws InvalidObjectException { throw new InvalidObjectException("proxy 가 필요합니다"); }
Java
마지막으로 바깥 클래스와 동일한 instance 를 반환하도록 readResolve 를 내부에 추가한다
private Object readResolve() { return new Period(start, end); }
Java
이 method 드는 공개된 API 만을 사용해 바깥 클래스의 instace 를 생성한다.
proxy pattern이 강력한 이유는 readResolve 때문인데 이렇게 만들면 직렬화의 문제 되는점을 상당부분 제거한다. 즉, 일반 instance 를 만드는 방법과 동일해진다
그러면 생성자에 있는 validation 로직을 거치는 것과 동일하기 때문에 객체 생성의 다른 방법을 고려하지 않아도 된다.
방어적 복사와 같이 가짜 byte stream 공격과 내부 필드 탈취 공격을 proxy 수준에서 차단한다
private static class SerializationProxy implements Serializable { private static final long serialVersionUID = -2785633062946028119L; private final Date start; private final Date end; SerializationProxy(Period period) { start = period.start; end = period.end; } private Object readResolve() { return new Period(start, end); } }
Java
이러한 proxy 를 구상한다면 앞서 final 필드를 선언하지 못했던 문제가 없어진다.
item 36의 EnumSet 에도 이러한 proxy pattern 이 있다.
EnumSet 에 있는 proxy pattern
private static class SerializationProxy<E extends Enum<E>> implements java.io.Serializable { private static final Enum<?>[] ZERO_LENGTH_ENUM_ARRAY = new Enum<?>[0]; /** * The element type of this enum set. * * @serial */ private final Class<E> elementType; /** * The elements contained in this enum set. * * @serial */ private final Enum<?>[] elements; SerializationProxy(EnumSet<E> set) { elementType = set.elementType; elements = set.toArray(ZERO_LENGTH_ENUM_ARRAY); } /** * Returns an {@code EnumSet} object with initial state * held by this proxy. * * @return a {@code EnumSet} object with initial state * held by this proxy */ @SuppressWarnings("unchecked") private Object readResolve() { // instead of cast to E, we should perhaps use elementType.cast() // to avoid injection of forged stream, but it will slow the // implementation EnumSet<E> result = EnumSet.noneOf(elementType); for (Enum<?> e : elements) result.add((E)e); return result; } private static final long serialVersionUID = 362491234563181265L; }
Java
직렬화 proxy pattern 에 두가지 한계가 있다.
1.
client 가 멋대로 확장할 수 있는 class에는 적용할 수 없다.
2.
객체 그래프에 순환이 있는 클래스에 적용할 수 없다.
이러한 객체의 method 를 serializable 을 적용하면 proxy의 readResolve 안에서 호출할 경우 ClassCastException 이 발생한다.
마지막으로 직렬화 proxy pattern 의 안정성의 대가가 따른다.
저자의 컴퓨터에서 방어적 복사에 비해 14% 정도의 성능 하락이 있다.
TOP