Skip to content

Latest commit

 

History

History
139 lines (103 loc) · 4.64 KB

50_적시에_방어적복사본을_만들라.md

File metadata and controls

139 lines (103 loc) · 4.64 KB

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

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

불변식 지키지 못한 예시

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;
    }
}

첫번째 공격

Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78);  // p의 내부를 변경했다! 객체의 허락 없이 내부를 수정하도록 할 수 있다.
  • 원인 : Date가 가변이므로 불변식이 깨진다.
  • Date는 낡은 API이므로 더이상 사용 x
    • 대신 LocalDateTime, ZonedDateTime, Instant 사용

해결책 (생성자)

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 + "보다 늦다.");
 }
  • 생성자에서 받은 가변 매개변수 각각을 방어적으로 복사(defensive copy)해야 한다.
    • 즉 Period 안에서는 복사본을 사용한다.
  • 매개변수 유효성 검사전에 방어적 복사본을 만들고, 복사본으로 유효성 검사한다.
    • 멀티 스레딩 환경에서 원본 객체 유효성 검사한 후 복사본을 만드는 순간에 다른 스레드가 원본 객체를 수정할 위험이 있음
  • 매개변수가 확장될 수 있는 타입(final 클래스아닌..) 이라면 방어적 복사본을 만들때 clone을 사용하면 안된다.
    • clone은 하위 클래스 인스턴스를 반환할 수 있다.
      • 만약 악의 가진 하위클래스가 start, end 필드 참조를 private 정적 리스트에 담아뒀다가 공격자에게 리스트에 접근하게 할 수 있다.

두번째 공격

start = new Date();
end = new Date();
p = new Period(start, end);
p.end().setYear(78);  // p의 내부를 변경했다! 
System.out.println(p);
  • 원인 : 접근자 메서드가 내부의 가변정보를 직접 드러내기 때문이다.

해결책 (접근자)

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

public Date end() {
    return new Date(end.getTime());
}
  • 접근자가 가변 필드의 방어적 복사본을 만들면 된다.
  • 생성자와 달리 접근자 메서드는 방어적 복사에 clone 가능
    • Period가 가지고 있는 Date 객체는 하위타입이 아닌게 확실하니까

결론

  • 메서드든 생성자든 클라이언트가 제공한 객체의 참조를 내부의 자료구조에 보관 할때면 항상 그 객체가 잠재적으로 변경될 수 있는지 생각

    • 확신할 수 없으면 복사본 만들어 저장
  • 클래스가 불변이든 가변이든, 가변인 내부 객체를 클라이언트에 반환할때는 반드시 조심.

    • 안심할 수 없다면 방어적 복사

주의사항

방어적 복사에는 성능 저하가 따른다.

호출자가 컴포넌트 내부를 수정하지 않으리라 확신하면 방어적 복사를 생략할 수 있다.

  • ex) 같은 패키지 속하거나..
  • 문서화 필요 (호출자가 매개변수나 반환값을 수정하지 말아야한다)
  • 다른 패키지에 속한다고 항상 방어적으로 복사해 저장하는 것은 아니다.
    • 매개변수로 넘기는 행위가 그 객체의 통제권을 넘긴다는 뜻이 있다. -> 클라이언트가 더이상 직접 수정하지 않아야함
    • 문서화 필요

방어적 복사를 생략해도 될때는?

  1. 클래스, 클라이언트를 상호 신뢰할 수 있을때
  2. 불변식이 깨져도 영향이 오직 호출한 클라이언트일떄

Reference