자바로 작성한 클래스는 시스템의 다른 부분에서 무슨 짓을 하든 그 불변식이 지켜진다.
하지만 아무리 자바라 해도 다른 클래스로부터의 침범을 아무런 노력 없이 다 막을 수 있는건 아니다.
클라이언트가 불변식을 깨뜨리려 혈안이 되어 있다고 가정하고 방어적으로 프로그래밍해야 한다.
어떤 객체든 그 객체의 허락 없이는 외부에서 내부를 수정하는 일은 불가능하다. 하지만 주의를 기울이지 않으면 자기도 모르게 내부를 수정하도록 허락하는 경우가 생긴다.
// 불변식을 지키지 못한 기간을 표현하는 Period
public final class Period {
private final Date start;
private final Date end;
public Period(Date start, Date end) {
if (start.compareTo(end) > 0)
throw new IllegalArgumentException();
this.start = start;
this.end = end;
}
public Date start() {
return start;
}
public Date end() {
return end;
}
}
public class Example {
public static void main(String[] args) {
Date start = new Date();
Date end = new Date();
Period period = new Period(start, end);
end.setYear(2002); // (1)
}
}
Date는 변경될 수 있기 때문에 불변식을 깨뜨릴 수 있다.
외부 공격으로부터 인스턴스의 내부를 보호하려면 생성자에서 받은 가변 매개변수 각각을 방어적으로 복사해야 한다.Date 참조 대신 Date.getTime()이 반환하는 long 정수를 사용하거나
자바8 이후 버전이라면 Instant, LocalDateTime, ZonedDateTime을 사용하는 것이 좋다.
// 방어적 복사를 적용한 생성자
public final class Period {
private final Date start;
private final Date end;
public Period(Date start, Date end) {
this.start = new Date(start.getTime()); // (1)
this.end = new Date(end.getTime());
if (this.start.compareTo(this.end) > 0) // (2)
throw new IllegalArgumentException();
}
public Date start() {
return start;
}
public Date end() {
return end;
}
}
public class Example {
public static void main(String[] args) {
Date start = new Date();
Date end = new Date();
Period period = new Period(start, end);
end.setYear(2002); // period 영향 없음
period.end().setYear(2002); // (3)
}
}
// 방어적 복사를 적용한 접근자
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();
}
public Date start() {
return new Date(start.getTime()); // (1)
}
public Date end() {
return new Date(end.getTime());
}
}
public class Example {
public static void main(String[] args) {
Date start = new Date();
Date end = new Date();
Period period = new Period(start, end);
end.setYear(2002); // period 영향 없음
period.end().setYear(2002); // period 영향 없음
}
}
위와 같이 작성하면 Period는 시작 시각이 종료 시각보다 나중일 수 없는 완벽한 불변으로 거듭난다.
방어적 복사에는 성능 저하가 따르고, 항상 쓸 수 있는 것도 아니다.
방어적 복사를 생략해도 되는 상황은 해당 클래스와 그 클라이언트가 상호 신뢰할 수 있을 때, 혹은 불변식이 깨지더라도 그 영향이 오직 호출한 클라이언트로 국한될 때로 한정해야 한다. 이러한 상황이라도 호출자에서 해당 매개변수나 반환값을 수정하지 말아야 함을 명확히 문서화하는 것이 좋다.
클래스가 클라이언트로부터 받는 혹은 클라이언트로 반환하는 구성요소가 가변이라면 그 요소는 반드시 방어적으로 복사해야 한다. 복사 비용이 너무 크거나 클라이언트가 그 요소를 잘못 수정할 일이 없음을 신뢰한다면 방어적 복사를 수행하는 대신 해당 구성요소를 수정했을 때의 책임이 클라이언트에 있음을 문서에 명시하도록 하자.