Search

아이템50 : 적시에 방어적 복사본을 만들라

“클라이언트로부터 받거나/반환하는 구성요소가 가변이면 해당 요소는 반드시 방어적으로 복사해야 한다.”

 클래스를 보호하자

C, C++ 언어와 비교했을 때 자바는 안전한 언어이다. 버퍼 오버런, 배열 오버런, 와일드 포인터 같은 메모리 충돌로부터 안전하다. 또 자바로 작성한 클래스는 시스템의 다른 부분에서 무슨 짓을 하든 불변식이 지켜진다. 그러나 아래와 같은 사례로 인해 클래스를 보호하는 데 충분한 시간을 투자하고 방어적으로 프로그래밍해야 한다.
1.
악의적인 의도를 가지고 시스템 보안을 뚫으려는 시도 증가
2.
프로그래머 실수로 인한 클래스 오작동

 Date 클래스로 보는 불변식 문제

Date now = new Date(System.currentTimeMillis()); Article firstArticle = new Article(now); Article secondArticle = new Article(now); //Before(1) : Mon Dec 27 22:36:10 KST 2021 //Before(2) : Mon Dec 27 22:36:10 KST 2021 System.out.println("Before(1) : " + firstArticle.getDate()); System.out.println("Before(2) : " + secondArticle.getDate()); //두번째 아티클 시간을 1시간 더한다. secondArticle.getDate().setTime(now.getTime() + 3600000); //After(2) : Mon Dec 27 23:36:10 KST 2021 //After(2) : Mon Dec 27 23:36:10 KST 2021 System.out.println("After(2) : " + firstArticle.getDate()); System.out.println("After(2) : " + secondArticle.getDate());
Java
Date 클래스는 가변 객체이다.
다른 코드에서 공유하여 사용한다면 한 쪽에서 변경한 값이 다른 부분에 영향을 미칠 수 있다.
자바 8 이후로는 LocalDateTime 혹은 ZonedDateTime으로 쉽게 해결할 수 있다.
Date는 낡은 API이니 더 이상 사용하면 안된다.

 방어적 복사(defensive copy)

class Article { private final Date date; public Article(Date date) { //checkDate(date); TOCTOU 공격 조심 //this.date = date; this.date = new Date(date.getTime()); checkDate(this.date); //복사본으로 유효성 검사 } public Date getDate() { return date; } private void checkDate(Date date) { ... } }
Java
외부로부터 보호하려면 가변 매개변수 각각을 방어적으로 복사(defensive copy)해야 한다.
즉, 인스턴스 안에서는 원본이 아닌 복사본을 사용한다.
매개변수 유효성을 검사하기 전에 방어적 복사본을 만들고, 이 복사본으로 유효성을 검사한다.
멀티스레딩 환경이라면 원본 객체의 유효성을 검사한 후
복사본을 만드는 그 찰나의 취약한 순간에
다른 스레드가 원본 객체를 수정할 위험이 있기 때문이다.
이를 검사시점/사용시점(time-of-check/time-of-use) 공격 혹은 TOCTOU 공격이라고 한다.

 방어적 복사에 clone 메서드를 사용하지 않은 이유?

clone이 악의적으로 잘못된 하위 클래스의 인스턴스를 반환할 수 있다.
매개변수가 제3자에 의해 확장될 수 있는 타입이라면 방어적 복사본을 만들 때 clone을 사용해서는 안 된다.
인스턴스를 복사하는 데는 일반적으로 생성자나 정적 팩터리를 쓰는게 좋다.
아이템13 : clone 재정의는 주의해서 진행하라

 접근자도 복사본을 반환하자

... public Date getDate() { return new Date(date.getTime()); //완벽한 불변으로 거듭나기 } ...
Java
위 코드처럼 새로운 접근자까지 갖춰야 완벽한 불변으로 거듭난다.
모든 필드가 객체 안에 완벽하게 캡슐화되었다.

 방어적 복사의 목적

꼭 불변 객체를 만들기 위함이 아니다.
메서드든 생성자든 클라이언트가 제공한 객체의 참조를 내부 자료구조에 보관해야 한다면 해당 객체가 잠재적으로 변경될 수 있는지 생각해야 한다.
내부 객체를 클라이언트에게 반환할 때는 반드시 심사숙고해야 한다.
확신할 수 없다면 방어적 복사본을 만들어 반환하자. (원본 제공 x)
길이가 1 이상인 배열은 무조건 가변임을 잊지 말자.
내부에서 사용하는 배열을 클라이언트에 반환할 때는 항상 방어적 복사를 수행해야 한다.

 방어적 복사의 단점은?

성능 저하가 따르고 항상 쓸 수 있는 것도 아니다.
호출자가 컴포넌트 내부를 수정하지 않으리라 확신하면 방어적 복사를 생략할 수 있다.
단, 매개변수나 반환값을 수정하지 말아야 함을 명확히 문서화하는 것이 좋다.
되도록 불변 객체들을 조합해 객체를 구성해야 방어적 복사를 할 일이 줄어든다.
아이템17 : 변경 가능성을 최소화하라

 핵심 정리

클라이언트로부터 받거나/반환하는 구성요소가 가변이라면 반드시 방어적으로 복사하자.
복사 비용이 크거나 클라이언트가 요소를 잘못 수정할 일이 없음을 신뢰한다면 방어적 복사 대신 수정했을 때의 책임이 클라이언트에 문제가 있음을 명시하자.(문서화)
불변 객체들을 조합하여 방어적 복사를 줄이자.