Item50. 적시에 방어적 복사본을 만들라

클래스 불변식을 클라이언트가 꺠트릴 수 없도록 방어적으로 프로그래밍해야 한다.

다음과 같이 클래스 불변식을 유지하고자 하는 class가 있다고 가정하자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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 + " after " + end);
}
this.start = start;
this.end = end;
}

public Date start(){
return start;
}

public Date end(){
return end;
}
}

Period class는 한번 값이 설정되면 변경되지 않는 것 처럼 보이나 Date class 자체가 가변이라는 사실을 이용하면 클래스 불변식을 꺠트릴 수 있다.

1
2
3
4
5
6
public static void main(String[] args) {
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(68); // p 객체의 값이 변경된다.
}

따라서 Period class를 불변으로 유지하려면 불변 필드인 Instant를 사용해야 한다.

방어적 복사 (defensive copy)

외부 공격으로부터 class 불변식을 유지하려면 생성자에서 받은 가변 매개변수는 모두 방어적으로 복사해야 한다.

예시로든 Period class에 방어적 복사를 적용하면 다음과 같이 생성자에서 복사본을 필드로 가지게 만들고, 접근자에서도 복사본을 반환함으로서 클래스 불변식을 유지할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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(start + " after " + end);
}
}

public Date start(){
return new Date(start.getTime());
}

public Date end(){
return new Date(end.getTime());
}

주의할점은 매개변수의 유효성을 검사하기전에 방어적 복사본을 만들고 나서, 이 복사본으로 유효성 검사를 해야한다는 점이다.

멀티쓰래드 환경에서 원본 객체의 유효성을 검사한 후 복사본을 만드는 그 찰나의 취약한 순산에 다른 thread가 원본 객체를 수정할 위험이 있기 떄문이다. (이를 검사시점/사용시점(time-of-check/time-of-use) 공격 혹은 TOCTOU 공격이라고 한다.)

clone method를 사용해서 복사하지 않은 이유는 매개변수가 client에 의해 확장되어 clone method가 악의적인 method로 overriding될 수 있기 때문이다.

따라서 매개변수가 제 3자에 의해 확장될 수 있는 타입이라면 방어적 복사본을 만들 때 clone을 사용해서는 안된다.

방어적 복사에 대한 고려사항

매개변수를 방어적으로 복사하는 목적은 불변 객체를 만들기 위해서만은 아닌데, method든 생성자든 client가 제공한 객체의 참조를 내부의 자료구조에 보관할떄면 항상 그 객체가 잠재적으로 변경될 수 있는지를 생각해야 한다.

만약 변경된다면 , 변경이 클래스의 동작에 영향을 끼치는지 여부를 판단해서 방어적 복사 여부를 결정하면 된다. 애매한 경우에는 방어적 복사를 수행하는게 좋다.

방어적 복사가 성능적인 이유로 사용이 불가능하다면 반환값이나, 매개변수를 수정하면 안됨을 문서화해야 한다.

방어적 복사를 생략해도 되는 상황은 해당 class와 client가 상호 신뢰할 수 있을떄, 혹은 불변식이 깨지더라도 그 영향이 오직 호출한 client로 국한될때로 한정해야 한다.

Comments