자바는 안전한 언어이지만 다른 클래스로부터의 침범을 아무런 노력 없이 다 막을 수 있는 것은 아니다. 따라서 클라이언트가 불변식을 깨뜨리려고 혈안되어 있다고 가정하고 방어적
으로 프로그래밍해야 한다.
// 기간을 표현하는 클래스
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) {
if (start.compareTo(end) > 0)
throw new IllegalArgumentException(
start + "가 " + end + "보다 늦다.");
this.start = start;
this.end = end;
}
public Date start() {
return start;
}
public Date end() {
return end;
}
public String toString() {
return start + " - " + end;
}
}
위와 같이 코드를 작성할 경우, 인스턴스 내부를 향한 공격에 노출될 수 있다.
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // p의 내부를 변경했다!
System.out.println(p);
start = new Date();
end = new Date();
p = new Period(start, end);
p.end().setYear(78); // p의 내부를 변경했다!
System.out.println(p);
다음과 같이 생성자와 접근자를 방어적 복사를 사용하도록 수정하면, Period는 완벽히 불변이 된다.
public final class Period {
private final Date start;
private final Date end;
// 수정한 생성자 - 매개변수의 방어적 복사본을 만든다.
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(
this.start + "가 " + this.end + "보다 늦다.");
}
// 수정한 접근자 - 필드의 방어적 복사본을 반환한다.
public Date start() {
return new Date(start.getTime());
}
public Date end() {
return new Date(end.getTime());
}
}
Q. 왜 매개변수 유효성 검사를 하기 전 방어적 복사본을 만들고, 복사본으로 유효성을 검사하나?
멀티스레딩 환경이라면 원본 객체의 유효성을 검사한 후 복사본을 만드는 찰나의 취약한 순간에 다른 스레드가 원본 객체를 수정할 위험이 있기 때문이다.
따라서 순서가 부자연스러워 보이더라도, 반드시 이렇게 작성해야 한다.
1) 매개변수가 제3자에 의해 확장될 수 있는 타입이라면 방어적 복사본을 만들 때 clone을 사용해서는 안 된다.
2) 클라이언트가 제공한 객체의 참조를 내부의 자료구조에 보관해야 할 때, 그 객체가 잠재적으로 변경될 수 있다면 방어적 복사본을 사용해야 한다.
3) 해당 클래스와 그 클라이언트가 상호 신뢰할 수 있을 때, 혹은 불변식이 깨지더라도 그 영향이 오직 호출한 클라이언트로 국한될 때는 방어적 복사를 생략할 수 있다. 그러나 방어적 복사를 생략하더라도 호출자에서 해당 매개변수나 반환값을 수정하지 말아야 함을 문서화하자.
🔖 핵심 정리
클래스가 클라이언트로부터 받는 혹은 클라이언트로 반환하는 구성요소가 가변이라면 그 요소는 반드시 방어적으로 복사해야 한다.
복사 비용이 너무 크거나 클라이언트가 그 요소를 잘못 수정할 일이 없음을 신뢰한다면 방어적 복사를 수행하는 대신 해당 구성요소를 수정했을 때의 책임이 클라이언트에 있음을 문서에 명시하도록 하자.