• 객체 직렬화란 자바가 객체를 바이트 스트림으로 인코딩하고 그 바이트 스트림으로부터 객체를 재구성하는 메커니즘을 의미
  • 이번장은 직렬화가 품고 있는 위험과 그 위험을 최소화하는 방법에 집중

아이템 85. 자바 직렬화의 대안을 찾으라

  • 직렬화의 근본적인 문제는 공격범위가 너무 넓고 지속적으로 더 넓어저 방어하기가 어렵다는 점
    • ObjectInputStream의 readObject 메서드를 호출하면서 객체 그래프가 역직렬화 되기 때문
  • readObject 메서드는 클래스패스 안의 거의 모든 타입의 객체를 만들어낼수 있는 사실상 마법같은 생성자
  • 바이트 스트림을 역직렬화하는 과정에서 이 메서드는 그 타입들 안의 모든 코드를 수행할 수 있음, 그 타입들의 코드 전체가 공격 범위에 들어감
  • 직렬화를 회피하는 가장 좋은 방법은 아무것도 역직렬화하지 않는것
  • 여러분이 작성하는 새로운 시스템에서 자바 직렬화를 사용할 이유는 전혀없음
  • 객체와 바이트 시퀀스를 변환해주는 다른 메커니듬들이 존재
    • JSON
    • 프로토콜버퍼
  • 신회할 수 없는 데이터는 절대 역직렬화해서는 안됨

아이템 86. Serializable을 구현할지는 신중히 결정하라

  • Serializable을 구현하면 릴리스한 뒤에는 수정하기 어려움
    • 커스텀 직렬화 형태를 사용하지 않고 자바의 기본 방식을 사용한 직렬화 형태는 최소 적용당시 클래스의 내부 구현방식에 영원히 묶여버림
  • Serializable을 구현하면 버그와 보안 구멍이 생길 위험이 높아짐
  • Serializable을 구현하면 해당 클래스의 신버전을 릴리스할때 테스트할 것이 늘어남
  • Serializable 구현 여부는 가볍게 결정한 사안이 아님
  • 상속용으로 설계된 클래스는 대부분 Serializable를 구현하면 안되며, 인터페이스도 대부분 Serializable을 확장해서는 안됨
  • 내부 클래스는 직렬화를 구현하면 안됨

아이템 87. 커스텀 직렬화 형태를 고려해보라

  • 먼저 고민해보고 괜찮다고 판단될 때만 기본 직렬화 형태를 사용하라
  • 객체의 물리적 표현과 논리적 내용이 같아면 기본 직렬화 형태라도 무방

아이템 88. readObject 메서드는 방어적으로 작성하라

  • readObject 메서드를 작성할 때는 언제나 public 생성자를 작성하는 자세로 임해야함
  • 어떤 바이트 스트림이 넘어오더라도 유효한 인스턴스를 만들어 내야함
  • 다음은 readObject를 작성하는 지침
    • private이어야 하는 객체 참조 필드는 방어적 복사 필요
    • 모든 불변식을 검사, 어긋나는 경우 InvalidObjectException을 던짐
    • 역직렬화 후 객체 그래프 전체 유효성을 검사해야한다면 ObjectInputValidation 인터페이스 사용필요
    • 직접적이든 간접적이든 재정의 할 수 있는 메서드는 호출하면 안됨

아이템 89. 인스턴스 수를 통제해야 한다면 readResolve보다는 열거 타입을 사용하라

  • 아이템3에서 싱글턴 패턴이 바깥에서 생성자를 호출하지 못하게 막는 방식으로 인스턴스가 오직 하나만 생성된다는것을 보여줌
  • 하지만 implements Serializable을 추가하는 순간 싱글턴이 아님
  • 기본 직렬화를 사용하지 않거나(아이템 87) 명시적인 readObject를 제공하더라도(아이템88) 소용없음
  • 어떤 readObject를 사용하던 이 클래스가 초기화 될 때 만들어진 인스턴스와는 별개인 인스턴스를 반환함
  • readResolve를 사용하면 readObject가 만들어낸 인스턴스대신 다른것으로 대체가능, 즉 싱글턴을 유지가능
  • 역직열화한 객체의 클래스가 readResolve를 적절히 정의해뒀다면, 역직렬화후 생성된 객체가 아닌 원하는 객체의 참조를 반환가능
private Object readResolve() {
  return INSTANCE;
}
  • 좀더 깊게 생각해보면 오직 하나의 인스턴스만 유지하므로 직렬화할때 객체의 맴버를 포함할 필요가 없음
    • 그러므로 모든 필드를 transient로 선언해야함
    • 그렇지 않으면 readResolve 메서드가 수행되기전에 역직렬화된 객체의 참조를 공격할 여지가 존재함
  • 맴버 보호하기 위해 맴버를 transient로 선언하는 방법 보다는 원소 하나짜리 열거 타입으로 바꾸는 방법이 더 나은 선택임
  • readResolve 메서드의 접근성은 매우 중요함
    • final 클래스에서라면 readResolve 메서드는 private

아이템 90. 직렬화된 인스턴스 대신 직렬화 프록시 사용을 검토하라

  • 이전에서 언급한것과 같이 Serializable을 구현하면 언어의 정상 메커니즘인 생성자 이외의 방법으로 인스턴스를 생성할 수 있게 됨
    • 버그와 보안 문제가 발생할 가능성이 커진다는 것
  • 이 위험을 크게 줄여주는 기법으로 직렬화 프록시 패턴(serialization proxy patter)이 존재
  • 먼저 바깥 클래스의 논리적 상태를 정밀하게 표현하는 중첩 클래스를 설계해 private static으로 선언
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 = 324234234234242L;
}
  • 다음으로 바깥 클래스에 다음의 writeReplace 메서드를 추가
  • 이 메서드가 자바의 직렬화 시스템이 바깥 클래스의 인스턴스 대신 SerializationProxy의 인스턴스를 반환하는 역할을 함
  • 즉 직렬화가 이루어지기전 바깥 클래스의 인스턴스를 직렬화 프록시로 변환해줌
private Object writeReplace() {
  return new SerializationProxy(this);
}
  • 다음의 readObject 메서드를 바깥 클래스에 추가하면 불변식을 훼손하는 공격을 가볍게 막아냄
private void readObject(ObjectInputStream stream) throws InvalidObjectException {
  throw new InvalidObjectException("프록시가 필요합니다.");
}
  • 마지막으로 마깥 클래스와 논리적으로 동일한 인스턴스를 반환하는 readResolve 메서드를 SerializationProxy에 추가