이번 아이템은 예전에 작성된 낡은 코드들을 대처하기 위한 것이다. - p.303
클라이언트가 여러분의 불변식을 깨뜨리려 혈안이 되어 있다고 가정하고 방어적으로 프로그래밍해야 한다. - p.302
- 실제로도 악의적인 의도를 가진 사람들이 시스템의 보안을 뚫으려 시도한다.
- 평범한 프로그래머도 실수로 여러분의 클래스를 오작동하게 만들 수 있다.
- 어떤 경우가 됐든 우리의 클래스를 보호하자!
(인텔리제이 복붙으로 시연)
public final class Period {
private final Date start;
private final Date end;
/**
* @param start 시작 시각
* @param end 종료 시각; 시작 시각보다 뒤여야 한다.
* @throws IllegalArgumentException 시작 시각이 종료 시각보다 늦을 때 발생한다.
* @throws NullPointerException end가 null이면 발생한다.
*/
public Period(Date start, Date end) {
if (start.compareTo(end) > 0) {
throw new IllegalArgumentException(start + "가 " + end + "보다 늦다.");
}
this.start = start;
this.end = end;
}
public Date strat() {
return start;
}
public Date end() {
return end;
}
// 생략
}
- 불변처럼 보인다.
- 하지만 아니다. 왜?
- Date가 가변이기 때문에!
public static void main(String[] args) {
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // 내부 상태 변경
}
Period
인스턴스 내부를 보호하려면 생성자에서 받은 가변 매개변수를 방어적 복사해야 한다.
public Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if (this.start.compareTo(end) > 0) {
throw new IllegalArgumentException(start + "가 " + end + "보다 늦다.");
}
}
- 앞에서 생성자나 메서드의 시작부에 유효성을 검사하라고 했다.
- 하지만 방어적 복사 할 때는 반대다.
- 멀티스레딩 환경에서, 원본 객체의 유효성 검사를 먼저 할 경우 복사본을 만드는 시간 동안 다른 스레드가 원본 객체를 수정할 위험이 있기 때문이다.
clone()
쓰지 마라.Date
가final
이 아니므로clone()
메서드로 악의적인 하위 클래스의 인스턴스를 반환할 수도 있다.
이걸로 끝이 아니다.
public static void main(String[] args) {
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
p.end().setYear(78); // 내부 상태 변경
}
public Date start() {
return new Date(start.getTime());
}
public Date end() {
return new Date(end.getTime());
}
- 이제 완벽한 불변이다.
- 불변을 깨뜨리려면 네이티브 메서드, 리플렉션과 같은 언어 외적인 수단을 동원해야 한다.
- 방어적 복사의 목적
- 불변 객체 만들기
- 객체가 다른 클래스에게 넘겨져도 문제없이 동작해야 한다.
- Set이나 Map에서 해당 인스턴스를 사용할 경우
- 해당 인스턴스의 상태가 변해도 Set과 Map이 정상적으로 동작하는지 생각해야 한다.
- Set이나 Map에서 해당 인스턴스를 사용할 경우
- 자바 8 이후로는
Date
대신Instant
,LocalDateTime
,ZonedDateTime
을 사용하라. 불변이다. - 가변 객체 사용할 때는 방어적 복사를 해라.
- 복사 비용이 너무 크거나 안정성에 확신이 있다면, 객체를 수정했을 때의 책임이 클라이언트에 있음을 문서에 명시하라.