아이템 90. 직렬화된 인스턴스 대신 직렬화 프록시 사용을 검토하라. - ksw6169/effective-java GitHub Wiki
개요
Serializable
을 구현하는 순간 생성자 이외의 방법으로 객체를 생성할 수 있게 된다.- 따라서 버그와 보안 문제가 일어날 가능성이 커진다.
- 하지만 이러한 위협을 크게 줄여줄 기법이 있는데, 바로 직렬화 프록시 패턴(serialization proxy pattern)을 사용하는 것이다.
직렬화 프록시 패턴(serialization proxy pattern)
- 직렬화 대상 클래스의 논리적 상태를 정밀하게 표현하는 중첩 클래스를 설계해 private static으로 선언한다. 이 중첩 클래스가 바로 직렬화 대상 클래스의 직렬화 프록시다.
- 중첩 클래스의 생성자는 단 하나여야 하며, 직렬화 대상 클래스를 매개변수로 받아야 한다. 이 생성자는 단순히 인수로 넘어온 인스턴스의 데이터를 복사하며, 일관성 검사나 방어적 복사도 필요 없다.
- 직렬화 대상 클래스와 직렬화 프록시 모두
Serializable
을 구현한다고 선언해야 한다.
// Period 클래스용 직렬화 프록시
private static class SerializationProxy implements Serializable {
private final Date start;
private final Date end;
SerializationProxy(Period p) {
this.start = p.start;
this.end = p.end;
}
// 아무 값이나 상관 없다.
private static final long serialVersionUID = 234098243823485285L;
}
- 다음으로 직렬화 대상 클래스에
writeReplace
메소드를 추가한다.
// 직렬화 프록시 패턴용 writeReplace 메소드
private Object writeReplace() {
return new SerializationProxy(this);
}
- 이 메소드는 자바의 직렬화 시스템이 직렬화 대상 클래스의 인스턴스 대신
SerializationProxy
의 인스턴스를 반환하게 한다. 달리 말해, 직렬화가 이뤄지기 전에 직렬화 대상 클래스의 인스턴스를 직렬화 프록시로 변환해준다. writeReplace
덕분에 직렬화 시스템은 직렬화 대상 클래스의 직렬화된 인스턴스를 생성해낼 수 없다.- 만약 공격자가 불변식을 훼손하고자 시도한다면 다음의
readObject
메소드를 직렬화 대상 클래스에 추가하면 이 공격을 가볍게 막아낼 수 있다.
private void readObject(ObjectInputStream stream) throws InvalidObjectException {
throw new InvalidObjectException("프록시가 필요합니다.");
}
- 마지막으로 직렬화 대상 클래스와 논리적으로 동일한 인스턴스를 반환하는
readResolve
메소드를SerializationProxy
클래스에 추가한다. 이 메소드는 역직렬화 시에 직렬화 시스템이 직렬화 프록시를 다시 직렬화 대상 클래스의 인스턴스로 변환하게 해준다. readResolve
메소드는 공개된 API만을 사용해 바깥 클래스의 인스턴스를 생성하는데, 이 패턴의 장점이 여기에 있다. 직렬화는 생성자를 이용하지 않고도 인스턴스를 생성하는 기능을 제공하는데, 이 패턴은 직렬화의 이런 언어도단적 특성을 상당 부분 제거한다.- 즉, 일반 인스턴스를 만들 때와 똑같은 생성자, 정적 팩토리 혹은 다른 메소드를 사용해 역직렬화된 인스턴스를 생성하는 것이다. 따라서 역직렬화된 인스턴스가 해당 클래스의 불변식을 만족하는지 검사할 또 다른 수단을 강구하지 않아도 된다.
// Period.SerializationProxy용 readResolve 메소드
private Object readResolve() {
return new Period(start, end); // public 생성자를 사용한다.
}
직렬화 프록시 패턴의 장점
- 가짜 바이트 스트림 공격과 내부 필드 탈취 공격을 프록시 수준에서 차단해준다.
- 직렬화 대상 클래스의 필드를 final로 선언해도 되므로 진정한 불변으로 만들 수 있다.
- 역직렬화 때 유효성 검사를 수행하지 않아도 된다.
- 역직렬화한 인스턴스와 직렬화된 인스턴스의 클래스가 달라도 정상 작동한다.
직렬화 프록시 패턴의 한계
1. 클라이언트가 멋대로 확장할 수 있는 클래스에는 적용할 수 없다.
2. 객체 그래프에 순환이 있는 클래스에도 적용할 수 없다.
- 이런 객체의 메소드를 직렬화 프록시의
readResolve
안에서 호출하려 하면ClassCastException
이 발생할 것이다. 직렬화 프록시만 가졌을 뿐 실제 객체는 아직 만들어진 것이 아니기 때문이다.
3. 직렬화 프록시 패턴은 방어적 복사 때보다 속도가 느리다.
- 저자 기준 14%가 느리다.
핵심 정리
- 제3자가 확장할 수 없는 클래스라면 가능한 한 직렬화 프록시 패턴을 사용하자.
- 이 패턴이 아마도 중요한 불변식을 안정적으로 직렬화해주는 가장 쉬운 방법일 것이다.
참고 자료
- Effective Java 3/E