유효성 검사를 먼저 하는 경우에도 어차피 객체 불변성을 보장하기 위해서는 방어적 복사를 해야 하기 때문
방어적 복사(Defensive Copy)?
객체를 복사할 때 해당 객체가 가변 객체인 경우, 객체의 상태를 수정하는 것이 의도치 않게 다른 객체의 상태를 변경할 수 있는 상황을 방지하기 위한 복사 방식
Serializable 만 구현하면 불변식을 보장할 수 있을까?
→ 아님! readObject 메서드도 생성자만큼이나 주의해야 함
2. 예시와 함께 보기
public final class Period {
private final Date start;
private final 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()); // 가변인 Date 클래스의 위험을 막기 위해 새로운 객체로 방어적 복사
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; }
// ... 나머지 코드는 생략
}
Period 클래스의 생성자에서
start와 end 필드에 대해 방어적 복사를 수행
start와 end 필드를 반환하는 메서드에서도 방어적 복사를 수행
→ start와 end 필드의 참조값이 노출되는 것을 방지하여 불변 객체처럼 동작하도록 하고 있음
하지만, Period 클래스는 여전히 불변식을 보장하지 못함
start와 end 필드가 가변인 Date 클래스의 객체를 참조하고 있기 때문
Period 클래스의 사용자가 start와 end 필드가 참조하는 Date 객체를 수정할 수 있다면, Period 객체의 불변성이 깨질 수 있음
Period period = new Period(new Date(), new Date());
Date end = period.end();
end.setYear(100);
Period 객체를 생성한 후, end 필드가 참조하는 Date 객체를 얻어와서 해당 객체의 연도를 100년으로 수정하게 됨
→ 따라서 Date 클래스 대신 불변한 LocalDateTime 클래스나 Instant 클래스 등을 사용해야 하며, start와 end 필드가 참조하는 객체를 완전히 복사하는 방식으로 방어적 복사해야 함
3. 정리하면
readObject 메서드를 작성할 때는 언제나 public 생성자를 작성하는 자세로 임해야 한다.
readObject 메서드는 어떤 바이트 스트림이 넘어오더라도 유효한 인스턴스를 만들어내야 한다.
이 바이트 스트림이 항상 진짜 직렬화된 인스턴스라고 믿으면 안 된다.
안전한 readObject 메서드를 작성하는 지침
private 이여야 하는 객체 참조 필드는 각 필드가 가리키는 객체를 방어적으로 복사하라.
모든 불변식을 검사하고, 어긋난다면 InvalidObjectException을 던져라.
역직렬화 후 객체 그래프 전체의 유효성을 검사해야 한다면 ObjectInputValidation를 사용하라.
readObject에서 주의사항
1. 방어적 복사를 수행하는 불변 클래스 개선하기
readObject()
유효성 검사를 먼저 하는 경우에도 어차피 객체 불변성을 보장하기 위해서는 방어적 복사를 해야 하기 때문
방어적 복사(Defensive Copy)?
Serializable 만 구현하면 불변식을 보장할 수 있을까?
→ 아님! readObject 메서드도 생성자만큼이나 주의해야 함
2. 예시와 함께 보기
Period 클래스의 생성자에서
start와 end 필드를 반환하는 메서드에서도 방어적 복사를 수행
→ start와 end 필드의 참조값이 노출되는 것을 방지하여 불변 객체처럼 동작하도록 하고 있음
하지만, Period 클래스는 여전히 불변식을 보장하지 못함
Period 객체를 생성한 후, end 필드가 참조하는 Date 객체를 얻어와서 해당 객체의 연도를 100년으로 수정하게 됨
→ 따라서 Date 클래스 대신 불변한 LocalDateTime 클래스나 Instant 클래스 등을 사용해야 하며, start와 end 필드가 참조하는 객체를 완전히 복사하는 방식으로 방어적 복사해야 함
3. 정리하면
readObject
메서드를 작성할 때는 언제나 public 생성자를 작성하는 자세로 임해야 한다.readObject
메서드는 어떤 바이트 스트림이 넘어오더라도 유효한 인스턴스를 만들어내야 한다.readObject
메서드를 작성하는 지침private
이여야 하는 객체 참조 필드는 각 필드가 가리키는 객체를 방어적으로 복사하라.InvalidObjectException
을 던져라.ObjectInputValidation
를 사용하라.