본문 바로가기

독서찰기(讀書札記)/이펙티브 자바

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

[Why]

자바는 안전한 언어다. 하지만 아무리 자바라 해도 다른 클래스로부터의 침범을 아무런 노력 없이 다 막을 수 있는 건 아니다. 그러니 클라이언트가 여러분의 불변식을 깨뜨리려 혈안이 되어 있다고 가정하고 방어적으로 프로그래밍해야 한다.

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;
    }
    
    ... // 나머지 코드 생략
}
  • 얼핏 이 클래스는 불변처럼 보이고, 시작 시각이 종료 시각보다 늦을 수 없다는 불변식이 무리 없이 지켜질 것 같다.
  • 하지만 Date가 가변이라는 사실을 이용하면 어렵지 않게 그 불변식을 깨뜨릴 수 있다.
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78);    // p의 내부를 수정했다!
  • 자바 8 이후로는, Date 대신 불변인 Instant를 사용하면 된다.
    (혹은 LocalDateTime이나 ZonedDateTime을 사용해도 된다)

매개변수를 방어적으로 복사하는 목적이 불변 객체를 만들기 위해서만은 아니다. 클라이언트가 제공한 객체의 참조를 내부의 자료구조에 보관할 때 그 객체가 변경된다면 우리의 클래스가 문제없이 동작하는지 따져봐야하기 때문이다.

  • 예컨대 클라이언트가 건네준 객체를 내부의 Set 인스턴스에 저장하거나 Map 인스턴스의 키로 사용한다면, 추후 그 객체가 변경될 경우 객체를 담고 있는 Set 혹은 Map의 불변식이 깨질 것이다.
  • 길이가 1 이상인 배열은 무조건 가변이다!

 

[How]

외부 공격으로부터 인스턴스 내부를 보호하려면 생성자에서 받은 가변 매개변수 각각을 방어적으로 복사(defensive copy)해야 한다. 그런 다음 인스턴스 안에서는 원본이 아닌 복사본을 사용한다.

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 + "보다 늦다.");
    }
}
  • 매개변수의 유효성을 검사(아이템49)하기 전에 방어적 복사본을 만들고, 이 복사본으로 유효성을 검사한 점에 주목하자.
    • 멀티스레딩 환경이라면 원본 객체의 유효성을 검사한 후 복사본을 만드는 그 찰나의 취약한 순간에 다른 스레드가 원본 객체를 수정할 위험이 있기 때문이다.
    • 컴퓨터 보안 커뮤니티에서는 이를 검사시점/사용시점(time-of-check/time-of-use) 공격 혹은 영어 표기를 줄여서 TOCTOU 공격이라 한다.
  • 방어적 복사에 Date의 clone 메서드를 사용하지 않은 점에도 주목하자. 매개변수가 제3자에 의해 확장될 수 있는 타입이라면 방어적 복사본을 만들 때 clone을 사용해서는 안 된다.
    • Date는 final이 아니므로 clone이 Date가 정의한 게 아닐 수 있다.
    • 즉, clone이 악의를 가진 하위 클래스의 인스턴스를 반환할 수도 있다.
    • 예컨대 이 하위 클래스는 start와 end 필드의 참조를 private 정적 리스트에 담아뒀다가 공격자에게 이 리스트에 접근하는 길을 열어줄 수도 있다.

Period 인스턴스는 생성자를 수정했지만, 접근자 메서드가 내부의 가변 정보를 직접 드러내기 때문에 아직도 변경 가능하다.

Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
p.end().setYear(78);    // p의 내부를 변경했다!
  • 이 공격을 막아내려면 단순히 접근자가 가변 필드의 방어적 복사본을 반환하면 된다.
public Date start() {
    return new Date(start.getTime());
}

public Date end() {
    return new Date(end.getTime());
}
  • 생성자와 달리 접근자 메서드에서는 방어적 복사에 clone을 사용해도 된다.
    • Period가 가지고 있는 Date 객체는 java.util.Date임이 확실하기 때문이다.
    • 그렇더라도 아이템13에서 설명한 이유 때문에 인스턴스를 복사하는 데는 일반적으로 생성자나 정적 팩터리를 쓰는 게 좋다.

되도록 불변 객체들을 조합해 객체를 구성해야 방어적 복사를 할 일이 줄어든다! (아이템17)

 

다른 패키지에서 사용한다고 해서 넘겨받은 가변 매개변수를 항상 방어적으로 복사해 저장해야 하는 것은 아니다.

  • 방어적 복사를 생략해도 되는 상황은 해당 클래스와 그 클라이언트가 상호 신뢰할 수 있을 때, 혹은 불변식이 깨지더라도 그 영향이 오직 호출한 클라이언트로 국한될 때로 한정해야 한다.
  • 후자의 예로는 래퍼 클래스 패턴(아이템18)을 들 수 있다.