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

김종준·2023년 7월 24일
0

이펙티브자바

목록 보기
43/63

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

자바는 안전한 언어다.

하지만 아무리 자바라 해도 다른 클래스로부터의 침범을 아무런 노력 없이 다 막을 수 있는 건 아니다.

그러니 클라이언트가 불변식을 깨뜨리려 혈안이 되어 있다고 가정하고 방어적으로 프로그래밍하여야 한다.

어떤 객체든 그 객체의 허락 없이는 외부에서 내부를 수정하는 일은 불가능하다.

하지만 주의를 기울이지 않으면 자기도 모르게 내부를 수정하도록 허락하는 경우가 생긴다.

예를 들어 기간을 표현하는 다음 클래스는 한번 값이 정해지면 변하지 않도록 할 생각이다.

public final class Period {

    private final Date start;
    private final Date end;

    public Period(Date start, Date end) {
        this.start = start;
        this.end = end;
    }
    
    public Date start() {
        return start;
    }   
    
    public Date end() {
        return end;
    }
}

final 선언도 되어 있고 이 클래스는 불변처럼 보인다.

하지만 Date가 가변이라는 사실을 사용하면 다음과 같이 어렵지 않게 불변을 깨뜨릴 수 있다고 한다.

(즉, 클래스를 final과 같이 불변으로 선언하여도 내부 맴버가 가변이라면 불변이 깨질 가능성이 있는 것이다.)

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

이러한 외부 공격에서 내부를 보호하려면 생성자에서 받은 가변 매개변수 각각을 방어적 복사해야 한다.

그런 다음 인스턴스 안에서는 원본이 아닌 복사본을 사용하는 것이다.

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

위의 새로 작성한 생성자의 경우 매개변수의 유효성을 검사하기 전에 방어적 복사본을 만들고, 이 복사본으로 유효성을 검사하였다.

순서가 부자연스러워 보이지만 반드시 이렇게 작성해야 한다.

멀티스레딩 환경이라면 원본 객체의 유효성ㅇ르 검사한 후 복사본을 만드는 그 찰나의 취약한 순간에 다른 스레드가 원본 객체를 수정할 위험이 있기 때문이다.

방어적 복사를 매개변수 유효성 검사 전에 수행하면 이런 위험에서 벗어날 수 있다.

하지만 여전히 불변을 깨뜨릴 수 있다.

접근자 메서드가 내부의 가변 정보를 직접 드러내기 때문이다.

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

위와 같은 경우를 막으려면 가변 필드의 방어적 복사본을 반환하면 된다.

public Date start() {
    return new Date(start.getTime());
}

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

이렇게 새로운 접근자까지 갖추면 완벽한 불변으로 거듭난다.

매개변수를 방어적으로 복사하는 목적이 불변 객체를 만들기 위해서만은 아니다.

메서드든 생성자든 클라이언트가 제공한 객체의 참조를 내부의 자료구조에 보관해야 할 때면 항시 그 객체가 잠재적으로 변경될 수 있는지를 생각해야 한다.

변경될 수 있는 객체라면 그 객체가 클래스에 넘겨진 뒤 임의로 변경되어도 그 클래스를 문제없이 동작할지 따져보자.

확신할 수 없다면 복사본을 만들어 저장해야 한다.

예를 들어 클라이언트가 건네준 객체를 내부의 Set 인스턴스에 저장하거나 Map 인스턴스의 키로 사용한다면, 추후 그 객체가 변경될 경우 객체를 담고 있는 Set 혹은 Map의 불변식이 깨질 것이다.

내부 객체를 클라이언트에 건네주기 전에 방어적 복사본을 만드는 이유도 마찬가지다.

클래스가 불변이든 가변이든, 가변인 내부 객체를 클라이언트에 반환할 때는 반드시 심사숙고해야 한다.

안심할 수 없다면 방어적 복사본을 반환해야 한다.

방어적 복사에는 성능 저하가 따르고, 또 항상 쓸 수 있는 거도 아니다.

호출자가 컴포넌트 내부를 수정하지 않으리라 확신하면 방어적 복사를 생략할 수 있다.

이러한 상황이라도 호출자에서 해당 매개변수나 반환 값을 수정하지 말아야 함을 명확히 문서화하는 게 좋다.

다른 패키지에서 사용한다고 해서 넘겨받은 가변 매개변수를 항상 방어적으로 복사해 저장해야 하는 것은 아니다.

때로는 메서드나 생성자의 매개변수로 넘기는 행위가 그 객체의 통제권을 명백히 이전함을 뜻하기도 한다.

이처럼 통제권을 이전하는 메서드를 호출하는 클라이언트는 해당 객체를 더 이상 직접 수정하는 일이 없다고 약속해야 한다.

클라이언트가 건네주는 가변 객체의 통제권을 넘겨받는다고 기대하는 메서드나 생성자에서도 그 사실을 문서에 기재해야 한다.

통제권을 넘겨받기로 한 메서드나 생성자를 가진 클래스들은 악의적인 클라이언트의 공격에 취약하다.

따라서 방어적 복사를 생략해도 되는 상황은 해당 클래스와 그 클라이언트가 상호 신뢰할 수 있을 때, 혹은 불변식이 깨지더라도 그 영향이 오직 호출한 클라이언트로 국한될 때로 한정해야 한다.

0개의 댓글