📌 방어적 프로그래밍
💡 클라이언트가 우리의 불변식을 깨뜨리려 혈안이 되어 있다고 가정하고 방어적으로 프로그래밍하라.
- 자바로 작성한 클래스는 시스템의 다른 부분에서 무슨짓을 하든 그 불변식이 지켜진다.
- 하지만 악의적인 의도를 가지거나, 순전히 실수로 클래스를 오작동하도록 만들 수 있다.
📌 As-is. 불변식을 지키지 못한 클래스
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 (this.start.compareTo(this.end) > 0)
throw new IllegalArgumentException(
this.start + "가 " + this.end + "보다 늦다.");
this.start = start;
this.end = end;
}
public Date start() {
return start;
}
public Date end() {
return end;
}
// 나머지 코드 생략
}
- 불변식은 "시작 시각이 종료 시각보다 늦을 수 없다"
- 얼핏 불변처럼 보이는 클래스지만 그렇지 않다.
🔱 Attack 1. 가변 인스턴스를 이용한 공격
class AttackPeriod {
public static void main(String[] args) {
// Date 인스턴스를 향한 참조를 얻는다.
Date start = new Date();
Date end = new Date();
// Period 인스턴스를 생성한다.
Period p = new Period(start, end);
// 내부를 훔쳐보자.
end.setYear(78); // p의 내부를 수정했다!
}
}
- Period 자체는 불변 클래스일지언정, 멤버 필드의 Date가 가변 객체임에 유의하라
- Date 인자를 침투시키고 클라이언트 측에서 수정하면, 쉽게 불변식을 깰 수 있다.
🛡️ 가변 대신 불변인 Instant를 사용하라
💡 Date는 낡은 API이니 새로운 코드를 작성할 때는 더 이상 사용하지 마라.
- Date 대신 불변(Item 17)인 Instant를 사용하라.
- LocalDateTime이나 ZonedDateTime을 사용해도 좋다.
🛡️ 생성자에서 받은 가변 매개변수 각각을 방어적으로 복사(defensive copy)하라
💡 매개변수의 유효성을 검사(Item 49)하기 전에 방어적 복사본을 만들고, 복사본으로 유효성을 검사하라.
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 + "보다 늦다.");
}
- TOCTOU 공격(검사시점/사용시점; time-of-check/time-of-use)에 주의하라
- 멀티 스레팅 환경에서 유효성을 검사하고 복사하는 찰나에 다른 스레드가 원본 객체를 수정할 수 있다.
💡 매개변수가 제 3자에 의해 확장될 수 있는 타입이라면, clone으로 방어적 복사를 수행하지 마라
class EvilDate extends Date {
private static List<Date> references = new ArrayList<>();
public EvilDate(Date date) {
super(date.getTime());
references.add(this);
}
// 악의적인 작업을 수행할 수 있는 메서드
// ...
}
- Date는 final이 아니므로, 이를 상속받은 하위 클래스의 인스턴스를 인자로 넘겼을 수도 있다.
- 하위 클래스의 clone()을 호출하여, Period 인스턴스의 주도권을 빼앗길 수도 있다.
🔱 Attack 2. 접근자 메서드를 이용한 공격
// Period 인스턴스의 내부를 공격해보자.
class AttackPeriod {
public static void main(String[] args) {
// Date 인스턴스를 향한 참조를 얻는다.
Date start = new Date();
Date end = new Date();
// Period 인스턴스를 생성한다.
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());
}
- 접근자 메서드까지도 방어적 복사를 수행하면, Period는 완전한 불변으로 거듭난다.
✒️ 접근자 메서드에서의 clone
접근자 메서드에서는 clone을 사용해도 된다.
생성자 단계에서 잘 막았다면, Period의 Date 객체는 java.util.Date임이 확실하기 때문이다.
그렇더라도 Item 13과 같은 이유로 인스턴스 복사하는 데는 일반적으로 생성자나 정적 팩터리를 사용하라.
📌 방어적 복사를 하는 이유
💡 되도록 불변 객체들을 조합해 객체를 구성해야 방어적 복사를 할 일이 줄어든다. (Item 17)
불변 객체를 만들기 위해서만은 아니다.
- 클라이언트가 제공한 객체의 참조를 내부 자료 구조에 저장하기 전에, 해당 클래스가 변경되어도 문제가 없다는 확신이 없다면 복사본을 만들어 저장하라.
- 가변인 내부 객체를 클라이언트에게 반환할 때도 안심할 수 없다면 방어적 복사를 수행하라.
- 길이가 1이상인 배열은 무조건 가변
- 이 경우엔 배열의 불변 view를 반환하는 방법도 좋다. (Item 15)
- 방어적 복사는 성능 저하가 따르고, 항상 쓸 수 있는 방법도 아니다.
- 클라이언트 측과 상호 신뢰할 수 있고, 불변식이 깨지더라도 영향이 호출한 클라이언트로 국한될 때는 생략해도 좋다.
- 래퍼 클래스 패턴(Item 18)의 경우, 래퍼의 불변식을 쉽게 파괴할 수 있지만 영향을 클라이언트만 받는다.