티스토리 뷰

클라이언트가 불변식을 깨뜨리려 혈안이 되어 있다고 가정하고 방어적으로 프로그래밍해야 한다.

// 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)을 들 수 있다.
  • 래퍼 클래스의 특성상 클라이언트는 래퍼에 넘긴 객체에 여전히 직접 접근할 수 있다.
  • 따라서 래퍼의 불변식을 쉽게 파괴할 수 있지만 그 영향을 오직 클라이언트 자신만 받게된다.

결론

  • 클래스가 클라이언트로부터 받는 혹은 클라이언트로 반환하는 구성요소가 가변이라면 그 요소는 반드시 방어적으로 복사해야 한다.
  • 복사 비용이 너무 크거나 클라이언트가 그 요소를 잘못 수정할 일이 없음을 신뢰한다면 방어적 복사를 수행하는 대신 해당 구성요소를 수종했을 때의 책임이 클라이언트에 있음을 문서에 명시하도록 하자.