[Effective Java] 적시에 방어적 복사본을 만들라

들어가며

어떤 객체든 그 객체의 허락 없이는 외부에서 내부를 수정하는 일은 불가능하다. 하지만 주의를 기울이지 않는다면 실수하기 마련이다. 다음 코드를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public 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 is later than end");
this.start = start;
this.end = end;
}

public Date start() {
return start;
}

public Date end() {
return end;
}
}

이 클래스는 불변으로 의도했지만 불변이 아니다. Date가 가변이기 때문이다. 다음과 같은 방법으로 내부 상태를 수정할 수 있다.

1
2
3
4
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYeat(10);

다행스럽게 Date는 자바8 이후라면 Instand나 LocalDateTime, ZonedDateTime을 사용하면 된다. 하지만 그렇다고 모든 문제가 해결되는 것은 아니다. 외부 공격으로부터 Period 인스턴스 내부를 보호하려면 생성자에서 받은 가변 매개변수 각각을 방어적으로 복사해야 한다. 다음 코드를 보자.

1
2
3
4
5
6
7
public Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTimte());

if(this.start.compareTo(this.end) > 0)
throw new IllegalArgumentException("start is later then end");
}

위 코드는 방어적으로 복사를 수행했다. 또한 유효성 검사 이전에 방어적 복사를 수행했다. 멀티 스레드 환경에서의 안전을 위해서다. 방어적 복사를 유효성 검사 이전에 수행하면 이런 위험에서 벗어날 수 있다. 이런 위험을 검사시점/사용시점(time-of-check/time-of-use) 공격이라 하며 TOCTOU 공격이라 한다.

또한 방어적 복사에서 Date가 final이 아니므로 clone을 수행하지 않았다. clone을 통해 하위 클래스의 인스턴스를 반환하고 이를 통해 보안에 취약해질 수 있기 때문이다. 즉, 매개변수가 제3자에 의해 확장될 수 있는 타입이라면 방어적 복사본을 만들 때 clone을 사용해서는 안된다.

Period 클래스는 아직도 문제가 있다. getter 메소드에서 내부의 가변 정보를 직접 노출시키기 때문이다. 그렇기 때문에 다음과 같이 수정해야 한다.

1
2
3
4
5
6
7
public Date start() {
return new Date(start.getTime());
}

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

매개변수를 방어적으로 복사하는 목적이 불변 객체를 만들기 위함은 아니다. 클라이언트가 제공한 객체의참조를 내부 자료구조에 보관해야 할 때면 항시 그 객체가 잠재적으로 변경될 수 있는지를 생각해야 한다. 내부 객체를 클라이언트에게 전해줄 때도 마찬가지다. 방어적 복사를 수행해야 한다.

방어적 복사에는 성능 저하가 따르고 또 항상 사용할 수 있는 방법은 아니다. 다만 호출자가 컴포넌트 내부를 수정하지 않으리라 확신하면 방어적 복사를 생략할 수 있다. 때로는 메소드나 생성자의 매개변수로 객체를 넘기는 행위가 그 객체의 통제권을 명백히 이전함을 뜻하기도 한다. 이처럼 통제권을 이전하는 메소드를 호출하는 클라이언트는 해당 객체를 더 이상 직접 수정하는 일이 없다고 약속해야 한다. 통제권을 넘겨받기로 한 메소드나 생성자를 가진 클래스들은 악의적인 클라이언트의 공격에 취약하다. 따라서 방어적 복사를 생략해도 되는 상황은 해당 클래스와 그 클라이언트가 상호 신뢰할 수 있을 때, 혹은 불변식이 깨지더라도 그 영향이 오직 호출한 클라이언트로 국한될 때로 한정해야 한다.

Author: Song Hayoung
Link: https://songhayoung.github.io/2020/08/17/Languages/Effective%20JAVA/item50/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.