Effective Java #39 필요하다면 방어적 복사본을 만들라.
자바를 사용하는 것은 안전한 언어이기 때문이다. 네이티브 메서드를 사용하지 않으면 버퍼 오버런, 배열 오버런, 와일드 포인터 같은 메모리 훼손 오류가 생기지 않는다.
이런 문제들은 C나 C++ 처럼 안전하지 않은 언어를 쓸 때 빈번히 발생한다. 안전한 언어를 사용하면 시스템의 다른 부분에서 무슨 일이 생기더라도 불변식이 항구적으로 만족되는 클래스를 작성할 수 있다.
(C나 C++이 왜 안전하지 않은건지 이해가 안간다.. 쓰는 사람의 차이가 아닐까 ?)
안전한 언어를 쓴다고 해도 노력하지 않으면 다른 클래스의 영향으로부터 자유로울 수는 없다.
아래 예제는 클래스의 클라이언트가 불변식을 망가뜨리기 위해 최선을 다할 것이라는 가정 하에 방어적인 프로그래밍을 해야한다.
시스템의 보안을 무너뜨리는 악의적인 사용자, 그리고 API를 이상하게 사용하는 프로그래머가 있을 수 있다. 그러니 클라이언트가 이상한 짓을 해도 안정적으로 동작하는 클래스를 만들기 위해 노력할 필요가 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | 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 getStart() { return start; } public Date getEnd() { return end; } public static void main(String[] args) { } } |
얼핏 보면 Start와 end가 final에 private로 되어 있어 불가능해 보이지만 Date는 변경 가능 클래스라는 것을 이용한다면 불변식을 깨뜨릴 수 있다.
1 2 3 4 5 6 7 | public static void main(String[] args) { Date start = new Date(); Date end = new Date(); Period p = new Period(start, end); end.setYear(78); } |
Peroid 객체 내부의 값을 변경가능하게 만드려면 반드시 방어적으로 복사해야한다.
1 2 3 4 5 6 7 8 | 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(start + " after " + end); } } |
이렇게 하면 앞에서 했던 공격을 막을 수 있다. 인자의 유효성을 검사하기 전에 방어적 복사본을 만든다는 것을 유의하자.
위의 생성자를 사용하면 생성자 인자를 통한 공격은 막을 수 있지만 접근자를 통한 공격은 막을 수 없다.
1 2 3 4 5 6 7 | public static void main(String[] args) { Date start = new Date(); Date end = new Date(); Period p = new Period(start, end); p.getEnd().setYear(78); } |
접근자(getEnd)를 통해 얻은 end객체를 통해 내부를변경할 수 있다.
공격을 막기 위해 변경 가능 내부 필드에 대한 방어적 복사본을 반환하도록 접근자를 수정해야 한다.
1 2 3 4 5 6 7 8 9 | public Date getStart() { return new Date(start.getTime()); } public Date getEnd() { return new Date(end.getTime()); } |
생성자와 접근자를 수정하면 진정한 변경 불가능 클래스가 된다.
방어적 복사는 변경 불가능 클래스에만 쓰이는 기법은 아니다. 클라이언트가 제공한 객체를 내부 자료 구조에 반영하는 생성자나 메서드에는 사용 가능하다.
방어적 복사본을 만들도록 하면 성능에서 손해를 보기 때문에, 적절치 않을 때도 있다. 클라이언트가 같은 패키지가 있다거나 하는 이유로 클라이언트가 객체의 내부 상태를 변경하려 하지 않는다는 것이 확실하다면 방어적 복사본을 만들지 않아도 된다.. 그런 상황에서는 클래스문서에 메서다 호출자가 인자나 반환값을 변경하면 안된다는 사실을 명시해야 할 것이다.
요약
클라이언트로부터 구했거나 클라이언트에게 반환되는 변경 가능 컴포턴트가 있는 경우, 해당 클래스는 그 컴포넌트를 반드시 방어적으로복사해야 한다. 복사 오버헤드가 너무 크고 클래스 사용자가 그 내부 컴포넌트를 부적절하게 변경하지 않는다는 보장이 있을 때는, 방어적 복사를 하는 대신 클라이언트 측에서 해당 컴포턴트를 변경해서는 안된다는 사실을 명시하자.