HomeAboutMeBlogGuest
© 2025 Sejin Cha. All rights reserved.
Built with Next.js, deployed on Vercel
📖
공부한 책
/
📒
Effective Java
/
아이템 50: 적시에 방어적 복사본을 만들라

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

주제불변식을 지키지 못하는 사례해결책추가적 공격교훈방어적 복사 단점과 생략가능 경우
💡
매개변수를 방어적으로 복사하는 목적이 불변 객체를 만들기 위해서만은 아니다. 메서드든 생성자든 클라이언트가 제공한 객체의 참조를 내부의 자료구조에 보관해야 할 때면 항시 그 객체가 잠재적으로 변경될 수 있는지를 생각해야 한다. 변경될 수 있는 객체라면 그 객체가 클래스에 넘겨진 뒤 임의로 변경되어도 그 클래스가 문제없이 동작할 지를 따져보라 확신할 수 없다면 복사본을 만들어 저장해야 한다. 복사 비용이 너무 크거나 클라이언트가 그 요소를 잘못 수정할 일이 없음 을 신뢰한다면 방어적 복사를 수행하는 대신 해당 구성요소를 수정 했을 때의 책임이 클라이언트에 있음 을 문서에 명시하도록 하자.

주제

  • 자바는 안전한 언어다. 네이티브 메서드를 사용하지 않으니 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의 내부를 수정!!

해결책

  1. 해결방안 : Date 대신 불변인 Instant나 LocalDateTime, ZonedDateTime을 사용
    1. Date는 낡은 API 이니 새로운 코드를 작성할 때는 더 이상 사용하면 안된다.
  1. 생성자에서 받은 가변 매개변수 각각을 방어적으로 복사해야 한다. (Legacy라 Date를 계속 사용해야 하는경우)
    1. // 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); }
      • 매개변수의 유효성을 검사하기 전에 방어적 복사본을 만든것에 주의! 멀티스레딩 환경에서 유효성 검사 후 복사본을 만들게 되면 그 사이에 원본객체가 수정될수 있으니(이를 TOCTOU 공격이라 함)
      • 방어적 복사에 clone 메서드 사용하지 않은 것도 주의 (Date는 final 이 아니므로 clone이 Date가 정의한 게 아닐 수도 있음!)

추가적 공격

Date start = new Date(); Date end = new Date(); Period p = new Period(start, end); p.end().setYear(70);
  • 이를 막기 위해서는 단순히 접근자가 가변 필드의 방어적 복사본을 반환

교훈

  • 이상의 모든 작업에서 우리는 되도록 불변 객체들을 조합해 객체를 구성해야 방어적 복사를 할 일이 줄어든다는 교훈을 얻을 수 있다.

방어적 복사 단점과 생략가능 경우

  • 방어적 복사에는 성능 저하가 따르고, 또 항상 쓸 수 있는 것도 아니다. (같은 패키지에 속하는 등의 이유로) 호출자가 컴포넌트 내부를 수정하지 않으리라 확신하면 방어적 복사를 생략할 수 있다.
  • 다른 패키지에서 사용한다고 해서 넘겨받은 가변 매개변수를 항상 방어적으로 복사해 저장해야 하는 것은 아니다
    • 때로는 메서드나 생성자의 매개변수로 넘기는 행위가 그 객체의 통제권을 명백히 이전함을 뜻하기도 함.
    • 이처럼 통제권을 이전하는 메서드를 호출하는 클라이언트는 해당 객체를 더 이상 직접 수정하는 일이 없다고 약속해야 함
💡
방어적 복사를 생략해도 되는 상황은 해당 클래스와 그 클라이언트가 상호 신뢰할 수 있을 때, 혹은 불변식이 깨지더라도 그 영향이 오직 호출한 클라이언트로 국한될 때로 한정해야 한다. 후자의 예는 래퍼 클래스 패턴