아이템 50. 적시에 방어적 복사본을 만들라

wisdom·2022년 9월 30일
0

Effetctive Java

목록 보기
50/80
post-thumbnail

방어적 복사 (defensive copy)

자바는 안전한 언어이지만 다른 클래스로부터의 침범을 아무런 노력 없이 다 막을 수 있는 것은 아니다. 따라서 클라이언트가 불변식을 깨뜨리려고 혈안되어 있다고 가정하고 방어적으로 프로그래밍해야 한다.


1. 불변식을 지키지 못하는 예제

// 기간을 표현하는 클래스
public final class Period {
    private final Date start;
    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;
    }

    public String toString() {
        return start + " - " + end;
    }
}

위와 같이 코드를 작성할 경우, 인스턴스 내부를 향한 공격에 노출될 수 있다.

공격 예시1

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

공격 예시2

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

2. 방어적 복사 예제

다음과 같이 생성자와 접근자를 방어적 복사를 사용하도록 수정하면, 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 (this.start.compareTo(this.end) > 0)
            throw new IllegalArgumentException(
                    this.start + "가 " + this.end + "보다 늦다.");
    }

    // 수정한 접근자 - 필드의 방어적 복사본을 반환한다.
    public Date start() {
        return new Date(start.getTime());
    }

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

Q. 왜 매개변수 유효성 검사를 하기 전 방어적 복사본을 만들고, 복사본으로 유효성을 검사하나?

멀티스레딩 환경이라면 원본 객체의 유효성을 검사한 후 복사본을 만드는 찰나의 취약한 순간에 다른 스레드가 원본 객체를 수정할 위험이 있기 때문이다.
따라서 순서가 부자연스러워 보이더라도, 반드시 이렇게 작성해야 한다.


3. 주의 사항

1) 매개변수가 제3자에 의해 확장될 수 있는 타입이라면 방어적 복사본을 만들 때 clone을 사용해서는 안 된다.

2) 클라이언트가 제공한 객체의 참조를 내부의 자료구조에 보관해야 할 때, 그 객체가 잠재적으로 변경될 수 있다면 방어적 복사본을 사용해야 한다.

3) 해당 클래스와 그 클라이언트가 상호 신뢰할 수 있을 때, 혹은 불변식이 깨지더라도 그 영향이 오직 호출한 클라이언트로 국한될 때는 방어적 복사를 생략할 수 있다. 그러나 방어적 복사를 생략하더라도 호출자에서 해당 매개변수나 반환값을 수정하지 말아야 함을 문서화하자.



🔖 핵심 정리

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

profile
백엔드 개발자

0개의 댓글