이번장의 핵심은...
클래스가 클라이언트로부터 받는 혹은 클라이언트로 반환하는 구성요소가 가변이라면
그 요소는 반드시 방어적으로 복사해야 한다
복사 비용이 너무 크거나 클라이언트가 그 요소를 잘못 수정할 일이 없음을 신뢰한다면
방어적 복사를 수행하는 대신 구성요소를 수정했을 때의 책임이 클라이언트에 있음을 문서에 명시하자
Date
자바에서 제공하는 Date 클래스는 가변입니다
자바 8부턴 Date 대신 LocalDateTime와 ZonedDateTime같은 불변클래스를 사용합니다
Period
얼핏보면 Period 클래스는 불변처럼 보이고, 시작 시간이 종료 시간보다 늦을 수 없다는 불변식이 지켜질 것 같지만, Date가 가변이라는 사실을 이용하면 쉽게 깰 수 있습니다
// 코드 50-1 기간을 표현하는 클래스 - 불변식을 지키지 못했다.
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를 쓴 낡은 코드들을 대처하기 위해..
Period 공격 1
//코드 50-2 Period 인스턴스의 내부를 공격해보자.
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // p의 내부를 수정했다!
어떻게 방어할 수 있을까요?
외부 공격으로부터 Period 인스턴스의 내부를 보호하려면 생성자에서 받은 가변 매개변수 각각을 방어적으로 복사해야합니다 (defensive copy)
Period 인스턴스 안에는 원본이 아닌 복사본을 사용합니다
// 코드 50-3 수정한 생성자 - 매개변수의 방어적 복사본을 만든다.
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 공격])
그렇다면 왜 clone()으로 복사하지 않을까요?
clone()은 매개변수가 final 클래스가 아니어서 상속이 가능한 타입이라면 사용하면 안됩니다
Date 클래스를 상속한 클래스가 재정의하여 하위 클래스의 인스턴스를 반환할 수도 있기 때문에 하위 클래스는 start, end 필드의 참조를 private 정적 리스트에 담아뒀다가 공격자에게 이 리스트에 접근하는 길을 열어줄 수 있습니다
즉, 매개변수가 제 3자에 의해 확장될 수 있는 타입이라면 방어적 복사본을 만들 때 clone을 사용해서는 안됩니다
이렇게 생성자를 수정하여 공격 1을 막아낼 수 있습니다
Period 공격 2
Period 인스턴스를 변경하여 공격
// 코드 50-4 Period 인스턴스를 향한 두 번째 공격
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
p.end().setYear(78); // p의 내부를 변경했다!
위의 공격을 막아내려면 접근자가 가변 필드의 방어적 복사본을 반환하면 됩니다
//public Date start() {
// return start;
// }
// public Date end() {
// return end;
// }
// 코드 50-5 수정한 접근자 - 필드의 방어적 복사본을 반환한다.
public Date start() {
return new Date(start.getTime());
}
public Date end() {
return new Date(end.getTime());
}
이제 Period는 완벽한 불변입니다
Period 자신 말고는 가변 필드에 접근할 방법이 없습니다
모든 필드가 객체 안에 완벽하게 캡슐화되어 있습니다
이 방법에선 생성자와 달리 접근자 메서드에서는 방어적 복사 clone을 사용해도 됩니다
Period가 가지고 있는 Date 객체가 java.util.Date임이 확실하기 때문입니다 (신뢰할 수 없는 하위 클래스가 아닙니다)
이 예제의 경우 자바 8이상을 사용하고 있다면, Instant(혹은 LocalDateTime or ZoneDateTime)을 사용하는게 좋습니다
이 글은 “이펙티브 자바 3판” 책 내용을 정리한 글입니다.
만약 저작권 관련 문제가 있다면 “shk3029@kakao.com”로 메일을 보내주시면, 바로 삭제하도록 하겠습니다.
'Java > Effective Java 3E' 카테고리의 다른 글
[이펙티브자바 3판] ITEM52. 다중정의는 신중히 사용하라 (2) | 2021.01.04 |
---|---|
[이펙티브자바 3판] ITEM51. 메서드 시그니처를 신중히 설계하라 (0) | 2020.12.27 |
[이펙티브자바 3판] ITEM49. 매개변수가 유효한지 검사하라 (0) | 2020.12.19 |
[이펙티브자바 3판] ITEM48. 스트림 병렬화는 주의해서 적용하라 (0) | 2020.12.15 |
[이펙티브자바 3판] ITEM47. 반환타입으로 스트림보다 컬렉션이 낫다 (0) | 2020.12.13 |
댓글