티스토리 뷰

반응형

자바는 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]
  • 호출자가 내부를 수정하지 않는다고 확신하면 방어적 복사 생략 가능 → 이때도 호출자에서 해당 매개변수/반환값을 수정하지 말아야함을 문서화해라
  • 방어적 복사를 생략해도 되는 상황은 클래스와 클라이언트가 상호 신뢰할 수 있을 때, 불변식이 깨지더라도 그 영향이 오직 호출한 클라이언트로 국한될 때 (래퍼 클래스 패턴)

 

결론

클래스가 클라이언트로부터 받는 혹은 클라이언트에게 반환하는 구성요소가 가변이라면 그 요소는 반드시 방어적 복사를 해야한다.

복사 비용이 너무 크거나 클라이언트가 그 요소를 잘못 수정할 일이 없다고 확신한다면 방어적 복사 대신 수정 시에 책임이 클라이언트에게 있음을 문서에 명시하자.

반응형
댓글
반응형
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday