본문 바로가기

JAVA/Effective Java

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

728x90

클라이언트가 악의적으로 불변식을 깨드리려 한다고 가정하고 방어적으로 프로그래밍 하라

- 악의를 가진 프로그래머

- 평범한 프로그래머의 실수로 인한 오작동

- 어떤 객체든 그 객체의 허락 없이 외부에서 내부를 수정하지 못하도록 하라.

 

기간을 나타내는 Period 객체를 만들어봤다.

import java.util.Date;

public final class Period {
    private final Date start; //Period의 멤버변수인 Date가 불변 객체가 아니므로 Period가 불변이 아니게 된다.
    private final Date end;

    public Period(Date start, Date end) {
        if (start.compareTo(end) > 0)
            throw new IllegalArgumentException(
                    start + " after " + end);
        this.start = start;
        this.end = end;
    }

    public Date getStart() {
        return start;
    }

    public Date getEnd() {
        return end;
    }
}



Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78);

Period의 멤버변수 Date가 내부적으로 가변이기 때문에 Period 또한 불변이 아니다. 그러므로 각종 RuntimeException, DeadLock에 취약해질 수 있다.

 

- 근본적으로 Date 대신 Java 8 버전의 불변 객체인 LocalDateTime,  Instant 등을 사용하고 Date를 더이상 사용하지 않으면 된다.

- 그러나 모든 케이스에 대해서 이렇게 할 순 없다.(호환성 등)

- 외부 공격으로부터 내부를 보호하기 위해 생성자에서 받은 가변 매개변수를 각각 방어적으로 복사(defensive copy)를 해준다.

 

import java.util.Date;

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 + " after " + end);
    }


	//getter에서 원본이 아니라 복사본을 반환한다.
    public Date getStart() {
        return new Date(start.getTime());
    }

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

반드시 방어적 복사본을 만들고 이 복사본으로 유효성을 검사해야한다. (순서에 유의한다.)

- 멀티 스레드 환경에서 원본객체의 유효성을 검사하고, 복사본을 만드는 그 순간에 다른 스레드가 원본 객체를 수정할 가능성이 있다.

(검사시점/사용시점 TOCTOU 공격)

TOCTOU 시나리오

쓰레드 1 악의를 가진 쓰레드 2
start.after(end) = false(유효성 검사)  
  end = 0 (위의 유효성 검사가 더 이상 유효하지 않다.)
복사  

방어적 복사에 clone 메소드를 사용하지 않는다.

- Date가 final 하지 않으므로 악의를 가진 하위 클래스를 반환할수도 있다.

- start, end 참조를 private static 리스트에 담아뒀다가 이 리스트에 접근하는 것을 공격자에게 허용할 수 있다.

 MaliciousDate someDate = new MaliciousDate();
 Date copyOfMaliciousDate = someDate;
 Date anotherDate = copyOfMaliciousDate.clone();

 

getter에서 원본이 아니라 방어적 복사본을 반환한다.

Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
p.end().setYear(78);

 

이로서 Period는 자신 말고는 가변 필드에 접근할방법이 없다.(네이티브 메서드나 리플렉션 같이 언어 외적인 방법 외에는 변경 불가능)

 

 

 메서드든 생성자든 클라이언트가 제공한 객체의 참조를 내부의 자료구조에 보관해야 할 때면 항시 그 객체가 잠재적으로 변경될 수 있는지를 생각해야 한다. (Set, Map이 불변이여도 가변 객체를 Map, Set의 키로 사용할 경우 불변식이 꺠진다.)

 

 

결론 

 

- 되도록 불변 객체들을 조합해서 객체를 구성한다.

- 그것이 안되면 방어적 복사를 한다.

- 호출자가 컴포넌트 내부를 수정하지 않으리라 확신하면 방어적 복사를 생략해도 된다. (예를 들어 같은 패키지의 경우)이런 경우에도 문서화하는 것이 좋다.

- 특히 통제권을 넘겨주거나 생성자를 가진 클래스들은 이러한 공격에 취약하다.