Home [Effective Java] Item50. 적시에 방어적 복사본을 만들라!
Post
Cancel

[Effective Java] Item50. 적시에 방어적 복사본을 만들라!

Item50. 적시에 방어적 복사본을 만들라!

Intro

  • 자바는 안전한 언어다.
  • 네이티브 메서드를 사용하지 않으니 C, C++ 처럼 메모리 충돌 오류가 나는 경우에 대해서 안전하다.
  • 자바로 작성한 클래스는 시스템의 다른 부분에서 무슨 짓을 하든 그 불변식이 지켜진다.

방어적 프로그래밍

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

외부에서 객체의 내부를 수정 가능한 문제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public final class Period {
    private final Date start;
    private final Date end;

    public Period(Date start, Date end) {
        if(start.compareTo(end) > 0) {
            try {
                throw new IllegalAccessException(
                        start +"가" + end + "보다 늦다.");
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
        this.start = start;
        this.end = end;
    }
    public Date start() { return start; }
    public Date end() { return end; }
    // ... 생략
}
  • 얼핏 이 클래스는 불변처럼 보인다. 시작 시각이 종료 시각보다 늦을 수 없다.
  • Date가 가변이라는 사실을 이용하면 어렵지 않게 불변식이 깨진다.
1
2
3
4
Date start = new Date();
Date end = new Date();
Period period = new Period(start, end);
end.setYear(3); // period의 내부를 수정
  • 다행히 이러한 문제는 자바 8 이후로는 손쉽게 해결 가능하다.
    • Date 대신 불변인 Instant (혹은 LocalDateTime이나 ZonedDateTime)을 사용하면 된다.
    • Date는 낡은 API다. 새로운 코드를 작성할 때는 더 이상 사용하면 안된다.
  • 여전히 Date를 사용하는 낡은 코드가 상당히 존재한다. 이를 대비해 외부 공격으로부터 Period 인스턴스 내부를 보호하려면 생성자에서 받은 가변 매개변수 각각을 방어적으로 복사해야 한다.

방어적 복사

1
2
3
4
5
6
7
8
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 +"가" + end + "보다 늦다.");
    }
}
  • 이러한 생성자를 사용하면 앞서의 공격은 더 이상 Period에 위협이 되지 않는다.
  • 매개변수의 유효성을 검사하기 전에 방어적 복사본을 만들고, 이 복사본으로 유효성을 검사한 점에 주목하자.
    • 반드시 이러한 순서로 작성해야 한다.
    • 멀티 스레딩 환경이라면 ‘원본 유효성 검사 -> 복사본 만들기’와 같은 찰나의 순간에 다른 스레드가 원본을 수정할 위협이 있기 때문이다.
      • 방어적 복사를 사용한다면 이러한 위협에서 자유롭다.
  • 매개변수가 제3자에 의해 확장될 수 있는 타입이라면 방어적 복사본을 만들 때 clone을 사용해서는 안된다.

접근자 메서드를 통해 내부를 공격하는 문제

1
2
3
4
Date start = new Date();
Date end = new Date();
Period period = new Period(start, end);
period.end().setMonth(3); // period 내부를 수정했다.
  • 이러한 공격을 막아내려면 단순히 접근자가 가변 필드의 방어적 복사본을 반환하면 된다.
1
2
public Date start() { return new Date(start.getTime()); }
public Date end(){ return new Date(end.getTime());
  • 이제 모든 필드가 객체 안에 완벽하게 캡슐화되었다.
  • 생성자와는 달리 접근자 메서드에서는 방어적 복사에 clone 메서드를 사용해도 된다.
    • 가능은 하지만 아이템 13의 설명을 참고해볼 때 인스턴스를 복사하는 데는 일반적으로 생성자나 정적 팩터리를 쓰는 게 좋다.

매개변수를 방어적으로 복사하는 목적

  • 불변 객체를 만들기 위해서만 사용하는 것은 아니다.
  • 잠재적으로 변경될 가능성이 있고, 변경될 수 있는 객체라면, 그 객체가 클래스에 넘겨진 뒤 임의로 변경되어도 그 클래스가 문제없이 동작할지 고려해야 한다.
    • 확신할 수 없다면 방어적 복사본을 만들어 저장해야 한다.
    • 내부 객체를 클라이언트에 건네주기 위해 방어적 복사본을 만드는 이유도 마찬가지다.
    • 길이가 1 이상인 배열은 무조건 가변이니, 내부에서 사용하는 배열을 클라이언트에 반환 시 항상 방어적 복사를 수행하자. 혹은 불변의 뷰를 반환하자.

방어적 복사의 단점

  • 성능 저하가 따르고, 항상 사용할 수 있는 것도 아니다.
    • 고로 되도록 불변 객체들을 조합하여 객체를 구성해, 방어적 복사를 할 필요를 줄이는 것이 좋겠다.

핵심 정리

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

[Effective Java] Item49. 매개변수가 유효한지 검사하라!

[Effective Java] Item51. 메서드 시그니처를 신중히 설계하라!

Comments powered by Disqus.