Skip to content

Latest commit

 

History

History
118 lines (91 loc) · 3.84 KB

적시에_방어적_복사본을_만들라_제우스.md

File metadata and controls

118 lines (91 loc) · 3.84 KB

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

이번 아이템은 예전에 작성된 낡은 코드들을 대처하기 위한 것이다. - p.303

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

  • 실제로도 악의적인 의도를 가진 사람들이 시스템의 보안을 뚫으려 시도한다.
  • 평범한 프로그래머도 실수로 여러분의 클래스를 오작동하게 만들 수 있다.
  • 어떤 경우가 됐든 우리의 클래스를 보호하자!

Period 예제

(인텔리제이 복붙으로 시연)

public final class Period {
    private final Date start;
    private final Date end;

    /**
     * @param start 시작 시각
     * @param end 종료 시각; 시작 시각보다 뒤여야 한다. 
     * @throws IllegalArgumentException 시작 시각이 종료 시각보다 늦을 때 발생한다.
     * @throws NullPointerException 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 strat() {
        return start;
    }
    
    public Date end() {
        return end;
    }
    
    // 생략
}
  • 불변처럼 보인다.
  • 하지만 아니다. 왜?
  • Date가 가변이기 때문에!
public static void main(String[] args) {
    Date start = new Date();
    Date end = new Date();
    Period p = new Period(start, end);
    end.setYear(78);    // 내부 상태 변경
}

생성자에서의 방어적 복사

  • Period 인스턴스 내부를 보호하려면 생성자에서 받은 가변 매개변수를 방어적 복사해야 한다.
public Period(Date start, Date end) {
    this.start = new Date(start.getTime());
    this.end = new Date(end.getTime());
    if (this.start.compareTo(end) > 0) {
        throw new IllegalArgumentException(start + "가 " + end + "보다 늦다.");
    }
}
  • 앞에서 생성자나 메서드의 시작부에 유효성을 검사하라고 했다.
  • 하지만 방어적 복사 할 때는 반대다.
  • 멀티스레딩 환경에서, 원본 객체의 유효성 검사를 먼저 할 경우 복사본을 만드는 시간 동안 다른 스레드가 원본 객체를 수정할 위험이 있기 때문이다.
  • clone() 쓰지 마라.
    • Datefinal이 아니므로 clone() 메서드로 악의적인 하위 클래스의 인스턴스를 반환할 수도 있다.

이걸로 끝이 아니다.

Getter에서의 방어적 복사

public static void main(String[] args) {
    Date start = new Date();
    Date end = new Date();
    Period p = new Period(start, end);
    p.end().setYear(78);    // 내부 상태 변경
}
public Date start() {
    return new Date(start.getTime());
}

public Date end() {
    return new Date(end.getTime());
}
  • 이제 완벽한 불변이다.
    • 불변을 깨뜨리려면 네이티브 메서드, 리플렉션과 같은 언어 외적인 수단을 동원해야 한다.

방어적 복사의 다른 목적

  • 방어적 복사의 목적
    • 불변 객체 만들기
    • 객체가 다른 클래스에게 넘겨져도 문제없이 동작해야 한다.
      • Set이나 Map에서 해당 인스턴스를 사용할 경우
        • 해당 인스턴스의 상태가 변해도 Set과 Map이 정상적으로 동작하는지 생각해야 한다.

정리

  • 자바 8 이후로는 Date 대신 Instant, LocalDateTime, ZonedDateTime을 사용하라. 불변이다.
  • 가변 객체 사용할 때는 방어적 복사를 해라.
  • 복사 비용이 너무 크거나 안정성에 확신이 있다면, 객체를 수정했을 때의 책임이 클라이언트에 있음을 문서에 명시하라.