개인룸/도윤

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

UroJem 2023. 11. 16. 21:07

방어적 복사를 사용하는 불변 클래스

 

아이템 50에서 불변인 날짜 범위 클래스를 만드는데 불변식을 지키고 불변을 유지하기 위해 생성자와 접근자 메서드에서  가변객체인 Date 객체를 방어적으로 복사하느라 코드가 상당히 길어졌다.

이 클래스를 직렬화 하기로 했을 때 Period 객체의 물리적 표현이 논리적 표현과 부합하므로 기본 직렬화 형태를 사용한다고 하면 이 클래스의 주요한 불변식을 더는 보장하지 못하게 된다.

문제는 readObject 메서드가 바이트 스트림을 받는 실질적 또다른 public 생성자이기 때문인데 불변식을 깨뜨릴 의도로 임의 생성한 바이트 스트림을 건네면 정상적인 생성자로 만들어낼 수 없는 객체를 생성해낼 수 있다.

 

 

실행하면 Period가 ClassNotFoundException가 나는데..

모쪼록 원래 결과 값은

Fri Jan 01 12:00:00 PST 1999 - Sun Jan 01 12:00:00 PST 1984

Period를 직렬화 할 수 있도록 선언한 것만으로도 시작날짜가 끝나는 날짜보다 더 미래인 클래스의 불변식을 깨뜨리는 객체를 만들 수 있다.

이 문제를 고치려면 Period의 readObject 메서드가 defaultReadObject를 호출한 다음 역직렬화된 객체가 유효한지 검사해야 한다.

 

Period 객체에 readObject 메서드 추가

이상의 작업으로 공격자가 허용되지 않는 Period 인스턴스를 생성하는 일을 막을 수 있지만

정상 Period 인스턴스에서 시작된 바이트 스트림 끝에 private Date 필드로의 참조를 추가하면 가변 Period 인스턴스를 만들어낼 수 있다.

 

공격자는 ObjectInputStream에서 Period 인스턴스를 읽은 후 스트림 끝에 추가된 이 '악의적인 객체 참조'를 읽어 Period 객체의 내부정보를 얻을 수 있다.

이제 이 참조로 얻은 Date 인스턴스들을 수정할 수 있으니 Period 인스턴스는 더는 불변이 아니게 된다.

 

이 예에서 Period 인스턴스는 불변식을 유지한 채 생성됐지만 의도적으로 내부의 값을 수정할 수 있었다.

이 문제의 근원은 Period의 readObject 메서드가 방어적 복사를 충분히 하지 않은 데 있다.

객체를 역직렬화할 때는 클라이언트가 소유해서는 안 되는 객체 참조를 갖는 필드를 모두 반드시 방어적으로 복사해야한다.

 

 

이 readObject 메서드를 사용하려면 start와 end 필드에서 final 한정자를 제거해야한다.

 

다시 제대로 출력되는 실행 결과

 

 

 

기본 readObject 메서드를 사용 판단 방법

transient 필드를 제외한 모든 필드의 값을 매개변수로 받아 유효성 검사없이 필드에 대입하는 public 생성자를 추가해도 괜찮은가? 답이 "아니오"라면 커스텀 readObject 메서드를 만들어 모든 유효성 검사와 방어적 복사를 수행해야 한다.

혹은 프록시 패턴을 사용하는 방법도 있다. 이 패턴은 역직렬화를 안전하게 만드는 데 필요한 노력을 상당히 경감해 준다.

 

 

final이 아닌 직렬화 가능 클래스라면 readObject와 생성자의 공통점이 하나 더 있다. 마치 생성자처럼 readObject 메서드도 재정의 가능 메서드를 호출해서는 안된다. 해당 메서드가 재정의 되면, 하위 클래스의 상태가 완전히 역직렬화되기 전에 하위 클래스에서 재정의된 메서드가 실행되어 프로그램 오작동으로 이어질 것이다.

 

핵심정리

readObject 메서드를 작성할 때는 언제나 public 생성자를 작성하는 자세로 임해야 한다. readObject는 어떤 바이트 스트림이 넘어오더라도 유효한 인스턴스를 만들어내야 한다. 바이트 스트림이 진짜 직렬화된 인스턴스라고 가정해서는 안된다. 이번 아이텀에서는 기본 직렬화 형태를 사용한 클래스를 예로 들었지만 커스텀 직렬화를 사용하더라도 모든 문제가 그대로 발생할 수 있다. 이어서 안전한 readObject 메서드를 작성하는 지침을 요약해 보았다.

  • private 이어야 하는 객체 참조 필드는 각 필드가 가리키는 객체를 방어적으로 복사하라. 불변 클래스 내의 가변 요소가 여기 속한다.
  • 모든 불변식을 검사하여 어긋나는게 발견되면 InvalidObjectException을 던진다. 방어적 복사 다음에는 반드시 불변식 검사가 뒤따라야 한다.
  • 역직렬화 후 객체그래프 전체의 유효성을 검사해야 한다면 ObjectInputValidation 인터페이스를 사용하라
  • 직접적이든 간접적이든, 재정의할 수 있는 메서드는 호출하지 말자