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

신명철·2022년 3월 15일
0

Effective Java

목록 보기
48/80

방어적 프로그래밍

JAVA 의 클래스는 시스템의 다른 부분에서 무슨 짓을 하던 불변식이 지켜진다. 네이티브 메서드를 사용하지 않기 때문에, 메모리 전체를 하나의 거대한 배열로 사용하는 C, C++ 같은 언어에서는 누릴 수 없는 강점이다.

하지만 다른 클래스로부터 침범을 아무런 노력없이 다 막을 수 있는 것은 아니다. 그러니 클라이언트가 클래스의 불변식을 깨뜨리기 위해 혈안이 되어 있다고 가정하며 프로그래밍을 해야 한다. 어떤 객체든 그 객체의 허락 없이는 외부에서 내부를 수정하는 일은 불가능 하지만 주의를 기울이지 않는다면 자기도 모르게 내부를 수정하도록 허락하는 경우가 생긴다. 다음 코드를 보자.

private final class Period {
	private final Date start;
    private final Date end;
	
    public Period(Date start, 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가 가변이라는 사실을 이용하면 어렵지 않게 불변식을 깨뜨릴 수 있다.

Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // p의 내부를 수정할 수 있다

외부의 공격으로부터 Period 인스턴스의 내부를 보호하려면 생성자에서 받은 가변 매개변수 각각을 방어적으로 복사(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(
            	start + "가 " + end + "보다 높다.");
}

새로 작성한 생성자는 앞서의 공격에 더 이상 위협받지 않는다. 매개변수의 유효성을 검사하기 전에 방어적 복사본은 만들고, 이 복사본으로 유효성을 검사한 점에 주목하자. 순서가 부자연스러워 보이지만 멀티스레딩 환경이라면 원본 객체의 유효성을 검사한 후 복사본을 만드는 그 찰나에 순간에 다른 스레드가 원본 객체를 수정할 수 있기 때문이다.

방어적 복사에 Date의 clone 메서드를 사용하지 않은 것도 주목하자. Date 는 final 이 아니기 때문에 clone 이 Date 가 정의한 게 아닐 수도 있다. 즉, clone 이 악의를 가진 하위 클래스의 인스턴스를 반환할 수도 있다. 예컨대 이 하위 클래스는 start와 end 필드의 참조를 private 정적 리스트에 담아뒀다가 공격자에게 이 리스트에 접근하는 길을 열어줄 수도 있다. 이렇게 되면 결국 공격자에게 Period 인스턴스 자체를 송두리째 맡기는 꼴이 된다. 이런 공격을 막기 위해서는 매개변수가 제 3자에 의해서 확장될 수 있는 타입이라면 방어적 복사본을 만들 때 clone을 사용해서는 안된다.

생성자를 수정하면 앞서의 공격을 막을 수는 있지만 Period 인스턴스는 아직도 변경이 가능하다. 접근자 메서드가 내부의 가변 정보를 직접 드러내기 때문이다.

Date start = new Date();
Date end = new Date();
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());
}
  • 생성자와는 달리 접근자 메서드에서는 방어적 복사에 clone 을 사용해도 된다. Period가 가지고 있는 Date 객체는 java.util.Date임이 확실하기 때문이다(신뢰할 수 없는 하위 클래스가 아니라는 의미다.)

방어적 복사와 가변 객체

방어적 복사는 불변 객체를 만들기 위해서만 사용하는 것이 아니다. 메서드든 생성자든 클라이언트가 제공한 객체의 참조를 내부의 자료구조에 보관해야 할 때면 항시 그 객체가 잠재적으로 변경될 수 있는지를 생각해야 한다. 예를 들어 Map이나 Set의 Key값으로 매개변수로 주어진 객체를 사용한다고 했을 때 그 객체가 변경된다면 Map 혹은 Set의 불변식이 깨질 것이다.

가변인 내부 객체를 클라이언트에 반환할 때도 마찬가지다. 길이 1 이상인 배열은 무조건 가변임을 잊지 말아야 한다. 그러니 내부에서 사용하는 배열을 클라이언트에 반환할 때는 항상 방어적 복사를 수행해야 한다. 혹은 배열의 불변 뷰를 반환하는 대안도 있다(아이템 15 참고).

방어적 복사의 예외

방어적 복사에는 성능 저하가 따르고 항상 쓸 수 있는 것도 아니다. 호출자가 컴포넌트의 내부를 수정하지 않으리라 확신한다면 방어적 복사를 생략할 수 있다. 이러한 상황이라도 호출자에서 해당 매개변수나 반환값을 수정하지 말아야 함을 명확히 문서화하는 것이 좋다.

다른 패키지에서 사용한다고 해서 넘겨받은 가변 매개변수를 항상 방어적으로 복사해서 저장해야 하는 것은 아니다. 때론 메서드나 생성자의 매개변수로 넘기는 행위 자체가 그 객체의 통제권을 명백히 이전함을 뜻하기도 한다. 이처럼 통제권을 이전하는 경우 해당 객체를 더 이상 수정하는 일이 없다고 약속해야 한다. 클라이언트가 건네주는 가변객체의 통제권을 넘겨받는다고 기대하는 메서드나 생성자에서도 그 사실을 확실히 문서에 기재해야 한다.

통제권을 넘겨받기로 한 메서드나 생성자를 가진 클래스들은 악의적인 클라이언트의 공격에 취약하다. 따라서 방어적 복사를 생략해도 되는 상황은 해당 클래스와 그 클라이언트가 상호 신뢰할 수 있을 때, 혹은 불변식이 깨지더라도 그 영향이 오직 호출한 클라이언트로 국한될 때로 한정해야 한다(래퍼 클래스 패턴을 예로 들 수 있다(아이템18)). 래퍼 클래스의 특성상 클라이언트는 래퍼에 넘긴 객체에 직접 접근해 불변식을 파괴할 수 있지만 그 영향은 오직 클라이언트 자신만 받게 된다.

profile
내 머릿속 지우개

0개의 댓글