티스토리 뷰
반응형
자바는 C, C++ 와 다르게 메모리 충돌 오류에서 안전하다.
자바로 작성한 클래스는 항상 그 불변식이 지켜진다.
but 클라이언트가 불변식을 깨뜨릴 수 있다고 가정하고 방어적 프로그래밍을 해야함
불변식을 깨뜨리는 예시
public final class Period {
private final Date start;
private final Date end;
public Period(Date strart, Date end) {
if (start.compareTo(end) > 0)
throw new IllegalArgumentException(
start + "가 " + end + "보다 늦다.");
this.start = start;
this.end = end;
}
public Date start() {
return start;
}
public Date end() {
return end;
}
}
위 Date가 가변이라는 사실을 이용하면 어렵지 않게 이 클래스의 불변식을 깨뜨릴 수 있다.
//Period 인스턴스의 내부를 공격해보자.
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); //p의 내부를 수정
자바8 이후로는 낡은 API인 Date가 아닌 불변인 Instant를 사용하면 된다.
그러나 이전에 작성했던 코드를 대처해야한다.
방어적 복사(defensive copy)
위 코드의 Period 내부를 보호하기 위해선 생성자에서 받은 가변 매개변수 각각을 방어적으로 복사해야함
//수정한 생성자 - 매개변수의 방어적 복사본을 만들어 복사본을 사용
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()을 사용해서는 안됨 (Date는 final이 아니기 때문에 clone이 Date가 정의한게 아닐 수 있음 → 악의를 가진 하위 클래스의 인스턴스를 반환할 수도 있음)
접근자의 수정
위의 수정을 거쳐도 Period 인스턴스는 접근자 메서드 때문에 아직도 변경 가능하다.
→ 접근자가 가변 필드의 방어적 복사본을 반환하면 된다.
//수정한 접근자 - 필드의 방어적 복사본을 반환
public Date start() {
return new Date(start.getTime());
}
public Date end() {
return new Date(end.getTime());
}
생성자와 달리 접근자 메서드에서는 방어적 복사에 clone을 사용해도됨 (Period가 가지고 있는 Date 객체는 java.util.Date 가 확실하기 때문)
→ 그렇더라도 인스턴스 복사에는 생성자나 정적 팩터리가 좋음 [아이템 13]
방어적 복사의 필요성
- 클라이언트가 제공한 객체의 참조를 내부의 자료구조에 보관해야할때면 그 객체가 언젠가 변경될 수 있는지 생각해보고 변경 되었을때 문제 없이 동작할 지를 따져봐야함
→ 확신할 수 없다면, 방어적 복사본을 만들어 저장해라 - 되도록 불변 객체들을 조합해 객체를 구성해야 방어적 복사 할일이 줄어듦 [아이템17]
- 호출자가 내부를 수정하지 않는다고 확신하면 방어적 복사 생략 가능 → 이때도 호출자에서 해당 매개변수/반환값을 수정하지 말아야함을 문서화해라
- 방어적 복사를 생략해도 되는 상황은 클래스와 클라이언트가 상호 신뢰할 수 있을 때, 불변식이 깨지더라도 그 영향이 오직 호출한 클라이언트로 국한될 때 (래퍼 클래스 패턴)
결론
클래스가 클라이언트로부터 받는 혹은 클라이언트에게 반환하는 구성요소가 가변이라면 그 요소는 반드시 방어적 복사를 해야한다.
복사 비용이 너무 크거나 클라이언트가 그 요소를 잘못 수정할 일이 없다고 확신한다면 방어적 복사 대신 수정 시에 책임이 클라이언트에게 있음을 문서에 명시하자.
반응형
'Java > Effective Java' 카테고리의 다른 글
[Effective Java] 52.다중정의는 신중히 사용하라 (0) | 2022.04.04 |
---|---|
[Effective Java] 51.메서드 시그니처를 신중히 설계하라 (0) | 2022.04.04 |
[Effective Java] 49.매개변수가 유효한지 검사하라 (0) | 2022.04.04 |
[Effective Java] 48. 스트림 병렬화는 주의해서 적용하라 (0) | 2022.04.01 |
[Effective Java] 47. 반환 타입으로는 스트림보다 컬렉션이 낫다 (0) | 2022.04.01 |
댓글