반응형
이 포스팅에서 작성하는 내용은 EFFECTIVE JAVA(이펙티브자바) 에서 발췌하였습니다.
아이템 90. 직렬화된 인스턴스 대신 직렬화 프록시 사용을 검토하라
- Serializable을 구현하면, 언어의 정상 메커니즘인 생성자 이외의 방법으로 인스턴스를 생성할 수 있다.
- 버그와 보안 문제가 일어날 가능성이 커진다는 뜻이지만, 이 위험을 크게 줄여줄 기법이 있는데 이를 ‘직렬화 프록시 패턴’ 이라고 한다.
직렬화 프록시 패턴
- 바깥 클래스의 논리적 상태를 정밀하게 표현하는 중첩 클래스를 설계해 private static으로 선언한다.
- 이 중첩 클래스가 바로 바깥 클래스의 직렬화 프록시이다.
- 중첩 클래스의 생성자는 단 하나여야 하며, 바깥 클래스를 매개변수로 받아야 한다.
- 이 생성자는 단순히 인수로 넘어온 인스턴스의 데이터를 복사한다. (일관성 검사나 방어적 복사도 필요 없다.)
- 설계상, 직렬화 프록시의 기본 직렬화 형태는 바깥 클래스의 직렬화 형태로 쓰기에 이상적이다.
- 바깥 클래스와 직렬화 프록시 모두 Serializable을 구현한다고 선언해야 한다.
ex1) Period 클래스용 직렬화 프록시
class Period implements Serializable {
private final Date start;
private final Date end;
public Period(Date start, Date end) {
this.start = start;
this.end = end;
}
private static class SerializationProxy implements Serializable {
private static final long serialVersionUID = 1231243242L;
private final Date start;
private final Date end;
public SerializationProxy(Period p) {
this.start = p.start;
this.end = p.end;
}
private Object readResolve() {
return new Period(start, end); // public 생성자를 사용한다.
}
}
// 직렬화 프록시 패턴용 writeReplace 메서드
private Object writeReplace() {
return new SerializationProxy(this);
}
// 직렬화 프록시 패턴용 readObject 메서드
private void readObject(ObjectInputStream stream) throws InvalidObjectException {
throw new InvalidObjectException("프록시가 필요합니다.");
}
}
- 아이템 50에서 작성했고 아이템 88에서 직렬화한 Period 클래스를 예로 든 것이다.
- Period는 아주 간단해 직렬화 프록시도 바깥 클래스와 같은 필드로 구성되었다.
- writeReplace Method
- 바깥 클래스의 writeReplace 메서드는 범용적이니 직렬화 프록시를 사용하는 모든 클래스에 그대로 복사해 쓰면 된다.
- 자바의 직렬화 시스템이 바깥 클래스의 인스턴스 대신 SerializationProxy의 인스턴스를 반환하게 하는 역할을 한다.
- 직렬화가 이뤄지기 전에 바깥 클래스의 인스턴스를 직렬화 프록시로 변환해준다.
- writeReplace 로 인해 직렬화 시스템은 결코 바깥 클래스의 직렬화된 인스턴스를 생성해낼 수 없다.
- readObject Mehtod
- 바깥 클래스와 논리적으로 동일한 인스턴스를 반환하는 readResolve 메서드를 SerializationProxy 클래스에 추가한다.
- 이 메서드는 역직렬화 시에 직렬화 시스템이 직렬화 프록시를 다시 바깥 클래스의 인스턴스로 변환하게 해준다.
- 공개된 API만을 사용해 바깥 클래스의 인스턴스를 생성한다.
- 직렬화는 생성자를 사용하지 않고도 인스턴스를 생성하는 기능을 제공하는데, 이 패턴은 직렬화의 특성을 상당 부분 제거한다.
- 즉, 일반 인스턴스를 만들 때와 똑같은 생성자, 정적 팩터리, 혹은 다른 메서드를 사용해 역직렬화된 인스턴스를 생성하는 것이다.
- 따라서, 역직렬화된 인스턴스가 해당 클래스의 불변식을 만족하는 지 검사할 다른 수단을 찾지 않아도 된다.
직렬화 프록시 패턴의 장점
- 방어적 복사처럼 가짜 바이트 스트림 공격과 내부 필드 탈취 공격을 프록시 수준에서 차단한다.
- 직렬화 프록시는 Period의 필드를 final로 선언해도 되므로 Period 클래스를 진정한 불변으로 만들 수도 있다.
- 역직렬화 때 유효성 검사를 수행하지 않아도 된다.
- 역직렬화한 인스턴스와 원래의 직렬화된 인스턴스의 클래스가 달라도 정상 작동한다.
ex 2) EnumSet의 직렬화 프록시
private static class SerializationProxy <E extends Enum<E>> implements Serializable {
// 이 EnumSet의 원소 타입
private final Class<E> elementType;
// 이 EnumSet 안의 원소들
private final Enum<?>[] elements;
SerializationProxy(EnumSet<E> set) {
elementType = set.elementType;
elements = set.toArray(new Enum<?>[0]);
}
private Object readResolve() {
EnumSet<E> result = EnumSet.noneOf(elementType);
for(Enum<?> e : elements)
result.add((E)e);
return result;
}
private static final long serialVersionUID = 4235412353412L;
}
직렬화 프록시 패턴의 한계
- 클라이언트가 멋대로 확장할 수 있는 클래스에는 적용할 수 없다.
- 객체 그래프에 순환이 있는 클래스에는 적용할 수 없다.
- 이런 객체의 메서드를 직렬화 프록시의 readResolve 안에서 호출하려 하면 ClassCastException이 발생한다.
- 직렬화 프록시만 가졌을 뿐 실제 객체는 아직 만들어 진것이 아니기 때문이다.
- 성능이 느려질 수도 있다.
정리
- 제 3자가 확장할 수 없는 클래스라면 가능한 한 직렬화 프록시 패턴을 사용하자
- 이 패턴이 중요한 불변식을 안정적으로 직렬화해주는 쉬운 방법일 것이다.
반응형
'BE > Java' 카테고리의 다른 글
[Junit 5] 테스트 이름 설정, Assertion (0) | 2023.02.09 |
---|---|
[Junit 5] Junit5 들어가기 (0) | 2023.02.07 |
[Effective Java] 아이템 89. 인스턴스 수를 통제해야 한다면 readResolve 보다는 열거 타입을 사용하라 (0) | 2023.01.04 |
[Effective Java] 아이템 88. readObject 메서드는 방어적으로 작성하라 (0) | 2023.01.03 |
[Effective Java] 아이템 87. 커스텀 직렬화 형태를 고려해보라 (1) | 2023.01.02 |