Skip to content

Latest commit

 

History

History
193 lines (146 loc) · 6.46 KB

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

File metadata and controls

193 lines (146 loc) · 6.46 KB

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


핵심정리

  • 클라이언트의 악의를 최대로 가정하고 프로그래밍하라
  • 생성자에서 받은 가변 매개변수를 방어적으로 복사하라
    • 복사 이후 검증하라
    • clone을 사용하지 마라
  • 가변 필드를 방어적 복사를 통해 반환하라

클라이언트가 당신의 불변식을 개뜨리려 혈안이 되어 있다고 가정하고 방어적으로 프로그래밍 하라

  • 어떤 객체든 허락 없이 외부에서 내부를 수정하는 일은 불가능해야 한다.
  • 하지만 내부를 수정하도록 허락하는 경우가 생긴다

참조가 공개되어있을 때 외부 변경의 위험

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 + " after " + end);
        this.start = start;
        this.end = end;
    }
    
    public Date start() {
        return start;
    }
    
    public Date end() {
        return end;
    }
}
  • 기간 객체는 start/end를 불변으로 만들 생각이었다.
  • 기간 객체의 불변식은 시작시간이 종료시간보다 앞선다 이다.
  • 그러나, 다음 같은 두가지 공격을 통해 이 불변식을 쉽게 깨뜨릴 수 있다

공격 방법1. 주입된 참조를 지닌 클라이언트 측의 변경

public static void main(String[] args) {
    // Attack the internals of a Period instance  (Page 232)
    Date start = new Date();
    Date end = new Date();
    Period p = new Period(start, end);
    end.setYear(78);  // p에 들어가 있는 end Date에 대해 Modifies internals of p!
    System.out.println(p);
    
    // 결과 : Thu May 16 10:41:58 KST 2024 - Tue May 16 10:41:58 KST 1978
    // end시각이 1978년으로 설정됨 => 불변식이 깨짐
}

방어 방법1 : 생성자에서 받은 가변 매개변수를 방어적으로 복사

 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 + " after " + this.end);
    }

복사 > 유효성 검사 순서를 지켜야 한다

  • 복사 > 유효성 검사 : 반드시 이 순서로 수행해야 한다
  • cause) TOCTOU(Time Of Check / Time Of Use)
  • 올바른 값 > 유효성 검사(Check) > 원하는 값으로 변경 > 사용(Use)
  • 멀티 스레드 환경에서 유효성 검사 이후 복사본을 만드는 순간에 객체 ㅅ정의 위험이 있음

매개변수가 3자에 의해 확장될 수 있다면 clone을 사용하지 말자

  • 만약 clone을 통해 복사를 한다고 가정하자
public Period(Date start, Date end) {
        this.start = (Date) start.clone();
        this.end   = (Date) end.clone();;

        if (this.start.compareTo(this.end) > 0)
            throw new IllegalArgumentException(
                    this.start + " after " + this.end);
    }
  • Date의 하위 타입인 AttackDate에서 clone을 재정의할 수 있다.
public class AttackDate extends Date {
    
    public AttackDate(long date) {
        super(date);
    }

    @Override
    public Object clone() {
        Date cloneDate = new Date();
        cloneDate.setYear(78);
        return cloneDate;
    }
}
  • 그렇다면 외부에서 의도를 가지고 공격을 감행할 수 있다.
  • 즉, clone의 대상이 확장된 인스턴스일 때 악의를 가진 인스턴스를 반환할 수 있다.
public static void main(String[] args) {
    start = new AttackDate(new Date().getTime());
    end = new AttackDate(new Date().getTime());
    p= new Period(start, end);
    System.out.println(p);
    
    // 결과 : Tue May 16 11:14:20 KST 1978 - Tue May 16 11:14:20 KST 1978

}
-

따라서, 제 3자에 이해 확장될 수 있는 타입이라면 방어적 복사본을 만들 때 clone을 사용하지 마라


공격 방법2. getter 를 통해 얻은 참조로 공격

public static void main(String[] args) {
    start = new Date();
    end = new Date();
    p = new Period(start, end);
    p.end().setYear(78);  // Modifies internals of p!
    System.out.println(p);
    
    // 결과 : Thu May 16 10:41:58 KST 2024 - Tue May 16 10:41:58 KST 1978
    // end시각이 1978년으로 설정됨 => 불변식이 깨짐
}

방어 방법2 : 가변 필드의 방어적 복사본을 반환

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

public Date end() {
        return new Date(end.getTime());
}
  • Period 자신말고는 가변필드에 접근할 방법이 없으로 온전히 불변화 된다
  • Date 객체임이 보장되므로 clone을 사용해도 된다.

방어적 리스트 복사에 대해 더 알아보자 feat) 당코

1) Collections. unmodifiableList - 원본 참조

  • 읽기 전용 뷰만을 제공하며 추가, 삭제를 막음
  • 내부적으로 원본 리스트를 멤버 변수로 가지고 있다 == 원본리스트의 변경이 반영된다
  • 리스트 내부 객체의 참조값을 그대로 사용한다 == 내부 객체의 변경이 반영된다
  • 주의점1) 원본 리스트의 추가/삭제 등 수정 > 변경 반영(참조값이 같으므로)
  • 주의점2) 내부 객체의 수정 > 변경 반영

2) List.copyOf - 원본 복사

  • 데이터의 추가, 삭제 기능을 막은 상태로 읽기만 지원
  • 새로운 리스트를 생성하여 원본 데이터를 복사 반환 == 원본 리스트의 변경이 반영되지 않는다
  • unmodifiableList와 같이 내부 객체의 참조값은 그대로 복사된다 == 내부 객체의 변경이 반영된다
  • 주의점1) 원본 리스트 수정 시 > 변경이 반영되지 않음 (참조값이 다른 리스트이므로)
  • 주의점2) 내부 객체 수정 시 > 변경 반영

몇가지 디저트 조언

  • 불변객체들을 조합해 객체르 구성하면 방어적 복사를 할 일이 줄어든다
  • 방어적 복사는 성능 저하가 따르고, 또 항상 쓸 수 있는 것도 아니다
  • 방어적 복사는 두가지 상황에서 생략 가능하다
    • 상황1) 해당 클래스와 클라이언트가 상호 신뢰할 수 있을 때
    • 상황2) 불변식이 깨지더라도 그 영향이 오직 클라이언트로 국한 될 때