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

Loopy·2022년 10월 1일
0

이펙티브 자바

목록 보기
49/76
post-thumbnail

1️⃣ 방어적 복사(Defensive Copy)


자바는 C/C++와 같이 안전하지 않은 언어에서 자주 보는 버퍼 오버런, 배열 오버런, 와일드 포인터 같은 메모리 충돌 오류가 일어나지 않는다는 점에서 안전한 언어이다.

즉 자바로 작성한 클래스는, 무조건 그 불변식이 지켜진다. 하지만 클라이언트가 불변식을 깨뜨릴 수 있다는 점을 기억하고 방어적으로 프로그래밍 해야 한다.

🔗 불변식을 지키지 못하는 경우

기간을 표현하는 클래스 : 불변식을 지키지 못함

public final class Period {
    private final Date start;  // 가변 객체(불변 X)
    private final Date end;

    /**
     * @param  start 시작 시각
     * @param  end 종료 시각. 시작 시각보다 뒤여야 한다.
     * @throws IllegalArgumentException 시작 시각이 종료 시각보다 늦을 때 발생한다.
     * @throws NullPointerException start나 end가 null이면 발생한다.
     */
    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가, 즉 필드의 객체가 가변이라는 사실을 이용하면, 어렵지 않게 불변식을 깨뜨릴 수 있다.

🔖 공격 예시 1

Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78);  // p의 내부를 변경했다!
System.out.println(p);

해당 공격은, Date 대신 불변인 Instant이나 LocalDateTime/ZondeDateTime 을 사용하면 방어가 가능하다.

LocalDateTime은 불변

LocalDateTime에는 불변을 해치는 set 메서드가 없다. 또한, 생성자는 private 으로 선언해두고 of() 정적 팩터리 메서드를 제공한다.

단, 자바 8 이후에만 가능하므로 이전에 작성된 낡은 코드들에는 소용이 없다.

🔖 해결) 매개변수의 방어적 복사본 생성

따라서, 외부 공격으로부터 클래스 인스턴스의 내부를 보호하려면 생성자에서 받은 가변 매개변수 각각을 방어적으로 복사하고, 인스턴스 안에서는 원본이 아닌 복사본을 사용해야 한다. 위 공격처럼 주입받은 외부 객체에서 값이 바뀔 가능성이 있기 때문이다.

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(
               this.start + "가 " + this.end + "보다 늦다.");
    }

주의할 점은 두가지이다.

  1. 매개변수 유효성을 검사(아이템 49)하기 이전에 방어적 복사본을 만들고, 이 복사본으로 유효성을 검사해야 한다. 순서가 바뀐다면 멀티스레딩 같은 환경에서 객체를 수정할 위험이 생기므로 지켜야 한다.

  2. 매개변수가 제 3자에 의해 확장될 수 있는 타입이라면 방어적 복사본을 만들때 clone() 을 사용하면 안된다. Date는 final 이 아니므로 상속이 가능해 clone() 의 반환 값이 악의를 가진 하위 클래스의 인스턴스가 될 수도 있기 때문이다.

🔖 공격 예시 2

start = new Date();
end = new Date();
p = new Period(start, end);
p.end().setYear(78);  // p의 내부를 변경했다!
System.out.println(p);

두 번째 공격을 막아내려면, 게터 메서드와 같은 접근자가 가변 필드의 방어적 복사본을 반환하면 된다. 이제 비로서 Period 객체는 완벽한 불변으로 거듭날 수 있으며 완벽하게 "캡슐화" 되었다.

🔖 해결) 필드의 방어적 복사본을 반환

public final class Period {
    ...
    public Date start() {
        return new Date(start.getTime());
    }

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

생성자와 달리, 접근자 메서드에서는 방어적 복사에 clone() 을 사용해도 된다. Period가 가지고 있는 Date 객체는 신뢰할 수 없는 하위 클래스가 아닌 java.util.Date 임이 확실하기 때문이다. 하지만 여전히 생성자나 정적 팩터리를 쓰는 방식이 더 좋다.

TIP
매개변수를 방어적으로 복사하는 목적이 꼭 불변 객체를 만들기 위해서만은 아니다. 클라이언트가 제공한 객체의 참조를 내부의 자료구조에 보관해야 할 때면, 항시 그 객체가 잠재적으로 변경될 수 있는지 생각해야 한다. 변경될 수 있는 객체라면 객체가 클래스로 넘겨진 뒤 임의로 변경되어도 클래스 자체는 문제없이 동작할지 따져보자.

예컨대 클라이언트가 건네준 객체를 내부의 Set 인스턴스에 저장하거나 Map 인스턴스의 키로 사용한다면, 추후 그 객체가 변경될 경우 객체를 담고 있는 Set 혹은 Map 의 불변식이 깨질 것이다.

2️⃣ 방어적 복사 주의사항

1. 불변 객체를 조합해 객체를 구성한다면 방어적 복사를 할 일이 줄어든다.

따라서 Period 예제의 경우 자바 8 이상이라면 Instant(LocalDateTime / ZonedDateTime )을 사용해라. 혹은 Date 참조 대신 Date.getTime()을 사용해 long정수를 반환해도 좋다.

2. 호출자가 컴포넌트 내부를 수정하지 않는다고 확신하거나 객체의 통제권을 명백히 이전하는 경우, 방어적 복사를 하지 않고 수정하지 말아야 함을 명확히 문서화 해야 한다.

방어적 복사에는 성능 저하가 따르고, 항상 쓸 수 있는 것도 아니기 때문이다.

3. 클래스와 그 클라이언트가 상호 신뢰할 수 있을때 혹은 불변식이 깨지더라도, 영향이 오직 호출한 클라이언트로 국한될 때만 방어적 복사를 생략해도 된다.

예를 들어 아이템 18의 래퍼 클래스 패턴을 들어보자.
래퍼 클래스의 특성상, 클라이언트는 래퍼에 넘긴 객체에 여전히 직접 접근할 수 있다. 따라서 래퍼의 불변식을 쉽게 파괴할 수 있지만 그 영향을 오직 클라이언트 자신만 받게 된다.(하지만 클래스에 영향을 주는 것은 절대 허용 불가다.)

📚 핵심 정리
클래스가 클라이언트로부터 받는 혹은 클라이언트로 반환하는 구성요소가 가변이라면 그 요소는 반드시 방어적으로 복사해야 한다. 복사 비용이 너무 크거나 클라이언트가 그 요소를 잘못 수정할 일이 없음을 신뢰한다면, 대신 해당 구성요소를 수정했을 때의 책임이 클라이언트에 의해 있음을 문서에 명시하자.

profile
개인용으로 공부하는 공간입니다. 잘못된 부분은 피드백 부탁드립니다!

0개의 댓글