아이템 50. 적시에 방어적 복사본을 만들라. - ksw6169/effective-java GitHub Wiki
의도했든, 의도하지 않았든 간에 외부에서는 내가 작성한 클래스의 불변식을 깨뜨릴 수 있다. 따라서 클라이언트가 내가 작성한 클래스의 불변식을 깨뜨릴 수 있다고 가정하고 방어적으로 프로그래밍을 해야 한다. 예컨대 아래 클래스는 기간(period)을 표현하는 클래스로 start와 end 값을 불변으로 만들 계획이었다.
public 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 + "가 " + end + "보다 늦다.");
this.start = start;
this.end = end;
}
public Date start() { return start; }
public Date end() { return end; }
}
하지만 외부에서는 어렵지 않게 불변식을 깨뜨릴 수 있다.
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니 새로운 코드를 작성할 때는 더 이상 사용하면 안된다.) - 또는
Date.getTime()
이 반환하는 long 정수를 사용하는 방법도 있다.
외부 공격으로부터 인스턴스의 내부를 보호하려면 생성자에서 받은 가변 매개변수 각각을 방어적으로 복사(depensive copy) 해야 한다. 즉, 인스턴스 안에서는 매개변수의 값으로 원본이 아닌 복사본을 사용한다.
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 + "보다 늦다.");
}
- 위 코드에서 매개변수의 유효성을 검사하기 전에 방어적 복사본을 만들고, 이 복사본으로 유효성을 검사한 점에 주목하자.
- 멀티스레딩 환경이라면 원본 객체의 유효성을 검사한 후 복사본을 만드는 찰나의 순간에 다른 스레드가 원본 객체를 수정할 위험이 있기 때문이다. 방어적 복사를 매개변수 유효성 검사 전에 수행하면 이런 위험에서 해방될 수 있다.
- 보안 커뮤니티에서는 이를 검사시점/사용시점(time-of-check/time-of-use) 공격 혹은 영어 표기를 줄여서 TOCTOU 공격이라 한다. (검사시점과 사용시점 사이에 다른 스레드가 개입하여 공용자원의 상태를 변경시키고 이를 통해 문제를 발생시키는 공격을 말한다.)
- 매개변수가 제3자에 의해 확장될 수 있는 타입이라면 방어적 복사본을 만들 때 clone을 사용해서는 안된다.
- 위 코드의 생성자에서 Date 타입으로 받은 매개변수가 악의적 의도를 가진 클라이언트에 의해 만들어진 Date의 하위 클래스일 수 있다. 따라서 clone 메소드가 Date에서 정의된 메소드라 판단할 수 없기 때문에 clone 메소드 대신 생성자를 통한 방어적 복사를 수행해야 한다.
클라이언트의 악의적인 접근을 방지하기 위해 생성자와 마찬가지로 접근자에서도 방어적 복사를 수행해야 한다. 이렇게 작성한 클래스는 네이티브 메소드나 리플렉션 같이 언어 외적인 수단을 동원하지 않고는 외부에서 수정할 수 없다.
public Date start() {
return new Date(start.getTime());
}
public Date end() {
return new Date(end.getTime());
}
- 생성자와 달리 접근자 메소드에서는 방어적 복사에 clone 메소드를 사용해도 된다.
- Period가 가지고 있는 Date 객체가 java.util.Date 임이 확실하기 때문이다. (신뢰할 수 없는 하위 클래스가 아니다.)
- 그럼에도 클래스의 인스턴스 필드를 복사하는 데는 일반적으로 생성자나 정적 팩토리를 쓰는 게 좋다.
- 메소드든 생성자든 클라이언트가 제공한 객체의 참조를 클래스 내부의 자료구조에 보관하는 경우에는 외부의 객체가 임의로 변경되어도 클래스가 문제없이 동작할지를 따져봐야 한다. 만약 이를 확신할 수 없다면 복사본을 만들어 저장해야 한다.
- 예컨대 클라이언트가 건네준 객체를 내부의 Set 인스턴스에 저장하거나 Map 인스턴스의 키로 사용한다면 추후 그 객체가 변경될 경우 객체가 담고 있는 Set 혹은 Map의 불변식이 깨질 것이다.
- 클래스가 불변이든 가변이든, 가변인 내부 객체를 클라이언트에 반환할 때는 반드시 심사숙고해야 한다. 안심할 수 없다면 원본이 아닌 방어적 복사본을 반환해야 한다.
- 길이가 1 이상인 배열은 무조건 가변이므로 내부에서 사용하는 배열을 클라이언트에 반환할 때는 항상 방어적 복사를 수행해야 한다. 방어적 복사 대신 배열의 불변 뷰를 반환하는 대안도 있다.
// 내부 배열을 불변 뷰(여기서는 VALUES)로 반환
private static final Thing[] PRIVATE_VALUES = { ... };
public static final List<Thing> VALUES =
Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
방어적 복사에는 성능 저하가 따르고, 항상 쓸 수 있는 것도 아니기 때문에 특정 상황에서는 방어적 복사를 생략해도 된다.
- 호출자가 컴포넌트 내부를 수정하지 않으리라 확신하는 경우 방어적 복사를 생략할 수 있다.
- 이 경우 가변 객체를 전달 받는 메소드나 생성자에서 해당 매개변수나 반환값을 수정하지 말아야 함을 명확히 문서화하는 게 좋다.
- 예컨대 래퍼 클래스의 경우 클라이언트는 래퍼에 넘긴 객체에 여전히 직접 접근이 가능하다. 따라서 래퍼의 불변식을 쉽게 파괴할 수 있지만 그 영향을 오직 클라이언트 자신만 받게 된다.
- 입력 매개변수로 받거나 반환하는 구성요소가 가변이라면 반드시 방어적으로 복사해야 한다.
- 복사 비용이 너무 크거나 클라이언트가 그 요소를 잘못 수정할 일이 없음을 신뢰한다면 방어적 복사를 수행하는 대신 해당 구성 요소를 수정했을 때의 책임이 클라이언트에 있음을 문서에 명시하도록 하자.
- Effective java 3/E