Effective Java

[Effective Java] 아이템 50 - 적시에 방어적 복사본을 만들라

로드태환 2022. 4. 5. 16:17

Java는 포인터를 사용하는 언어와 달리 메모리 충돌 오류에서 안전하지만 클라이언트가 우리가 작성한 클래스의 불변식을 깨려고 노력한다는 것을 가정에 두고 방어적으로 프로그래밍 해야 한다.

 

기간을 표현하는 클래스를 보자

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 + " after " + end);
  }
  this.start = start;
  this.end = end;
 }
 public Date start() { return start; }
 public Date end() { return end; }
 // ... 생략
}

//공격
public static void main(String[] args) {
		Date start = new Date();
		Date end = new Date();
		Period p = new Period(start, end);
		end.setYear(78);
}

불변 처럼 보이지만 Date가 가변이란 사실을 알면 불변성을 깨기 쉽다. 

사실 이미 Date.setXXX는 deprecated 되었고 LocalDateTime을 사용하면 불변성을 지킬 수 있다.

 

 그 이전이라 생각하고 Period의 인스턴스 내부를 보호하려면 생성자의 각 가변 매개변수를 각각 방어적 복사해야 한다. 

public Period(Date start, Date end) {
 this.start = new Date(start.getTime());
 this.end = new Date(end.getTime());
 if(start.compareTo(end) > 0) {
  throw new IllegalArgumentException(start + " after " + end);
 }
}

이렇게 하면 앞의 방식의 공격에 안전하다.

 

방어적 복사본을 만든 뒤 유효성 검사를 한 점에 주목해보자.

멀티 스레드 환경에서는 유효성을 검사한 후 복사본을 만들기 전 그 찰나에 다른 스레드가 원본 객체를 수정할 위협이 있기 때문에 이렇게 해야한다. 이런 공격을 검사시점/사용시점(TOCTOU) 공격이라 한다.

 

 방어적 복사에 clone이 아닌 생성자를 사용한 것에도 주목해 보자.

Date는 final이 아니기 때문에 공격자가 정의한 악의적인 Date 클래스의 하위 인스턴스를 clone이 반환할 수 도 있기 때문이다. 이런 악성 인스턴스는 start와 end 필드의 참조를 가지고 있다가 클라이언트가 이에 접근할 수 있도록 길을 열어 줄 수 있다.

즉, 매개변수가 클라에 의해 확장될 수 있는 타입이라면 방어적 복사를 할때 clone을 사용하면 안된다.

 

다음 공격을 보자.

public static void main(String[] args) {
 Date start = new Date();
 Date end = new Date();
 Period p = new Period(start, end);
 p.end().setYear(78);
}

접근자 메서드가 가변 필드를 직접 드러내 발생한 문제이다. 이는 접근자가 가변 필드의 방어적 복사본을 반환하면 해결된다. 이때는 Date 객체가 java.util.Date인것이 확실 하므로 clone을 사용해도 된다. 하지만 아이템 13에서 말한 것 처럼 생성자난 정적 팩터리 메서드가 바람직 하다.

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

 

 메서드든 생성자든 클라이언트가 제공한 객체의 참조를 내부의 자료구조에서 보관해야 한다면 그 객체가 잠재적으로 변경될 수 있는지 생각해 보고 변경되어도 클래스가 문제없이 동작할지 생각해 봐야 한다. 클라에서 받은 객체를 Set에 저장하거나 Map 의 키로 사용한다면 추후 그 객체가 변경되면 set, map의 불변식이 깨지게 된다.

 아이템 17의 '되도록 불변 객체를 이용해 객체를 구성해야 방어적 복사가 준다'는 교훈을 다시한번 떠올리자

 

방어적 복사에는 성능 저하가 따르고 항상 쓸 수 있는 것도 아니다. 호출자가 컴포넌트 내부를 수정하지 않으리라 확신되면 방어적 복사를 생략할 수 있지만 수정하지 말아야 함을 명확히 문서화 해야한다.

 넘겨 받은 가변 매개변수를 항상 방어적 복사를 해야되는 것은 아니다. 그 객체의 통제권을 명핵히 이전했을 수도 있다. 이럴떄는 클라이언트가 해당 객체를 더 이상 직접 수정하는 일이 없다고 약속해야 한다. 이 또한 문서에 기재해야겠다.

위 같은 메서드나 생성자를 가진 클래스는 취약하다 따라서, 신뢰할 수 있거나, 불변식이 깨지더라도 해당 클라에서만 문제가 되는 경우만 방어적 복사를 생략할 수 있다. 후자의 예로 래퍼클래스 패턴이 있다. 래퍼 클래스 특성상 클라는 래퍼에 넘긴 객체에 집접 접근할 수 있지만 그 영향은 오직 해당 클라에 있다.

결론
클래스가 클라로부터 받고 반환하는 경우 해당 필드가 가변이라면 그 요소는 반드시 방어적으로 복사하자.
방어적 복사 비용이 높다면 생략가능한지 생각해보고 아니라면 래퍼클래스의 활용도 검토해 보자.
이 모든 것은 문서화하자.