- 클라이언트의 악의를 최대로 가정하고 프로그래밍하라
- 생성자에서 받은 가변 매개변수를 방어적으로 복사하라
- 복사 이후 검증하라
- clone을 사용하지 마라
- 가변 필드를 방어적 복사를 통해 반환하라
클라이언트가 당신의 불변식을 개뜨리려 혈안이 되어 있다고 가정하고 방어적으로 프로그래밍 하라
- 어떤 객체든 허락 없이 외부에서 내부를 수정하는 일은 불가능해야 한다.
- 하지만 내부를 수정하도록 허락하는 경우가 생긴다
public final 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 + " after " + end);
this.start = start;
this.end = end;
}
public Date start() {
return start;
}
public Date end() {
return end;
}
}
- 기간 객체는 start/end를 불변으로 만들 생각이었다.
- 기간 객체의 불변식은
시작시간이 종료시간보다 앞선다
이다. - 그러나, 다음 같은 두가지 공격을 통해 이 불변식을 쉽게 깨뜨릴 수 있다
public static void main(String[] args) {
// Attack the internals of a Period instance (Page 232)
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // p에 들어가 있는 end Date에 대해 Modifies internals of p!
System.out.println(p);
// 결과 : Thu May 16 10:41:58 KST 2024 - Tue May 16 10:41:58 KST 1978
// end시각이 1978년으로 설정됨 => 불변식이 깨짐
}
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 + " after " + this.end);
}
복사 > 유효성 검사 순서를 지켜야 한다
복사 > 유효성 검사
: 반드시 이 순서로 수행해야 한다- cause) TOCTOU(Time Of Check / Time Of Use)
- 올바른 값 > 유효성 검사(Check) > 원하는 값으로 변경 > 사용(Use)
- 멀티 스레드 환경에서 유효성 검사 이후 복사본을 만드는 순간에 객체 ㅅ정의 위험이 있음
매개변수가 3자에 의해 확장될 수 있다면 clone을 사용하지 말자
- 만약 clone을 통해 복사를 한다고 가정하자
public Period(Date start, Date end) {
this.start = (Date) start.clone();
this.end = (Date) end.clone();;
if (this.start.compareTo(this.end) > 0)
throw new IllegalArgumentException(
this.start + " after " + this.end);
}
- Date의 하위 타입인 AttackDate에서 clone을 재정의할 수 있다.
public class AttackDate extends Date {
public AttackDate(long date) {
super(date);
}
@Override
public Object clone() {
Date cloneDate = new Date();
cloneDate.setYear(78);
return cloneDate;
}
}
- 그렇다면 외부에서 의도를 가지고 공격을 감행할 수 있다.
- 즉, clone의 대상이 확장된 인스턴스일 때 악의를 가진 인스턴스를 반환할 수 있다.
public static void main(String[] args) {
start = new AttackDate(new Date().getTime());
end = new AttackDate(new Date().getTime());
p= new Period(start, end);
System.out.println(p);
// 결과 : Tue May 16 11:14:20 KST 1978 - Tue May 16 11:14:20 KST 1978
}
-
따라서, 제 3자에 이해 확장될 수 있는 타입이라면 방어적 복사본을 만들 때 clone을 사용하지 마라
public static void main(String[] args) {
start = new Date();
end = new Date();
p = new Period(start, end);
p.end().setYear(78); // Modifies internals of p!
System.out.println(p);
// 결과 : Thu May 16 10:41:58 KST 2024 - Tue May 16 10:41:58 KST 1978
// end시각이 1978년으로 설정됨 => 불변식이 깨짐
}
public Date start() {
return new Date(start.getTime());
}
public Date end() {
return new Date(end.getTime());
}
- Period 자신말고는 가변필드에 접근할 방법이 없으로 온전히 불변화 된다
- Date 객체임이 보장되므로 clone을 사용해도 된다.
- 읽기 전용 뷰만을 제공하며 추가, 삭제를 막음
- 내부적으로 원본 리스트를 멤버 변수로 가지고 있다 == 원본리스트의 변경이 반영된다
- 리스트 내부 객체의 참조값을 그대로 사용한다 == 내부 객체의 변경이 반영된다
- 주의점1) 원본 리스트의 추가/삭제 등 수정 > 변경 반영(참조값이 같으므로)
- 주의점2) 내부 객체의 수정 > 변경 반영
- 데이터의 추가, 삭제 기능을 막은 상태로 읽기만 지원
- 새로운 리스트를 생성하여 원본 데이터를 복사 반환 == 원본 리스트의 변경이 반영되지 않는다
- unmodifiableList와 같이 내부 객체의 참조값은 그대로 복사된다 == 내부 객체의 변경이 반영된다
- 주의점1) 원본 리스트 수정 시 > 변경이 반영되지 않음 (참조값이 다른 리스트이므로)
- 주의점2) 내부 객체 수정 시 > 변경 반영
- 불변객체들을 조합해 객체르 구성하면 방어적 복사를 할 일이 줄어든다
- 방어적 복사는 성능 저하가 따르고, 또 항상 쓸 수 있는 것도 아니다
- 방어적 복사는 두가지 상황에서 생략 가능하다
- 상황1) 해당 클래스와 클라이언트가 상호 신뢰할 수 있을 때
- 상황2) 불변식이 깨지더라도 그 영향이 오직 클라이언트로 국한 될 때