[Why]
자바는 안전한 언어다. 하지만 아무리 자바라 해도 다른 클래스로부터의 침범을 아무런 노력 없이 다 막을 수 있는 건 아니다. 그러니 클라이언트가 여러분의 불변식을 깨뜨리려 혈안이 되어 있다고 가정하고 방어적으로 프로그래밍해야 한다.
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;
}
... // 나머지 코드 생략
}
- 얼핏 이 클래스는 불변처럼 보이고, 시작 시각이 종료 시각보다 늦을 수 없다는 불변식이 무리 없이 지켜질 것 같다.
- 하지만 Date가 가변이라는 사실을 이용하면 어렵지 않게 그 불변식을 깨뜨릴 수 있다.
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // p의 내부를 수정했다!
- 자바 8 이후로는, Date 대신 불변인 Instant를 사용하면 된다.
(혹은 LocalDateTime이나 ZonedDateTime을 사용해도 된다)
매개변수를 방어적으로 복사하는 목적이 불변 객체를 만들기 위해서만은 아니다. 클라이언트가 제공한 객체의 참조를 내부의 자료구조에 보관할 때 그 객체가 변경된다면 우리의 클래스가 문제없이 동작하는지 따져봐야하기 때문이다.
- 예컨대 클라이언트가 건네준 객체를 내부의 Set 인스턴스에 저장하거나 Map 인스턴스의 키로 사용한다면, 추후 그 객체가 변경될 경우 객체를 담고 있는 Set 혹은 Map의 불변식이 깨질 것이다.
- 길이가 1 이상인 배열은 무조건 가변이다!
[How]
외부 공격으로부터 인스턴스 내부를 보호하려면 생성자에서 받은 가변 매개변수 각각을 방어적으로 복사(defensive 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 + "보다 늦다.");
}
}
- 매개변수의 유효성을 검사(아이템49)하기 전에 방어적 복사본을 만들고, 이 복사본으로 유효성을 검사한 점에 주목하자.
- 멀티스레딩 환경이라면 원본 객체의 유효성을 검사한 후 복사본을 만드는 그 찰나의 취약한 순간에 다른 스레드가 원본 객체를 수정할 위험이 있기 때문이다.
- 컴퓨터 보안 커뮤니티에서는 이를 검사시점/사용시점(time-of-check/time-of-use) 공격 혹은 영어 표기를 줄여서 TOCTOU 공격이라 한다.
- 방어적 복사에 Date의 clone 메서드를 사용하지 않은 점에도 주목하자. 매개변수가 제3자에 의해 확장될 수 있는 타입이라면 방어적 복사본을 만들 때 clone을 사용해서는 안 된다.
- Date는 final이 아니므로 clone이 Date가 정의한 게 아닐 수 있다.
- 즉, clone이 악의를 가진 하위 클래스의 인스턴스를 반환할 수도 있다.
- 예컨대 이 하위 클래스는 start와 end 필드의 참조를 private 정적 리스트에 담아뒀다가 공격자에게 이 리스트에 접근하는 길을 열어줄 수도 있다.
Period 인스턴스는 생성자를 수정했지만, 접근자 메서드가 내부의 가변 정보를 직접 드러내기 때문에 아직도 변경 가능하다.
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
p.end().setYear(78); // p의 내부를 변경했다!
- 이 공격을 막아내려면 단순히 접근자가 가변 필드의 방어적 복사본을 반환하면 된다.
public Date start() {
return new Date(start.getTime());
}
public Date end() {
return new Date(end.getTime());
}
- 생성자와 달리 접근자 메서드에서는 방어적 복사에 clone을 사용해도 된다.
- Period가 가지고 있는 Date 객체는 java.util.Date임이 확실하기 때문이다.
- 그렇더라도 아이템13에서 설명한 이유 때문에 인스턴스를 복사하는 데는 일반적으로 생성자나 정적 팩터리를 쓰는 게 좋다.
되도록 불변 객체들을 조합해 객체를 구성해야 방어적 복사를 할 일이 줄어든다! (아이템17)
다른 패키지에서 사용한다고 해서 넘겨받은 가변 매개변수를 항상 방어적으로 복사해 저장해야 하는 것은 아니다.
- 방어적 복사를 생략해도 되는 상황은 해당 클래스와 그 클라이언트가 상호 신뢰할 수 있을 때, 혹은 불변식이 깨지더라도 그 영향이 오직 호출한 클라이언트로 국한될 때로 한정해야 한다.
- 후자의 예로는 래퍼 클래스 패턴(아이템18)을 들 수 있다.
'독서찰기(讀書札記) > 이펙티브 자바' 카테고리의 다른 글
[아이템 52] 다중정의는 신중히 사용하라 (0) | 2022.03.15 |
---|---|
[아이템 51] 메서드 시그니처를 신중히 설계하라 (0) | 2022.03.13 |
[아이템 49] 매개변수가 유효한지 검사하라 (0) | 2022.03.11 |
[아이템 48] 스트림 병렬화는 주의해서 적용하라 (0) | 2022.03.10 |
[아이템 47] 반환 타입으로는 스트림보다 컬렉션이 낫다 (0) | 2022.03.09 |