티스토리 뷰

728x90
반응형

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


  • 자바는 안전한 언어입니다. 네이티브 메서드를 사용하지 않으니 C, C++ 같이 안전하지 않은 언어에서 흔히보는 버퍼 오버런, 배열 오버런, 와일드 포인터 같은 메모리 충돌 오류로부터 안전합니다. 자바로 작성한 클래스는 시스템의 다른 부분에서 무슨 짓을 하든 그 불변식이 지켜집니다. 메모리 전체를 하나의 거대한 배열로 다루는 언어안에서는 누릴 수 없는 강점입니다.

 

💡 문제가 발생하는 상황

  • 아래 Period 객체는 생성자에서 유효성 검사를 하고 있습니다. 하지만 main 메서드에서 객체 생성 후에 값을 수정하고 있어 문제가 발생할 수 있습니다.
  • 자바 8 이후로는 Date 클래스 대신 불변인 Instant를 사용하거나 LocalDateTime 또는 ZonedDateTime을 사용해도 됩니다.
  • Date는 낡은 API이니 새로운 코드를 작성할 때는 더이상 사용해서는 안됩니다.
  • 외부로부터 공격받은 Period 인스턴스의 내부를 보호하려면 생성자에서 받은 가변 매개변수 각각을 방어적 복사해야합니다. 그 다음 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(start + "가 " + end + "보다 늦습니다.");
        }
        this.start = start;
        this.end = end;
    }

    public Date getStart() {
        return start;
    }

    public Date getEnd() {
        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);

        start.setYear(78); // period의 내부 수정이 가능합니다.
    }
}

 

💡 문제 해결 방법

  • 매개변수의 유효성을 검사하기 전 방어적 복사본을 만들고 해당 복사본을 이용하여 유효성을 검사합니다.
  • 순서가 부자연스러워 보일 수 있으나, 반드시 이렇게 작성해야 합니다.
  • 멀티 스레딩 환경이라면 원본 객체의 유효성을 검사한 후 복사본을 만드는 그 찰나의 취약한 순간에 다른 스레드가 원본 객체를 수정할 위험이 있습니다.
  • 방어적 복사를 매개변수 유효성 검사 전에 수행하면 이런 위험에서 해방될 수 있습니다. 이를 컴퓨터 보안 커뮤니티에서는 검사시점/사용시점 공격 혹은 TOCTOU 공격이라 합니다.
  • Date는 final이 아니므로 clone이 Date가 정의한게 아닐 수도 있습니다. 즉 clone이 악의를 가진 하위 클래스의 인스턴스를 반환할 수도 있습니다. 그렇기 때문에 Date의 clone 메서드를 사용하지 않았습니다. 또한 매개변수가 제3자에 의해 확장될 수 있는 타입이라면 방어적 복사본을 만들 때 clone을 사용해서는 안됩니다.
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 (start.compareTo(end) > 0) {
            throw new IllegalArgumentException(start + "가 " + end + "보다 늦습니다.");
        }
    }

    public Date getStart() {
        return start;
    }

    public Date getEnd() {
        return end;
    }
}

 

💡 Period 인스턴스의 메서드를 통한 공격

public class Example {

    public static void main(String[] args) {
        Date start = new Date();
        Date end = new Date();
        Period period = new Period(start, end);
        period.getEnd().setYear(78); // Period 인스턴스의 메서드를 통한 공격
    }
}

 

💡 문제 해결 방법

  • 생성자에서는 방어적 복사본을 통해 공격으로부터 막았지만 getter와 같은 접근자 메서드를 통해 필드를 수정할려는 경우도 마찬가지로 방어적 복사본을 통해 해결할 수 있습니다.
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 (start.compareTo(end) > 0) {
            throw new IllegalArgumentException(start + "가 " + end + "보다 늦습니다.");
        }
    }

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

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

 

💡 너무 많은 방어적 복사..?

  • 객체를 생성할 때, 접근자를 통해 필드에 접근할려고 할 때마다 매번 새로운 객체를 생성하는 방어적 복사를 하게된다면 성능저하가 필수적으로 뒤따를 수 밖에 없습니다. 그렇기에 되도록이면 불변 객체를 조합해 객체를 구성함으로써 방어적 복사를 할 일 자체를 막아야 합니다.

 

💡 방어적 복사 생략

  • 방어적 복사의 목적은 외부에서 내부의 데이터를 함부로 변경할 수 없도록 막음으로써 통제권을 유지하는데 있습니다. 하지만 클라이언트에서 매개변수로 전달하는 인수들에 대한 통제권을 명백히 이전하고 수정하는 일이 없을 경우 방어적 복사본을 생략할 수 있습니다. 다만 이 경우에는 클라이언트가 통제권을 넘겨주는 가변 객체에 대한 내용을 통제권을 넘겨받는 메서드와 생성자에 문서화를 해야합니다.
  • 이렇게 통제권을 넘겨받기로 한 메서드, 생성자를 가진 클래스들은 취약할 수 밖에 없는데 따라서 해당 클래스와 클라이언트가 상호 신뢰할 수 있을 때, 혹은 불변식이 깨지더라도 그 영향이 오직 호출한 클라이언트에 국한될때 방어적 복사를 생략해도 됩니다.

 

✔️ 정리

  • 클래스가 클라이언트로부터 받은 혹은 클라이언트로 반환하는 구성 요소가 가변이라면 그 요소는 반드시 방어적 복사를 해야합니다.
  • 복사 비용이 너무 크거나 클라이언트가 그 요소를 잘못 수정할 일이 없음을 신뢰할 수 있다면 방어적 복사를 생략해도 되며 대신 해당 구성 요소가 수정되었을 경우 그 책임은 클라이언트에 있음을 문서화해야 합니다.

 

 

 

 

 

728x90
반응형