Effective Java | #50. 적시에 방어적 복사본을 만들라

보람·2022년 5월 8일
0

Effective-Java

목록 보기
19/25

자바는 안전한 언어지만 다른 클래스로부터의 침범을 아무런 노력 없이 다 막을 수 있지 않다.

클라이언트가 불변식을 깨뜨리려 혈안이 되어 있다고 가정하고 방어적으로 프로그래밍해야 한다.

불변식을 지키지 못하는 Period 클래스

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 + "가 " + end + "보다 늦다.");
        this.start = start; //넘어온 날짜 데이터를 그대로 넣어주고 있음
        this.end   = end;  //넘어온 날짜 데이터를 그대로 넣어주고 있음
    }
    
    //this.start & this.end 반환
    public Date start() {
        return start;
    }
    public Date end() {
        return end;
    }
    
    public String toString() {
        return start + " - " + end;
    }
}

얼핏 불변처럼 보이지만 Date은 가변이다.

Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
System.out.println(p); //Sun May 08 13:27:13 KST 2022 - Sun May 08 13:27:13 KST 2022
end.setYear(78);  // p의 내부를 변경했다!
System.out.println(p); //Sun May 08 13:27:13 KST 2022 - Mon May 08 13:27:13 KST 1978
  • 가변 데이터를 그대로 this.start에 넣는 위 클래스를 사용한다면 객체 생성후 end 날짜 데이터의 값을 바꾸면 p 객체에 있는 end 날짜 데이터까지 변경되어 end가 start보다 과거의 날짜가 되어서 불변조건을 깨는 문제가 발생된다.

이를 해결하기 위해서는 생성자에서 받은 가변 매개변수 각각을 방어적으로 복사해야 한다.

매개변수의 방어적 복사본 만들기

위 예제의 생성자를 아래와 같이 변경하면

// 매개변수의 방어적 복사본
public PeriodClone(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 + "보다 늦다.");
}
  • 방어적 복사본이 유효성 검사 코드 이전에 있다. 멀티스레딩 환경이라면 복사본을 만드는 찰나에 원본 객체를 수정할 위험이 있기 때문에 이전에 둬야 한다.
  • 매개변수가 제 3자에 의해 확장할 수 있는 타입이라면 방어적 복사본을 만들때 clone을 사용하면 안됨
    • 자칫하다가 공격자에게 이 리스트에 접근하는 길을 열어주게 될 수 있음

이제 안바뀌겠지?0?

Date start = new Date();
Date end = new Date();
PeriodClone p = new PeriodClone(start, end);
System.out.println(p); //Sun May 08 13:40:37 KST 2022 - Sun May 08 13:40:37 KST 2022
end.setYear(78);
System.out.println(p); //Sun May 08 13:40:37 KST 2022 - Sun May 08 13:40:37 KST 2022
p.end().setYear(78);   // p의 내부를 변경했다!
System.out.println(p); //Sun May 08 13:41:28 KST 2022 - Mon May 08 13:41:28 KST 1978

가변 데이터인 end 데이터를 변경할때는 생성자에 방어적 본사본을 만들었기 때문에 변경되지 않음을 확인했으나 end 게터 함수에서 return end를 하고 있기 때문에 다시 변경이 된다.
이를 해결하기 위해서는..

필드의 방어적 복사본 반환하기

start와 end 필드에 방어적 복사본을 반환하도록 처리하면 해결된다.

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

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

이러면 end() 호출 후 setYear()로 변경한다고 해도 p 내부 값은 변경할 수 없다.

가변인 내부 객체를 클라이언트에 반환하는 방법 두가지는 (item-15)에서 확인

핵심 정리

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

profile
백엔드 개발자

0개의 댓글