적시에 방어적 복사본을 만들라
자바는 안전한 언어다. 네이티브 메서드를 사용하지 않으니 C, C++ 같이 안전하지 않은 언어에서 흔히 보는 버퍼 오버런, 배열 오버런, 와일드 포인터 같은 메모리 충돌 오류에서 안전하다. 자바로 작성한 클래스는 시스템의 다른 부분에서 무슨 짓을 하든 불변식이 지켜진다.
하지만 자바도 다른 클래스로부터의 침범을 아무런 노력 없이 다 막을 수 있지는 않다. 그래서 클라이언트가 불변식을 깨뜨리려 할 경우를 가정하고 방어적으로 프로그래밍해야 한다.
방어적 복사
불변식이 깨지는 예
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가 가변이라는 사실을 이용하면 불변식을 깨드릴 수 있다.
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78);
외부 공격으로부터 Period 인스턴스의 내부를 보호하려면 생성자에서 받은 가변 매개변수 각각을 방어적으로 복사해야 한다. 그런 다음 Period 인스턴스 안에서는 원본이 아닌 복사본을 사용한다.
자바 8 이후로는 Date 대신 불변인 Instant를 사용하면 이 문제를 해결할 수 있다. Date는 낡은 API이니 새로운 코드를 작성할 때는 더 이상 사용하면 안 된다.
매개변수의 방어적 복사
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 + "보다 늦다.");
}
매개변수의 유효성을 검사하기 전에 방어적 복사본을 만들고, 이 복사본으로 유효성을 검사했다. 멀티스레딩 환경이라면 원본 객체의 유효성을 검사한 후 복사본을 만드는 그 찰나의 취약한 순간에 다른 스레드가 원본 객체를 수정할 위험이 있기 때문이다.
방어적 복사에 Date의 clone 메서드를 사용하지 않았다. Date는 final이 아니므로 clone이 Date가 정의한 게 아닐 수 있기 때문이다. 즉, clone이 악의를 가진 하위 클래스의 인스턴스를 반환할 수도 있다.
매개변수가 제3자에 의해 확장될 수 있는 타입이라면 방어적 복사본을 만들 때 clone을 사용해서는 안 된다.
매개변수를 방어적으로 복사하는 목적이 불변 객체를 만들기 위해서만은 아니다. 메서드든 생성자든 클라이언트가 제공한 객체의 참조를 내부의 자료구조에 보관해야 할 때면 항시 그 객체가 잠재적으로 변경될 수 있는지를 생각해야 한다. 변경될 수 있는 객체라면 그 객체가 클래스에 넘겨진 뒤 임의로 변경되어도 그 클래스가 문제없이 동작할지를 따져야 한다. 확실할 수 없다면 복사본을 만들어 저장해야 한다.
가변 필드의 방어적 복사본을 반환
생성자를 수정했지만 접근자 메서드가 내부의 가변 정보를 직접 드러내는 문제가 있다. 이는 단순히 접근자가 가변 필드의 방어적 복사본을 반환하면 된다.
public Date start() {
return new Date(start.getTime());
}
public Date end() {
return new Date(end.getTime());
}
생성자와 달리 접근자 메서드에서는 방어적 복사에 clone을 사용해도 된다. Period가 가지고 있는 Date 객체는 java.util.Date임이 확실하기 때문이다(신뢰할 수 없는 하위 클래스가 아니다). 그렇더라도 인스턴스를 복사하는 데는 일반적으로 생성자나 정적 팩터리를 쓰는 게 좋다.
가변인 내부 객체를 클라이언트에 반환할 때는 반드시 심사숙고해야 한다. 안심할 수 없다면 방어적 복사본을 반환해야 한다. 길이가 1이상인 배열은 무조건 가변임을 잊지 말자. 그러니 내부에서 사용하는 배열을 클라이언트에 반환할 때는 항상 방어적 복사를 수행해야 한다. 혹은 배열의 불변 뷰를 반환하는 대안도 있다.
방어적 복사 생략해도 되는 경우
방어적 복사에는 성능 저하가 따르고, 항상 쓸 수 있는 것도 아니다.
호출자가 컴포넌트 내부를 수정하지 않으리라 확신할 때
같은 패키지에 속하는 등의 이유로 호출자가 컴포넌트 내부를 수정하지 않으리라 확신하면 방어적 복사를 생략할 수 있다. 이런 상황이라도 호출자에서 해당 매개변수나 반환값을 수정하지 말아야 함을 문서화하는 게 좋다.
해당 클래스와 그 클라이언트가 상호 신뢰할 수 있을 때
불변식이 깨지더라도 그 영향이 오직 호출한 클라이언트로 국한될 때
래퍼 클래스 패턴을 예로 들 수 있다. 래퍼 클래스의 특성상 클라이언트는 래퍼에 넘긴 객체에 여전히 직접 접근할 수 있다. 따라서 래퍼의 불변식을 쉽게 파괴할 수 있지만 그 영향을 오직 클라이언트만 받게 된다.
클래스가 클라이언트로부터 받는 혹은 클라이언트로 반환하는 구성요소가 가변이라면 그 요소는 반드시 방어적으로 복사해야 한다. 복사 비용이 너무 크거나 클라이언트가 그 요소를 잘못 수정할 일이 없음을 신뢰한다면 방어적 복사를 수행하는 대신 해당 구성요소를 수정했을 때의 책임이 클라이언트에 있음을 문서에 명시하자.
Comments