[Effective Java] 아이템50- 적시에 방어적 복사본을 만들라
클라이언트가 우리의 불변식을 깨뜨리려 혈안이 되어있다고 가정하고 방어적으로 프로그래밍해야 한다. 어떤 객체든 그 객체의 허락 없이는 외부에서 내부를 수정하는 일은 불가능하다. 하지만 주의를 기울이지 않으면 자기도 모르게 내부를 수정하도록 허락하는 경우가 생긴다. 아래 불변 클래스를 보자
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;
}
}
final
을 사용해서 불변 클래스로 만들었고, 멤버 변수인 start와 end 도 final
로 선언했다. 하지만 Date
가 가변이라는 사실을 이용하면 어렵지 않게 그 불변식을 깨뜨릴 수 있다.
공격!
public static void main(String[] args) {
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // 수정됐다!
}
자바8 이후부터는 Date
대신 불변인 Instant
를 사용하면 된다.
Date
는 낡은 API이니 새로운 코드를 작성할 떄는 더 이상 사용하면 안된다. 😱
매개변수를 방어적으로 복사하자 (defensive copy)
매개변수로 받은 Date
가 가변이므로 이를 복사해서 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 + "가 " + this.end + "보다 늦다.");
}
TOCTOU 공격 (time-of-check/time-of-use)
멀티 스레딩 환경이라면 원본 객체의 유효성을 검사한 후 복사본을 만드는 그 찰나의 취약한 순간에 다른 스레드가 원본 객체를 수정할 위험이 있다.
따라서, 매개변수의 유효성 검사를 하기 전에 방어적 본사본을 만들어 두고, 이 복사본으로 유효성을 검사하자
방어적 복사를 위해 clone
을 사용하지 않은 것도 주목하자. Date
는 final이 아니므로 언제든지 확장할 수 있다. 따라서 Date
의 clone 메서드가 하위 클래스의 clone 일 수도 있다.
접근자 메서드를 사용하자
현재 start 와 end는 직접 접근 가능하다. 접근자 메서드의 반환값 또한 가변 필드의 방어적 복사본을 반환하면 된다.
방어적 복사에 참고해야할 사항
-
매개변수를 방어적으로 복사하는 목적이 불변 객체를 만들기 위해서만은 아니다. 변경될 수 있는 객체라면 그 객체가 클래스에 넘겨진 뒤 임의로 변경되어도 그 클래스가 문제없이 동작할지를 따져봐라. 예를 들어 클라이언트가 건네준 객체를 Map 인스턴스의 키로 사용한다면, 추후 그 객체가 변경될 경우 객체를 담고있는 Map의 불변식이 깨질 것이다.
-
길이가 1 이상인 배열은 무조건 가변이므로 내부에서 사용하는 배열을 클라이언트에 반환할 때는 항상 방어적 복사를 수행해야 한다. (혹은 배열의 불변 뷰를 반환하는 대안도 있다.)
-
되도록 불면 객체들을 조합해 객체를 구성해야 방어적 복사를 할 일이 줄어든다. 앞서 살펴본
Period
객체인 경우Instant
를 사용하면 된다. -
방어적 복사를 생략해도 되는 상황은 해당 클래스와 그 클라이언트가 서로 상호 신뢰할 수 있을 때, 혹은 불변식이 깨지더라도 그 영향이 오직 호출한 클라이언트로 국한될 때 뿐이다.
댓글남기기