클라이언트가 악의적으로 불변식을 깨드리려 한다고 가정하고 방어적으로 프로그래밍 하라
- 악의를 가진 프로그래머
- 평범한 프로그래머의 실수로 인한 오작동
- 어떤 객체든 그 객체의 허락 없이 외부에서 내부를 수정하지 못하도록 하라.
기간을 나타내는 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의 키로 사용할 경우 불변식이 꺠진다.)
결론
- 되도록 불변 객체들을 조합해서 객체를 구성한다.
- 그것이 안되면 방어적 복사를 한다.
- 호출자가 컴포넌트 내부를 수정하지 않으리라 확신하면 방어적 복사를 생략해도 된다. (예를 들어 같은 패키지의 경우)이런 경우에도 문서화하는 것이 좋다.
- 특히 통제권을 넘겨주거나 생성자를 가진 클래스들은 이러한 공격에 취약하다.
'JAVA > Effective Java' 카테고리의 다른 글
item 62) 다른 타입이 적절하다면 문자열 사용을 피하라 (0) | 2021.03.27 |
---|---|
item 61) 박싱된 기본 타입보다 기본 타입을 사용하라 (0) | 2021.03.27 |
item 46) 스트림에서는 부작용 없는 함수를 사용하라 (0) | 2021.02.27 |
item 42) 익명 클래스보다는 람다를 사용하라 (0) | 2021.02.11 |
item 64) 객체는 인터페이스를 사용해 참조하라. (0) | 2021.02.07 |