개발하는 두더지

[Effective Java 규칙39] 필요하다면 방어적 복사본을 만들라 본문

Java,Android

[Effective Java 규칙39] 필요하다면 방어적 복사본을 만들라

덜지 2018. 10. 15. 17:02

[Effective Java 규칙39] 필요하다면 방어적 복사본을 만들라

Effective Java 2/E 책과 구글링을 통해 내용을 정리하고 개인적인 견해가 포함된 글입니다.


자바는 C나 C++보다 안전한 언어이다. 하지만 안전한 언어라도 시스템의 보안을 무너뜨리려는 악의적 사용자나 실수로 API를 이상하게 사용하는 프로그래머가 있을 수 있기때문에 방어적으로 복사하는 코드로 만들어야 한다.


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("end > start");
this.start = start;
this.end = end;
}

public Date start() { return start; }
public Date end() { return end; }

class도 final로 선언되어있고, 시작이 끝보다 이후일 수 없는 코드로 보이지만 Date는 변경가능한 클래스라는 점을 이용하여 깨뜨릴 수 있다.

public static void main(String[] args) {
Date start = new Date(1000);
Date end = new Date(2000);
Period p = new Period(start, end);

System.out.println(p.end.getTime());
end.setTime(500);
System.out.println(p.end.getTime());
}

따라서 Period 객체의 내부를 보호하려면 생성자로 전달되는 변경 가능 객체를 반드시 방어적으로 복사해서 그 복사본을 Period 객체의 컴포넌트로 사용해야 한다.


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

이 방어적 복사본을 만들 때 Date의 clone 메서드를 이용하지 않은 점을 주목하자. Date 클래스는 final이 아니므로, clone 메서드가 반드시 java.util.Date 객체를 반환할 보장이 없다. 공격을 위해 특별히 설게된 하위 클래스 객체가 반환될 수도 있다는 것이다. 그런 하위 클래스는 새로 만들어진 객체에 대한 참조를 private static 리스트안에 넣어서 공격자가 참조할 수 있다. 이런 공격을 막으려면 인자로 전달된 객체의 자료형이 제 3자가 상속받을 수 있는 자료형일 경우, 방어적 복사본을 만들 때 clone을 사용하지 않도록 해야 한다.


위의 생성자에서 객체를 만들 때 방어적 복사본을 이용하여 생성자 인자를 통한 공격은 막았지만 접근자를 통한 공격은 막을 수 없다.

public static void main(String[] args) {
Date start = new Date(1000);
Date end = new Date(2000);
Period p = new Period(start, end);

System.out.println(p.end.getTime());
p.end().setTime(500);
System.out.println(p.end.getTime());
}

이전 공격에서 Date 객체에서 직접 수정했다면 이번에는 Period 객체에서 end 내부 객체를 반환하여 수정한 것이다. 이 공격을 막으려면

변경 가능 내부 필드에 대한 방어적 복사본을 반환하도록 접근자를 수정해야 한다.


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

이렇게 수정하면 Period는 진정한 변경 불가능 클래스가되고 확실히 캡슐화가된 필드가 된 것이다.

생성자와 달리 접근자에서는 clone을 사용해도 된다. Period 내부 Date 객체가 java.util.Date가 확실하기 때문이다. 그래도 일반적으로 생성자나 정적 팩터리 메서드를 이용하는 것이 좋다.


만약 복사 오버헤드가 너무 크고 클래스 사용자가 그 내부 컴포넌트를 부적절하게 변경하지 않는다는 보장이 있을 때는, 방어적 복사를 하는 대신 클라이언트측에서 해당 컴포넌트를 변경해서는 안된다는 사실만 문서에 명시하면 된다.























Comments