[Effective Java] 아이템50- 적시에 방어적 복사본을 만들라

클라이언트가 우리의 불변식을 깨뜨리려 혈안이 되어있다고 가정하고 방어적으로 프로그래밍해야 한다. 어떤 객체든 그 객체의 허락 없이는 외부에서 내부를 수정하는 일은 불가능하다. 하지만 주의를 기울이지 않으면 자기도 모르게 내부를 수정하도록 허락하는 경우가 생긴다. 아래 불변 클래스를 보자

public final class Period {
    private final Date start;
    private final Date end;

    /**
     * @param  start 시작 시각
     * @param  end 종료 시각. 시작 시각보다 뒤여야 한다.
     * @throws IllegalArgumentException 시작 시각이 종료 시각보다 늦을 때 발생한다.
     * @throws NullPointerException start나 end가 null이면 발생한다.
     */
    public Period(Date start, Date end) {
        if (start.compareTo(end) > 0)
            throw new IllegalArgumentException(
                    start + "가 " + end + "보다 늦다.");
        this.start = start;
        this.end   = end;
    }

    public Date start() {
        return start;
    }
    public Date end() {
        return end;
    }

    public String toString() {
        return start + " - " + end;
    }
}

final 을 사용해서 불변 클래스로 만들었고, 멤버 변수인 start와 end 도 final로 선언했다. 하지만 Date가 가변이라는 사실을 이용하면 어렵지 않게 그 불변식을 깨뜨릴 수 있다.

공격!

public static void main(String[] args) {
  Date start = new Date();
  Date end = new Date();
  Period p = new Period(start, end);
  end.setYear(78); // 수정됐다!
}

자바8 이후부터는 Date 대신 불변인 Instant를 사용하면 된다.

Date는 낡은 API이니 새로운 코드를 작성할 떄는 더 이상 사용하면 안된다. 😱

매개변수를 방어적으로 복사하자 (defensive copy)

매개변수로 받은 Date가 가변이므로 이를 복사해서 Period 인스턴스 안에서는 원본이 아닌 복사본을 사용한다.

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 + "가 " + this.end + "보다 늦다.");
}

TOCTOU 공격 (time-of-check/time-of-use)

멀티 스레딩 환경이라면 원본 객체의 유효성을 검사한 후 복사본을 만드는 그 찰나의 취약한 순간에 다른 스레드가 원본 객체를 수정할 위험이 있다.

따라서, 매개변수의 유효성 검사를 하기 전에 방어적 본사본을 만들어 두고, 이 복사본으로 유효성을 검사하자

방어적 복사를 위해 clone을 사용하지 않은 것도 주목하자. Date는 final이 아니므로 언제든지 확장할 수 있다. 따라서 Date 의 clone 메서드가 하위 클래스의 clone 일 수도 있다.

접근자 메서드를 사용하자

현재 start 와 end는 직접 접근 가능하다. 접근자 메서드의 반환값 또한 가변 필드의 방어적 복사본을 반환하면 된다.


방어적 복사에 참고해야할 사항

  • 매개변수를 방어적으로 복사하는 목적이 불변 객체를 만들기 위해서만은 아니다. 변경될 수 있는 객체라면 그 객체가 클래스에 넘겨진 뒤 임의로 변경되어도 그 클래스가 문제없이 동작할지를 따져봐라. 예를 들어 클라이언트가 건네준 객체를 Map 인스턴스의 키로 사용한다면, 추후 그 객체가 변경될 경우 객체를 담고있는 Map의 불변식이 깨질 것이다.

  • 길이가 1 이상인 배열은 무조건 가변이므로 내부에서 사용하는 배열을 클라이언트에 반환할 때는 항상 방어적 복사를 수행해야 한다. (혹은 배열의 불변 뷰를 반환하는 대안도 있다.)

  • 되도록 불면 객체들을 조합해 객체를 구성해야 방어적 복사를 할 일이 줄어든다. 앞서 살펴본 Period 객체인 경우 Instant를 사용하면 된다.

  • 방어적 복사를 생략해도 되는 상황은 해당 클래스와 그 클라이언트가 서로 상호 신뢰할 수 있을 때, 혹은 불변식이 깨지더라도 그 영향이 오직 호출한 클라이언트로 국한될 때 뿐이다.

댓글남기기