• 방어적 복사 기법은 일종의 [[/pattern/quotes#idiom]]{이디엄}이다.
    • 불변값/불변객체 패러다임을 기반으로 삼는 프로그래밍 언어에서는 이 패턴이 필요한 경우가 거의 없다.

예: 취약점이 있는 클래스

다음은 이펙티브 자바 3/E1 에 나오는 예제를 약간 수정한 것이다.

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; }
}
  • 위의 코드는 [[/jargon/TOCTTOU]]{TOCTOU(Time Of Check / Time Of Use) 취약점}이 있다. 즉 검사시점/사용시점 공격이 가능하다.

첫 번째 취약점

위의 클래스는 다음과 같은 취약점이 있다.

Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);

end.setYear(78);    // 취약점: p의 내부를 수정할 수 있다.

이 코드에 대해서 알아둘 몇 가지 사실이 있다.

  • Date는 Deprecated API 이므로 이제는 사용하지 않아야 한다.
  • 위의 코드에서 Date 대신 immutable 인 LocalDateLocalDateTime을 쓴다면 문제는 해결된다.

방어 기법: defensive copy

LocalDate를 사용하면 해결되는 문제지만, 꼭 Date만의 문제는 아니므로 방어할 방법을 알아둘 필요가 있다.

이펙티브 자바에서는 이 문제에 대해 defensive copy 기법을 소개한다. 생성자 인자로 받은 객체를 복사하는 방법이다.

public Period(Date start, Date end) {
    this.start = new Date(start.getTime()); // defensive copy
    this.end = new Date(end.getTime());     // defensive copy
    if (this.start.compareTo(this.end) > 0) {
        throw new IllegalArgumentException("start가 end보다 늦으면 안된다");
    }
}

이펙티브 자바에서는 이 문제에 대해 다음과 같이 언급한다.

매개변수의 유효성을 검사하기 전에 방어적 복사본을 만들고, 이 복사본으로 유효성을 검사한 점에 주목하자. 순서가 부자연스러워 보이겠지만 반드시 이렇게 작성해야 한다. 멀티스레딩 환경이라면 원본 객체의 유효성을 검사한 후 복사본을 만드는 그 찰나의 취약한 순간에 다른 스레드가 원본 객체를 수정할 위험이 있기 때문이다. 방어적 복사를 매개변수 유효성 검사 전에 수행하면 이런 위험에서 해방될 수 있다. 컴퓨터 보안 커뮤니티에서는 이를 검사시점/사용시점(time of check/time of use) 공격 혹은 영어 표기를 줄여서 TOCTOU 공격이라 한다.

방어적 복사에 Date의 clone 메서드를 사용하지 않은 점에도 주목하자. Date는 final이 아니므로 clone이 Date가 정의한 게 아닐 수 있다. 즉, clone이 악의를 가진 하위 클래스의 인스턴스를 반환할 수도 있다. (중략) 이런 공격을 막기 위해서는 매개변수가 제3자에 의해 확장될 수 있는 타입이라면 방어적 복사본을 만들 때 clone을 사용해서는 안 된다.

두 번째 취약점

그런데 생성자에 defensive copy를 적용했지만 아직 취약점이 또 있다.

Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);

p.getEnd().setTime(78); // 취약점: p의 내부를 변경할 수 있다.

getter도 방어한다

위의 취약점을 방어하려면 getter 메소드에도 다음과 같이 defensive copy 기법을 적용해 주면 된다.

public Date getStart() {
    return new Date(start.getTime());
}

public Date getEnd() {
    return new Date(end.getTime());
}

이제 모든 필드가 완벽하게 캡슐화되었다.

방어 기법: 불변 객체의 사용

defensive copy의 대안으로 함께 고려해볼만한 방법이다.

불변 객체를 만드는 가장 쉬운 방법은 Collectionsunmodifiable... 시리즈를 사용하는 것이다.

private static final Integer[] PRIVATE_VALUES = new Integer[]{ 1,2,3 };
public static final List<Integer> VALUES =
    Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));

함께 읽기

  • [[/jargon/TOCTTOU]]

참고문헌

  • 이펙티브 자바 Effective Java 3/E / 조슈아 블로크 저/개앞맵시 역 / 인사이트(insight) / 초판 2쇄 2018년 11월 21일

주석

  1. 이펙티브 자바 3/E. 아이템 50.