Adventure Time - Lady Rainicorn [아이템 50] 적시에 방어적 복사본을 만들라
본문 바로가기
🤓 스터디/이펙티브 자바

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

by 강켄트 2023. 4. 17.

 

이번 아이템의 핵심 키워드는 "방어적 프로그래밍"이다.

 

"클라이언트가 여러분의 불변식을 깨뜨리려 혈안이 되어 있다고 가정하고

방어적으로 프로그래밍해야 한다."

 

"어떤 경우든 적절치 않은 클라이언트로부터 클래스를 보호하는 데 충분한 시간을 투자하는 게 좋다.

어떤 객체든 그 객체의 허락 없이는 외부에서 내부를 수정하는 일을 불가능하게 해야 한다!

 

예시) 불변식을 지키지 못한 클래스 Period

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 ent(){
        return end;
    }
}

 

Period 클래스를 보면 final이라 상속도 안되고, 멤버 변수들은 private final로 막아놔서 불변식을 지키는 것 같지만

멤버변수 타입 Date가 가변이기 때문에 이를 이용하면 불변식을 깨뜨리는 게 가능하다.

 

Date 클래스에 들어가 보면 이처럼 public으로 활짝 활짝 열려있다.(가변)

Date는 낡은 API이니 새로운 코드를 작성할 때는 더 이상 하용하면 안된다.

Instant, LocalDateTime, ZonedDateTime으로 대신할 수 있다.

 

 

예시) Period 인스턴스의 내부를 공격

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

    //... 생략

    public static void main(String[] args) {
        Date start = new Date();
        Date end = new Date();
        Period p = new Period(start, end);
        end.setDate(78);
    }
}

 

"외부 공격으로부터 Period 인스턴스의 내부를 보호하려면

생성자에서 받은 가변 매개변수 각각을 방어적으로 복사(defensive copy) 해야 한다.

그런 다음 Period 인스턴스 안에서는 원본이 아닌 복사본을 사용한다."

 

 

예시) 생성자를 수정해서 매개변수의 방어적 복사본을 만들자

public Period2(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+ "보다 늦다.");
       }
    }

 

방어적 복사에 Date의 clone 메서드를 사용하지 않았는데,

Date는 final이 아니라 clone이 Date가 정의한 게 아닐 수 있다.

즉, clone이 악의를 가진 하위 클래스의 인스턴스를 반환할 수도 있다.

"매개변수가 제삼자에 의해 확장될 수 있는 타입이라면 방어적 복사본을 만들 때 clone을 사용해서는 안 된다."

 

그리고

매개변수의 유효성을 검사하기 전에 방어적 복사본을 만들고, 이 복사본으로 유효성을 검사를 했는데,

"멀티스레딩 환경이라면 원본 객체의 유효성을 검사한 후 복사본을 만드는 그 찰나의 취약한 순간에 다른 스레드가 

원본 객체를 수정할 위험이 있기 때문이다."

 

이러한 공격을 

컴퓨터 보안 커뮤니티에서는 검사시점/사용자시점(time-of-check/time-of-use) 공격

혹은 TOCTOU 공격이라 한다. 

 

 

'Period 인스턴스의 내부를 공격' 예시처럼 생성자를 수정하면 공격은 막을 수 있었지만,

Period 인스턴스는 아직도 변경 가능한 상태이다.

접근자 메서드가 내부의 가변 정보를 직접 드러내기 때문

 

예시) Period 인스턴스를 향한 두 번째 공격

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

    public Period2(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+ "보다 늦다.");
       }
    }

    public Date start(){
        return start;
    }

    public Date end(){
        return end;
    }
    
    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 final class Period2 {
    private final Date start;
    private final Date end;

    public Period2(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+ "보다 늦다.");
       }
    }

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

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

    public static void main(String[] args) {
        Date start = new Date();
        Date end = new Date();
        Period p = new Period(start, end);
        p.end().setYear(78);
    }
}

 

이렇게 하면 Period 자신 말고는 가변 필드에 접근할 방법이 없고,

모든 필드가 객체 안에 완벽하게 캡슐화되었다.

 

생정자와 달리 접근자 메서드에서는 방어적 복사에 clone을 사용해도 된다.

Period가 가지고 있는 Date 객체는 java.util.Date임이 확실하기 때문이다.(신뢰할 수 없는 하위 클래스가 아님)

💡 하지만 인스턴스를 복사하는 데는 일반적으로 생성자나 정적 팩토리를 쓰는 것이 좋다.

 

"변경될 수 있는 객체라면 그 객체가 클래스에 넘겨진 뒤 임의로 변경되어도 그 클래스가 문제없이 동작할지를 따져보라.

확신할 수 없다면 복사본을 만들어 저장해야 한다."

 

정리

같은 패키지에 속하는 등 클라이언트가 객체의 상태를 변경하지 않는 것이 확실하다면

방어적 복사본을 만들지 않아도 되지만,

클래스가 클라이언트로부터 받거나 클라이언트로 반환하는 구성요소가 가변이라면

방어적으로 복사하자!

 

 

 

 

댓글