외부의 의도치 않은 수정을 막기 위해, 방어적 복사본을 만들어라.

클라이언트가 여러분의 불변식을 깨트리려 혈안이 되어 있다고 가정하고 방어적으로 프로그래밍해야 한다. 어떤 객체든 그 객체의 허락 없이는 외부에서 내부를 수정하는 일은 불가능하다. 하지만 주의를 기울이지 않으면 자기도 모르게 내부를 수정하도록 하는 경우가 생긴다.

외부의 의도치 않은 수정을 허용한 예시

문제점

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 + "after" + 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 인스턴스의 내부를 보호하려면 생성자에서 받은 가변 매개변수 각각을 방어적으로 복사해야 한다.
  • 접근자 메서드(필드에 접근하는 메서드)가 방어적 복사본을 반환하도록 만들어야 한다.
public final class Period {
    private final Date start;
    private final Date end;

    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);
            }
    }
    
    // 접근자 메서드
    public Date start() {
            return new Date(start.getTime());
    }
    
    public Date end() {
            return new Date(end.getTime());
    }
    ...
}

매개변수의 유효성을 검사 하기 전에 방어적 복사본을 만들고, 이 복사본으로 유효성을 검사한 점에 주목하자. 순서가 부자연스러워 보이겠지만 반드시 이렇게 작성해야 한다. 멀티스레딩 환경이라면 원본 객체의 유효성을 검사한 후 복사본을 만드는 그 찰나의 취약한 순간에 다른 스레드가 원본 객체를 수정할 위험이 있기 때문이다. 방어적 복사를 매개변수 유효성 검사 전에 수행하면 이런 위험에서 해방될 수 있다.

References