[Java] Item 50 : 적시에 방어적 복사본을 만들라
Effective Java를 읽고 정리한 정리본입니다.
📌 Item 50 : 적시에 방어적 복사본을 만들라
자바는 안전한 언어다.
네이티브 메서드에서 흔히 볼 수 있는 버퍼 오버런, 배열 오버런, 와일드 포인터 같은 메모리 충돌 오류에서 안전하다.
그러나 아무리 자바라 해도 다른 클래스로부터의 침범을 아무런 노력 없이 다 막을 수 있는 건 아니다.
그러니 클라이언트가 여러분의 불변식을 깨뜨리려 혈안이 되어 있다고 가정하고 방어적으로 프로그래밍해야 한다.
🫧 외부에서 내부를 수정하는 문제
어떤 객체든 그 객체의 허락 없이는 외부에서 내부를 수정하는 일은 불가능하다.
하지만 주의를 기울이지 않으면 자기도 모르게 내부를 수정하도록 허락하는 경우가 생긴다.
예컨대 기간을 표현하는 클래스는 한번 값이 정해지면 변하지 않도록 할 생각이었다.
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가 가변이라는 사실을 이용하면 어렵지 않게 그 불변식을 깨뜨릴 수 있다.
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // p의 내부를 수정했다...
다행히 이러한 문제는 쉽게 해결될 수 있다.
자바 8 이후 Date 대신 불변인 Instant를 사용하면 된다.
혹은 LocalDateTime이나 ZonedDateTime을 사용해도 된다.
하지만 앞으로 쓰지 않는다고 이 문제에서 해방되는 것은 아니다.
🫧 방어적 복사본 만들기 - 외부로부터 내부를 보호
외부 공격으로부터 Period 인스턴스의 내부를 보호하려면 생성자에서 받은 가변 매개변수 각각을 방어적으로 복사해야 한다.
그런 다음 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 + "보다 늦다.");
}
}
새로 작성한 생성자를 사용하면 앞서의 공격으 더 이상 Period에 위협이 되지 않는다.
매개변수의 유효성을 검사하기 전 방어적 복사본을 만들고, 이 복사본으로 유효성을 검사했다.
특히, 멀티스레딩 환경이라면 원본 객체의 유효성을 검사한 후 복사본을 만드는 그 찰나의 취약한 순간에 다른 스레드가 원본 객체를 수정할 위험이 있기 때문에 다음과 같은 순서로 진행해야 함을 명시하자.
컴퓨터 보안 커뮤니티에서는 이를 검사시점/사용시점 공격 혹은 영어 표기를 줄여 TOCTOU 공격이라 한다.
✨ 방어적 복사에서의 clone 메서드 사용
방어적 복사에 Date clone 메서드를 사용하지 않은 점에도 주목하자.
clone이 악의를 가진 하위 클래스의 인스터스를 반환할 수도 있다.
예컨대 이 하위 클래스는 start와 end의 필드의 참조를 private 정적 리스트에 담아뒀다가 공격자에게 이 리스트에 접근하는 길을 열어줄 수도 있다.
이런 공격을 막기 위해서는 매개변수가 제3자에 의해 확장될 수 있는 타입이라면 방어적 복사본을 만들 때 clone을 사용해서는 안 된다.
그러나 이 또한 접근자 메서드가 내부의 가변 정보를 직접 드러내기 때문에 접근자가 가변 필드의 방어적 복사본을 반환하도록 수정해야 한다.