매개변수를 방어적으로 복사하는 목적이 불변 객체를 만들기 위해서만은 아니다.
메서드든 생성자든 클라이언트가 제공한 객체의 참조를 내부의 자료구조에 보관해야 할 때면 항시 그 객체가 잠재적으로 변경될 수 있는지를 생각해야 한다.
변경될 수 있는 객체라면 그 객체가 클래스에 넘겨진 뒤 임의로 변경되어도 그 클래스가 문제없이 동작할 지를 따져보라
확신할 수 없다면 복사본을 만들어 저장해야 한다.
복사 비용이 너무 크거나 클라이언트가 그 요소를 잘못 수정할 일이 없음 을 신뢰한다면 방어적 복사를 수행하는 대신 해당 구성요소를 수정 했을 때의 책임이 클라이언트에 있음 을 문서에 명시하도록 하자.
주제
- 자바는 안전한 언어다. 네이티브 메서드를 사용하지 않으니 C, C++ 같이 안전하지 않은 언어에서 흔히 보는 버퍼 오버런, 배열 오버런, 와일드 포인터 같은 메모리 충돌 오류에서 안전함
- 자바로 작성한 클래스는 시스템의 다른 부분에서 무슨 짓을 하든 그 불변식이 지켜진다. 메모리 전체를 하나의 거대한 배열로 다루는 언어에서는 누릴 수 없는 강점이다.
하지만
아무리 자바라 해도 다른 클래스로 부터의 침범을 아무런 노력 없이 다 막을 수 있는 것은 아니다. 클라이언트가 여러분의 불변식을 깨트리려 혈안이 되어 있다고 가정하고 방어적으로 프로그래밍 해야 한다.
불변식을 지키지 못하는 사례
public final class Period { private final Date start; private final Date end; /** * @param start the beginning of the period * @param end the end of the period; must not precede start * @throws IllegalArgumentException if start is after end * @throws NullPointerException if start or end is null */ public Period(Date start, Date end) { if (start.compareTo(end) > 0) throw new IllegalArgumentException( start + " after " + end); this.start = start; this.end = end; } } Date start = new Date(); Date end = new Date(); Period p = new Period(start, end); end.setYear(78); // p의 내부를 수정!!
해결책
해결방안
: Date 대신 불변인Instant
나LocalDateTime
,ZonedDateTime
을 사용
Date는 낡은 API 이니 새로운 코드를 작성할 때는 더 이상 사용하면 안된다.
- 생성자에서 받은 가변 매개변수 각각을 방어적으로 복사해야 한다. (Legacy라 Date를 계속 사용해야 하는경우)
- 매개변수의 유효성을 검사하기 전에 방어적 복사본을 만든것에 주의! 멀티스레딩 환경에서 유효성 검사 후 복사본을 만들게 되면 그 사이에 원본객체가 수정될수 있으니(이를 TOCTOU 공격이라 함)
- 방어적 복사에 clone 메서드 사용하지 않은 것도 주의 (Date는 final 이 아니므로 clone이 Date가 정의한 게 아닐 수도 있음!)
// Repaired constructor - makes defensive copies of parameters (Page 232) 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); }
추가적 공격
Date start = new Date(); Date end = new Date(); Period p = new Period(start, end); p.end().setYear(70);
- 이를 막기 위해서는 단순히 접근자가 가변 필드의 방어적 복사본을 반환
교훈
- 이상의 모든 작업에서 우리는
되도록 불변 객체들을 조합해 객체를 구성해야 방어적 복사를 할 일이 줄어든다
는 교훈을 얻을 수 있다.
방어적 복사 단점과 생략가능 경우
- 방어적 복사에는 성능 저하가 따르고, 또 항상 쓸 수 있는 것도 아니다. (같은 패키지에 속하는 등의 이유로) 호출자가 컴포넌트 내부를 수정하지 않으리라 확신하면 방어적 복사를 생략할 수 있다.
- 다른 패키지에서 사용한다고 해서 넘겨받은 가변 매개변수를 항상 방어적으로 복사해 저장해야 하는 것은 아니다
- 때로는 메서드나 생성자의 매개변수로 넘기는 행위가 그 객체의 통제권을 명백히 이전함을 뜻하기도 함.
- 이처럼 통제권을 이전하는 메서드를 호출하는 클라이언트는 해당 객체를 더 이상 직접 수정하는 일이 없다고 약속해야 함
방어적 복사를 생략해도 되는 상황은 해당 클래스와 그 클라이언트가 상호 신뢰할 수 있을 때,
혹은 불변식이 깨지더라도 그 영향이 오직 호출한 클라이언트로 국한될 때로 한정해야 한다.
후자의 예는 래퍼 클래스 패턴