적시에 방어적 복사본을 만들어라
자바는 네이티브 메서드를 사용하지 않아 C, C++에서 흔히 보는 버퍼 오버런, 배열 오버런, 와일드 포인터와 같은 메모리 충돌 오류에서 안전하다. 자바로 작성한 클래스는 시스템의 다른 부분에서 무얼하든 불변식이 지켜진다.
클라이언트가 불변식을 깨뜨리려 혈안이 되어 있다고 가정하고 방어적으로 프로그래밍 해야 한다.
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 + "가 " + end + "보다 늦다."
);
}
this.start = start;
this.end = end;
}
public Date start() {
return start;
}
public Date end() {
return end;
}
}
Date start = new Date();
Date end = new Date();
Period period = new Period(start, end);
end.setYear(78); // period 내부 변경 성공!
Java는 CallByValue로, 참조형 변수를 넘긴다면 같은 객체를 바라보기에 가변인 객체를 넘기면 실제로 영향을 미친다. 자바8 이후 불변인 객체, 혹은 LocalDateTime이나 ZonedDateTime 사용하면 해결할 수 있다.
참고: https://bcp0109.tistory.com/360
외부 공격으로부터 Period 인스턴스의 내부를 보호하려면 생성자에서 받은 가변 매개변수 각각을 방어적으로 복사해야 한다.
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 + "보다 늦다."
);
}
}
먼저 방어적 복사를 한 후, 복사본으로 유효성을 검사하고 있다. 멀티스레딩 환경이라면 원본 객체의 유효성을 검사한 후, 복사본을 만드려고 하는 찰나 다른 스레드가 원본 객체를 수정할 위험이 있기때문이다.
위 방식은 여전히 접근자 메서드가 가변 정보를 직접 드러내기에 변경이 가능하다.
public Date start() {
return new Date(start.getTime);
}
public Date end() {
return new Date(end.getTime);
}
따라서 최종적으로 접근자도 방어적 복사본을 반환하도록 작성할 수 있다.
길이가 1 이상인 배열은 무조건 가변임을 잊지말고, 내부에서 사용하는 배열을 클라이언트에게 반환할 때는 항상 방어적 복사를 수행해야 한다. 되도록 불변 객체들을 조합해 객체를 구성해야 방어적 복사를 할 일이 줄어든다.
앞선 예에서 Date를 사용한다면, 자바8에서는 LocalDateTime이나 ZonedDateTime을 사용하는 방법이 있겠지만 이전에는 long 정수를 넘기는 방법이 있다.