Item50. 적시에 방어적 복사본을 만들라!
Intro
- 자바는 안전한 언어다.
- 네이티브 메서드를 사용하지 않으니 C, C++ 처럼 메모리 충돌 오류가 나는 경우에 대해서 안전하다.
- 자바로 작성한 클래스는 시스템의 다른 부분에서 무슨 짓을 하든 그 불변식이 지켜진다.
방어적 프로그래밍
- 아무리 자바라 해도 다른 클래스로부터의 침범을 아무런 노력없이 다 막을 수 있는 것은 아니다.
- 클라이언트가 불변식을 깨뜨리려 혈안이 되어 있다고 가정하고 방어적으로 프로그래밍해야 한다.
외부에서 객체의 내부를 수정 가능한 문제
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public final class Period {
private final Date start;
private final Date end;
public Period(Date start, Date end) {
if(start.compareTo(end) > 0) {
try {
throw new IllegalAccessException(
start +"가" + end + "보다 늦다.");
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
this.start = start;
this.end = end;
}
public Date start() { return start; }
public Date end() { return end; }
// ... 생략
}
- 얼핏 이 클래스는 불변처럼 보인다. 시작 시각이 종료 시각보다 늦을 수 없다.
- Date가 가변이라는 사실을 이용하면 어렵지 않게 불변식이 깨진다.
1
2
3
4
Date start = new Date();
Date end = new Date();
Period period = new Period(start, end);
end.setYear(3); // period의 내부를 수정
- 다행히 이러한 문제는 자바 8 이후로는 손쉽게 해결 가능하다.
- Date 대신 불변인 Instant (혹은 LocalDateTime이나 ZonedDateTime)을 사용하면 된다.
- Date는 낡은 API다. 새로운 코드를 작성할 때는 더 이상 사용하면 안된다.
- 여전히 Date를 사용하는 낡은 코드가 상당히 존재한다. 이를 대비해 외부 공격으로부터 Period 인스턴스 내부를 보호하려면 생성자에서 받은 가변 매개변수 각각을 방어적으로 복사해야 한다.
방어적 복사
1
2
3
4
5
6
7
8
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 +"가" + end + "보다 늦다.");
}
}
- 이러한 생성자를 사용하면 앞서의 공격은 더 이상 Period에 위협이 되지 않는다.
- 매개변수의 유효성을 검사하기 전에 방어적 복사본을 만들고, 이 복사본으로 유효성을 검사한 점에 주목하자.
- 반드시 이러한 순서로 작성해야 한다.
- 멀티 스레딩 환경이라면 ‘원본 유효성 검사 -> 복사본 만들기’와 같은 찰나의 순간에 다른 스레드가 원본을 수정할 위협이 있기 때문이다.
- 방어적 복사를 사용한다면 이러한 위협에서 자유롭다.
- 매개변수가 제3자에 의해 확장될 수 있는 타입이라면 방어적 복사본을 만들 때 clone을 사용해서는 안된다.
접근자 메서드를 통해 내부를 공격하는 문제
1
2
3
4
Date start = new Date();
Date end = new Date();
Period period = new Period(start, end);
period.end().setMonth(3); // period 내부를 수정했다.
- 이러한 공격을 막아내려면 단순히 접근자가 가변 필드의 방어적 복사본을 반환하면 된다.
1
2
public Date start() { return new Date(start.getTime()); }
public Date end(){ return new Date(end.getTime());
- 이제 모든 필드가 객체 안에 완벽하게 캡슐화되었다.
- 생성자와는 달리 접근자 메서드에서는 방어적 복사에 clone 메서드를 사용해도 된다.
- 가능은 하지만 아이템 13의 설명을 참고해볼 때 인스턴스를 복사하는 데는 일반적으로 생성자나 정적 팩터리를 쓰는 게 좋다.
매개변수를 방어적으로 복사하는 목적
- 불변 객체를 만들기 위해서만 사용하는 것은 아니다.
- 잠재적으로 변경될 가능성이 있고, 변경될 수 있는 객체라면, 그 객체가 클래스에 넘겨진 뒤 임의로 변경되어도 그 클래스가 문제없이 동작할지 고려해야 한다.
- 확신할 수 없다면 방어적 복사본을 만들어 저장해야 한다.
- 내부 객체를 클라이언트에 건네주기 위해 방어적 복사본을 만드는 이유도 마찬가지다.
- 길이가 1 이상인 배열은 무조건 가변이니, 내부에서 사용하는 배열을 클라이언트에 반환 시 항상 방어적 복사를 수행하자. 혹은 불변의 뷰를 반환하자.
방어적 복사의 단점
- 성능 저하가 따르고, 항상 사용할 수 있는 것도 아니다.
- 고로 되도록 불변 객체들을 조합하여 객체를 구성해, 방어적 복사를 할 필요를 줄이는 것이 좋겠다.
핵심 정리
- 클래스가 클라이언트로부터 받는 혹은 클라이언트로 반환하는 구성요소가 가변인 경우, 그 요소는 반드시 방어적 복사를 해야 한다.
- 복사 비용이 너무 크거나, 클라이언트가 그 요소를 잘못 수정할 일이 없음을 신뢰한다면 방어적 복사를 수행하는 대신, 해당 요소를 수정했을 때의 책임이 클라이언트에 있음을 문서에 명시하자.
Comments powered by Disqus.