JAVA/Effective Java

메서드- 규칙 39 필요하다면 방어적 복사본을 만들라.

반응형

포인터를 사용하지 않아 잘못된 메모리 접근으로 다른 메모리 영역에 데이터를 건드려서 beffer overrun(오버플로우)등의 오류는 자바에서는 발생하지 않는다.

하지만 아무리 안전한 언어를 사용한다고 해도, 스스로 노력하지 않는 경우, 클래스의 클라이언트가 불변식(invariant)을 깨버릴 수 있기 때문에, 조금 더 방어적인 프로그래밍을 해야 한다.

예를 통해서 확인해보자.

결재정보를 담고 있는 BillTimeObj라는 클래스는 결재 날짜 정보를 가지고 있는 Date 클래스를 생성자를 통해서 전달받는다.
BillTimeObj 클래스는 final로 선언되어 있어서 변경되지 않을 것 같아 보이지만 생성자로 전달되는 Date 객체가 변경이 가능하기 때문에 이는 불변식이 깨져버린다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package mehod39;
 
import java.util.Date;
 
public final class BillTimeObj {
    private Date payTime;
 
    public BillTimeObj(Date payTime) {
       if (payTime.compareTo(new Date()) > 0) {
            new IllegalArgumentException("현재 시간보다 빠를 수 없습니다.");
        }
        
        this.payTime = payTime;
    }
 
    public Date getPayTime() {
        return payTime;
    }
 
    public void setPayTime(Date payTime) {
        this.payTime = payTime;
    }
    
    public static void main(String args[]) {
        Date payDate = new Date();
        BillTimeObj p = new BillTimeObj(payDate);
        payDate.setTime(12321);
    }
 
}
cs



이런 문제를 예방하기 위해서 생성자로 전달되는 변경 가능 객체를 반드시 방어적으로 복사해서 그 복사하도록 해야 한다.

아래 코드와 같이 전달 받은 Date 객체의 정보를 이용하여 새로운 Date 객체를 만들어서 전달된 객체의 변경의 요지를 미리 차단해야 한다.


1
2
3
4
5
6
7
8
9
public BillTimeObj(Date payTime) {
        
        this.payTime = new Date(payTime.getTime());
        
        if (payTime.compareTo(new Date()) > 0) {
            new IllegalArgumentException("현재 시간보다 빠를 수 없습니다.");
        }
        
    }
cs



하지만 주의할 점이 몇가지 있다.

첫 번재 사례는 다음과 같다.
- 생성자에서 진행하는 인자의 유효성 검사는 방어적 복사본을 만든 후에 진행해야 한다.
이는 자연스러워 보이지 않을 수도 있다. 하지만 이렇게 진행하는 이유는 인자를 검사한 직후 복사본이 만들어지기 직전까지의 시간 사이(window of vulerablity)에 다른 스레드가 인자를 변경해 버리는 경우가 있어 이를 방지하기 위해서 이다.

두 번재 사례는 다음과 같다.
- 만약 생성자를 통해서 전달된 데이터가 변경가능한 객체일 때 이를 복사하기 위해서 clone() 메소드를 호출하고 싶을 때는 해당 클래스가 final로서 추가적으로 상속가능한 클래스가 아니어야 한다. 그 이유는 추가적으로 상속할 수 있는 경우 해당 객체의 clone()메소드를 실행 했을 때, 해당 객체가 반환된다고 보장할 수 없기 때문이다.



1
2
3
4
5
6
ex) 
private Animal animal;
public Test(Animal animal) {
   this.animal = animal.clone(); // 이렇게 진행할 경우 전달 받은 객체가 Animal객체가 아니라 이를 
                                                    // 상속한 객체의 clone이 실행될 수 있다.
}
cs




생성자 이외에도 접근자들에 대해서 방어적 복사본을 반환하도록 해야한다. 
왜냐하면 값이 변경될 여지가 있기 때문이다.



1
2
3
public Date getPayTime() {
    return new Date(payTime.getTime());
}
cs



하지만 이런 방어적 복사본을 만들면 성능에 어느 정도 손해를 볼 수 있기 때문에, 적절하지 않을 수도 있다. 만약 클라이언트에서 정말로 변경할 일이 없다고 판단이 된다면, 방어적 복사본을 꼭 만들지 않아도 된다. 하지만! 꼭 클래스 문서에 명시해 주어야 한다.

출처 : 조슈아 블로크, 『 Effective Java 2/E』, 이병준 옮김, 인사이트(2014.9.1), 규칙39 인용.

반응형