readObject 메서드는 방어적으로 작성하라
readObject 메서드
readObject 메서드는 실질적으로 또 다른 public 생성자이기 때문에 다른 생성자와 똑같은 수준으로 주의를 기울여야 한다. 보통의 생성자처럼 readObject 메서드에서도 인수가 유효한지 검사해야 하고 필요하다면 매개변수를 방어적으로 복사해야 한다. 이 작업을 제대로 수행하지 않으면 클래스의 불변식이 깨질 수 있다.
readObject는 매개변수로 바이트 스트림을 받는 생성자라 할 수 있다. 보통의 경우 바이트 스트림은 정상적으로 생성된 인스턴스를 직렬화해 만들어진다. 하지만 불변식을 깨뜨릴 의도로 임의 생성한 바이트 스트림을 건네면 문제가 생긴다. 정상적인 생성자로는 만들어낼 수 없는 객체를 생성해낼 수 있기 때문이다.
방어적 복사를 사용하는 불변 클래스
public final class Period {
private Date start;
private Date end;
/**
* @param start 시작 시각
* @param end 종료 시각; 시작 시각보다 뒤여야 한다.
* @throws IllegalArgumentException 시작 시각이 종료 시각보다 늦을 때 발생한다.
* @throws NullPointerException start나 end가 null이면 발행한다.
*/
public Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if(this.start.compareTo(this.end) > 0)
throw new IllegalArgumentException(start + " after" + end);
}
public Date start () { return new Date(start.getTime()); }
public Date end () { return new Date(end.getTime()); }
public String toString() { return start + " - " + end; }
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
// 방어적 복사
start = new Date(start.getTime());
end = new Date(end.getTime());
// 불변식을 검사
if(start.compareTo(end) > 0)
throw new InvalidObjectException(start + " after" + end);
}
...
}
객체를 역직렬화할 때는 클라이언트가 소유해서는 안 되는 객체 참조를 갖는 필드를 모두 방어적으로 복사해야 한다. 따라서 readObject에서는 불변 클래스 안의 모든 private 가변 요소를 방어적으로 복사해야 한다.
방어적 복사를 유효성 검사보다 앞서 수행하며, Date의 clone 메서드는 사용하지 않았다. 두 조치 모두 공격으로부터 보호하는 데 필요하다. 또한 final 필드는 방어적 복사가 불가능하니 주의하자. 그래서 이 readObject 메서드를 사용하려면 start와 end 필드에서 final 한정자를 제거해야 한다.
커스텀 readObject 메서드를 사용해야되는 경우
transient 필드를 제외한 모든 필드의 값을 매개변수로 받아 유효성 검사 없이 필드에 대입하는 public 생성자를 추가하면 안되는 경우, 커스텀 readObject 메서드를 만들어 모든 유효성 검사와 방어적 복사를 수행해야 한다. 혹은 직렬화 프록시 패턴을 사용하는 방법도 있다.
안전한 readObject 메서드 작성법
-
private이어야 하는 객체 참조 필드는 각 필드가 가리키는 객체를 방어적으로 복사하자. 불변 클래스 내의 가변 요소가 여기 속한다.
-
모든 불변식을 검사하여 어긋나는 게 발견되면 InvalidObjectException을 던지자. 방어적 복사 다음에는 반드시 불변식 검사가 뒤따라야 한다.
-
역직렬화 후 객체 그래프 전체의 유효성을 검사해야 한다면 ObjectInputValidation 인터페이스를 사용하자.
-
직접적이든 간접적이든, 재정의할 수 있는 메서드는 호출하지 말자.
readObject 메서드를 작성할 때는 언제나 public 생성자를 작성하는 자세로 임해야 한다. readObject는 어떤 바이트 스트림이 넘어오더라도 유효한 인스턴스를 만들어내야 한다. 바이트 스트림이 진짜 직렬화된 인스턴스라고 가정해서는 안 된다.
Comments