[Effective Java] 아이템 50 - 적시에 방어적 복사본을 만들라
Java는 포인터를 사용하는 언어와 달리 메모리 충돌 오류에서 안전하지만 클라이언트가 우리가 작성한 클래스의 불변식을 깨려고 노력한다는 것을 가정에 두고 방어적으로 프로그래밍 해야 한다.
기간을 표현하는 클래스를 보자
class Period {
private final Date start;
private final Date end;
public Period(Date start, Date end) {
if(start.compareTo(end) > 0) {
throw new IllegalArgumentException(start + " after " + end);
}
this.start = start;
this.end = end;
}
public Date start() { return start; }
public Date end() { return end; }
// ... 생략
}
//공격
public static void main(String[] args) {
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78);
}
불변 처럼 보이지만 Date가 가변이란 사실을 알면 불변성을 깨기 쉽다.
사실 이미 Date.setXXX는 deprecated 되었고 LocalDateTime을 사용하면 불변성을 지킬 수 있다.
그 이전이라 생각하고 Period의 인스턴스 내부를 보호하려면 생성자의 각 가변 매개변수를 각각 방어적 복사해야 한다.
public Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if(start.compareTo(end) > 0) {
throw new IllegalArgumentException(start + " after " + end);
}
}
이렇게 하면 앞의 방식의 공격에 안전하다.
방어적 복사본을 만든 뒤 유효성 검사를 한 점에 주목해보자.
멀티 스레드 환경에서는 유효성을 검사한 후 복사본을 만들기 전 그 찰나에 다른 스레드가 원본 객체를 수정할 위협이 있기 때문에 이렇게 해야한다. 이런 공격을 검사시점/사용시점(TOCTOU) 공격이라 한다.
방어적 복사에 clone이 아닌 생성자를 사용한 것에도 주목해 보자.
Date는 final이 아니기 때문에 공격자가 정의한 악의적인 Date 클래스의 하위 인스턴스를 clone이 반환할 수 도 있기 때문이다. 이런 악성 인스턴스는 start와 end 필드의 참조를 가지고 있다가 클라이언트가 이에 접근할 수 있도록 길을 열어 줄 수 있다.
즉, 매개변수가 클라에 의해 확장될 수 있는 타입이라면 방어적 복사를 할때 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);
}
접근자 메서드가 가변 필드를 직접 드러내 발생한 문제이다. 이는 접근자가 가변 필드의 방어적 복사본을 반환하면 해결된다. 이때는 Date 객체가 java.util.Date인것이 확실 하므로 clone을 사용해도 된다. 하지만 아이템 13에서 말한 것 처럼 생성자난 정적 팩터리 메서드가 바람직 하다.
public Date start() { return new Date(start.getTime()); }
public Date end() { return new Date(end.getTime()); }
메서드든 생성자든 클라이언트가 제공한 객체의 참조를 내부의 자료구조에서 보관해야 한다면 그 객체가 잠재적으로 변경될 수 있는지 생각해 보고 변경되어도 클래스가 문제없이 동작할지 생각해 봐야 한다. 클라에서 받은 객체를 Set에 저장하거나 Map 의 키로 사용한다면 추후 그 객체가 변경되면 set, map의 불변식이 깨지게 된다.
아이템 17의 '되도록 불변 객체를 이용해 객체를 구성해야 방어적 복사가 준다'는 교훈을 다시한번 떠올리자
방어적 복사에는 성능 저하가 따르고 항상 쓸 수 있는 것도 아니다. 호출자가 컴포넌트 내부를 수정하지 않으리라 확신되면 방어적 복사를 생략할 수 있지만 수정하지 말아야 함을 명확히 문서화 해야한다.
넘겨 받은 가변 매개변수를 항상 방어적 복사를 해야되는 것은 아니다. 그 객체의 통제권을 명핵히 이전했을 수도 있다. 이럴떄는 클라이언트가 해당 객체를 더 이상 직접 수정하는 일이 없다고 약속해야 한다. 이 또한 문서에 기재해야겠다.
위 같은 메서드나 생성자를 가진 클래스는 취약하다 따라서, 신뢰할 수 있거나, 불변식이 깨지더라도 해당 클라에서만 문제가 되는 경우만 방어적 복사를 생략할 수 있다. 후자의 예로 래퍼클래스 패턴이 있다. 래퍼 클래스 특성상 클라는 래퍼에 넘긴 객체에 집접 접근할 수 있지만 그 영향은 오직 해당 클라에 있다.
결론
클래스가 클라로부터 받고 반환하는 경우 해당 필드가 가변이라면 그 요소는 반드시 방어적으로 복사하자.
방어적 복사 비용이 높다면 생략가능한지 생각해보고 아니라면 래퍼클래스의 활용도 검토해 보자.
이 모든 것은 문서화하자.