-
아이템50. 적시에 방어적 복사본을 만들라책/이펙티브자바 2021. 8. 30. 19:44
클라이언트가 불변식을 깨뜨리려 혈안이 되어 있다고 가정하고 방어적으로 프로그래밍해야 한다.
// 50-1. 불변 클래스인가 ?? 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 getStart() { return start; } public Date getEnd() { return end; } }
// 50-2. Period 내부 변경해보기 public static void main(String[] args) { Date start = new Date(); Date end = new Date(); System.out.println("end.year: " + end.getYear()); Period period = new Period(start, end); System.out.println("---------------------"); end.setYear(78); System.out.println("end.year: " + period.getEnd().getYear()); } // 121, 78 출력
- Period가 불변식인줄 알았지만 Date가 가변객체이기에 불변식이 아니다.
- 여기서는 Date를 쓰지만, 자바8이후에 등장한 Instnat, LocalDateTime, ZonedDateTime 을 쓰면 쉽게 해결된다. Date는 쓰지 않도록 한다. (Deprecated 되어있음)
외부 공격으로부터 인스턴스 내부를 보호하려면 생성자에서 받은 가변 매개변수 각각을 방어적으로 복사(defensive copy)해야 한다.
// 50-3. 수정한 생성자 - 방어적 복사 public Period(Date start, Date end) { this.start = new Date(start.getTime()); this.end = new Date(end.getTime()); if (start.compareTo(end) > 0) { throw new IllegalArgumentException( "start가 end보다 늦다." ); } }
Item49에서는 매개변수의 유효성을 먼저 검사하라 했는데 Item50 에서는 방어적 복사본을 만드는 작업을 먼저하고 그 다음에 유효성 검사를 한다.
멀티스레딩 환경이라면 원본 객체의 유효성을 검사한 후 복사본을 만드는 그 찰나의 취약한 순간에 다른 스레드가 원본 객체를 수정할 위험이 있기 때문이다.
방어적 복사를 매개변수 유효성 검사 전에 수행하면 이런 위험에서 해방될 수 있다.
이를 검사시점/사용시점(time-of-check/time-of-use) 공격 혹은 TOCTOU 공격이라 한다.
- 매개변수가 제 3자에 의해 확장될 수 있는 타입이라면 방어적 복사본을 만들 때 clone 을 사용해서는 안된다.
- 이전 아이템 13 에서 clone 을 다뤘었는데 기본 원칙은 '복제 기능은 생성자와 팩토리를 이용하는게 최고' 이고 배열만은 clone 메서드 방식이 가장 깔끔한 방법이라고 하였다.
// 50-4. Period 내부 변경해보기 public static void main(String[] args) { Date start = new Date(); Date end = new Date(); System.out.println("end.year: " + end.getYear()); Period period = new Period(start, end); System.out.println("---------------------"); period.end.setYear(78); System.out.println("end.year: " + period.end.getYear()); } // 121, 78 출력
// 50-5. getter 에서도 방어적 복사본을 반환한다. public Date getStart() { return new Date(start.getTime()); } public Date getEnd() { return new Date(end.getTime()); }
- 되도록 불변 객체들을 조합해 객체를 구성해야 방어적 복사를 할 일이 줄어든다.
- 방어적 복사에는 성능 저하가 따르고, 또 항상 쓸 수 있는 것은 아니다. (같은 패키지에 속하는 등의 이유로) 호출자가 컴포넌트 내부를 수정하지 않으리라 확신하면 방어적 복사를 생략할 수 있다.
- 이러한 상황이라도 호출자에서 해당 매개변수나 반환값을 수정하지 말아야 함을 명확히 문서화하는게 좋다.
- 방어적 복사를 생략해도 되는 상황은 해당 클래스와 그 클라이언트가 상호 신뢰할 수 있을 때, 혹은 불변식이 깨지더라도 그 영향이 오직 호출한 클라이언트로 국한될 때로 한정해야 한다.
- 후자의 예로는 래퍼 클래스 패턴 (아이템 18)을 들 수 있다.
- 래퍼 클래스의 특성상 클라이언트는 래퍼에 넘긴 객체에 여전히 직접 접근할 수 있다.
- 따라서 래퍼의 불변식을 쉽게 파괴할 수 있지만 그 영향을 오직 클라이언트 자신만 받게된다.
결론
- 클래스가 클라이언트로부터 받는 혹은 클라이언트로 반환하는 구성요소가 가변이라면 그 요소는 반드시 방어적으로 복사해야 한다.
- 복사 비용이 너무 크거나 클라이언트가 그 요소를 잘못 수정할 일이 없음을 신뢰한다면 방어적 복사를 수행하는 대신 해당 구성요소를 수종했을 때의 책임이 클라이언트에 있음을 문서에 명시하도록 하자.
'책 > 이펙티브자바' 카테고리의 다른 글
아이템40. @Override 애너테이션을 일관되게 사용하라 (0) 2021.08.31 아이템39. 명명 패턴보다 애너테이션을 사용하라 (0) 2021.08.31 아이템49. 매개변수가 유효한지 검사하라 (0) 2021.08.30 아이템38. 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라 (0) 2021.08.29 아이템35. ordinal 메서드 대신 인스턴스 필드를 사용하라 (0) 2021.08.28