적시에 방어적 복사본을 만들라

외부로부터 안전한 불변 클래스를 만들자

Posted on 2019-10-10

적시에 방어적 복사본을 만들라


자바로 작성한 클래스는 시스템의 다른 부분에서 무슨 짓을 하든 그 불변식이 지켜진다.
하지만 아무리 자바라 해도 다른 클래스로부터의 침범을 아무런 노력 없이 다 막을 수 있는건 아니다.

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

어떤 객체든 그 객체의 허락 없이는 외부에서 내부를 수정하는 일은 불가능하다. 하지만 주의를 기울이지 않으면 자기도 모르게 내부를 수정하도록 허락하는 경우가 생긴다.



// 불변식을 지키지 못한 기간을 표현하는 Period
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();
        
        this.start = start;
        this.end = end;
    }
    
    public Date start() {
        return start;
    }
    
    public Date end() {
        return end;
    }
}

public class Example {
    public static void main(String[] args) {
        Date start = new Date();
        Date end = new Date();
        Period period = new Period(start, end);
        end.setYear(2002); // (1)
    }
}

  • (1) : 얼핏 불변처럼 보이지만 Date는 변경될 수 있기 때문에 불변식을 깨뜨릴 수 있다. 외부 공격으로부터 인스턴스의 내부를 보호하려면 생성자에서 받은 가변 매개변수 각각을 방어적으로 복사해야 한다.

Date 참조 대신 Date.getTime()이 반환하는 long 정수를 사용하거나 자바8 이후 버전이라면 Instant, LocalDateTime, ZonedDateTime을 사용하는 것이 좋다.


// 방어적 복사를 적용한 생성자
public final class Period {
    private final Date start;
    private final Date end;
    
    public Period(Date start, Date end) {
        this.start = new Date(start.getTime()); // (1)
        this.end = new Date(end.getTime());
        
        if (this.start.compareTo(this.end) > 0) // (2)
            throw new IllegalArgumentException();
    }
    
    public Date start() {
        return start;
    }
    
    public Date end() {
        return end;
    }
}

public class Example {
    public static void main(String[] args) {
        Date start = new Date();
        Date end = new Date();
        Period period = new Period(start, end);
        end.setYear(2002); // period 영향 없음
        period.end().setYear(2002); // (3)
    }
}

  • (1) : 매개변수의 방어적 복사를 통해 외부로부터 변경을 방지할 수 있다.
  • (2) : 매개변수 유효성 검사 순서가 변경되었다. 왜냐하면 멀티스레딩 환경이라면 원본 객체의 유효성을 검사한 후 복사본을 만드는 그 찰나의 취약한 순간에 다른 스레드가 원본 객체를 수정할 위험이 있기 때문에 방어적 복사를 매개변수 유효성 검사 전에 수행하면 이런 위험에서 해방될 수 있다. (TOCTOU 공격)
  • (3) : 생성자를 위와 같이 수정하면 이전 공격은 막아낼 수 있지만, 접근자 메서드가 내부의 가변 정보를 직접 드러내기 때문에 아직 변경이 가능하다.

// 방어적 복사를 적용한 접근자
public final class Period {
    private final Date start;
    private final Date end;
    
    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();
    }
    
    public Date start() {
        return new Date(start.getTime()); // (1)
    }
    
    public Date end() {
        return new Date(end.getTime());
    }
    
}

public class Example {
    public static void main(String[] args) {
        Date start = new Date();
        Date end = new Date();
        Period period = new Period(start, end);
        end.setYear(2002); // period 영향 없음
        period.end().setYear(2002); // period 영향 없음
    }
}

  • (1) : 접근자가 가변 필드의 방어적 복사본 반환을 통해 외부로부터 변경을 방지할 수 있다.

위와 같이 작성하면 Period는 시작 시각이 종료 시각보다 나중일 수 없는 완벽한 불변으로 거듭난다.

방어적 복사에는 성능 저하가 따르고, 항상 쓸 수 있는 것도 아니다.
방어적 복사를 생략해도 되는 상황은 해당 클래스와 그 클라이언트가 상호 신뢰할 수 있을 때, 혹은 불변식이 깨지더라도 그 영향이 오직 호출한 클라이언트로 국한될 때로 한정해야 한다. 이러한 상황이라도 호출자에서 해당 매개변수나 반환값을 수정하지 말아야 함을 명확히 문서화하는 것이 좋다.


핵심 정리

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


Reference
  • Effective Java