방어적 프로그래밍
자바는 안전한 언어이지만, 다른 클래스로부터의 침범을 다 막을 수 있는 건 아니다. 따라서 클라이언트가 불변식을 깨뜨리려 혈안이 되어 있다고 가정하고 방어적으로 프로그래밍해야 한다.
불변식을 지키지 못한 예시
public final class Period {
private final Date start;
private final Date end;
public Date start() {
return start;
}
public Date end() {
return end;
}
}
다음 코드는 불변같지만, Date가 가변이라는 사실을 이용하면 어렵지 않게 그 불변식을 깨뜨릴 수 있다.
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // p의 내부를 수정했다.
자바 8 이후로는 Date대신 Instant를 사용하면 된다. 혹은 LocalDateTime 또는 ZonedDateTime을 사용해도 된다. Date는 낡은 API로 새로운 코드를 작성할 때는 더 이상 사용하면 안 된다.
방어적 복사
외부 공격으로부터 Period 인스턴스의 내부를 보호하려면 생성자에서 받은 가변 매개변수 각각을 방어적으로 복사해야한다.
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 + " after " + this.end);
}
매개변수의 유효성을 검사하기 전에 방어적 복사본을 만들고, 이 복사본으로 유효성을 검사한 점에 주목하자. 순서가 부자연스러워 보일 수 있지만 멀티 스레딩 상황에서 방어적 복사를 먼저 하지 않으면 복사본을 만드는 그 찰나의 취약한 순간에 다른 스레드가 원복 객체를 수정할 위험이 있다.
방어적 복사에 Date의 clone 메서드를 사용하지 않은 점에도 주목하자. Date는 final이 아니므로 clone이 Date가 정의한 게 아닐 수 있다. 즉, clone이 악의를 가진 하위 클래스의 인스턴스를 반환할 수도 있다. 이런 공격을 막기 위해서는 매개변수가 제3자에 의해 확장될 수 있는 타입이라면 방어적 복사본을 만들 때 clone을 사용해서는 안 된다.
내부의 가변 정보를 숨기자
생성자를 수정하면 앞서의 공격은 막아낼 수 있지만, Period 인스턴스는 아직도 변경 가능하다. 접근자 메서드가 내부의 가변 정보를 직접 드러내기 때문이다.
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
p.end().setYear(78); // p의 내부를 변경했다.
두 번째 공격을 막아내려면 단순히 접근자가 가변 필드의 방어적 복사본을 반환하면 된다.
public Date start() {
return new Date(start.getTime());
}
public Date end() {
return new Date(end.getTime()):
}
접근자 메서드에서는 방어적 복사에 clone을 사용해도 된다. Period가 가지고 있는 Date객체는 java.util.Date임이 확실하기 때문이다. 하지만 인스턴스를 복사하는 데는 일반적으로 생성자나 정적 팩터리를 쓰는게 좋다 (아이템 13)
메서드든 생성자든 클라이언트가 제공한 객체의 참조를 내부의 자료구조에 보관해야 할 때면 항시 그 객체가 잠재적으로 변경될 수 있는지를 생각해야 한다. 변경될 수 있는 객체라면 그 객체가 클래스에 넘겨진 뒤 임의로 변경되어도 그 클래스가 문제없이 동작할지 따져야 한다.
확신할 수 없다면 복사본을 만들어 저장해야 한다. 예를 들어 클라이언트가 건네준 객체를 내부의 Set 인터페이스에 저장하거나 Map 인스턴스의 키로 사용한다면, 추후 그 객체가 변경될 경우 객체를 담고 있는 Set혹은 Map의 불변식이 깨질 것이다.
길이가 1 이상인 배열은 무조건 가변임을 잊지 말자. 그러니 내부에서 사용하는 배열을 클라이언트에 반환할 때는 항상 방어적 복사를 수행해야 한다.