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

반응형
728x90
반응형

불변식 깨뜨리기

자바는 메모리 충돌 오류에서 안전한 언어다. 자바로 작성한 클래스는 시스템의 다른 부분에서 무슨 짓을 하든 그 불변식이 지켜진다. 하지만 아무리 자바라고 해도 다른 클래스로부터의 침범을 아무런 노력없이 다 막을 수 있는건 아니므로 방어적으로 프로그래밍해야 한다. 

 

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

 

Period.java
package com.java.effective.item50;

import java.util.Date;

public class Period {
    private final Date start;
    private final Date end;
    
    public Period(Date start, Date end) {
        if (start.compareTo(end) > 0) {
            throw new IllegalArgumentException();
        }
        
        this.start = start;
        this.end = end;
    }
    
    public Date start() {
        return new Date(start.getTime());
    }

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

위 코드는 기간을 표현하는 클래스로, 불변식을 지키지 못했다. 

 

불변식을 깨는 코드
package com.java.effective.item50;

import java.util.Date;

public class Main {
    public static void main(String[] args) {
        Date start = new Date();
        Date end = new Date();


        Period p = new Period(start, end);
        end.setYear(72); // p 의 내부 수정
    }
}

Date 가 가변이라는 사실 때문에 그 불변식을 깨뜨릴 수 있다. 자바 8 이후부터는 Date 대신 불변인 Instant 를 사용하는걸 권장한다. LocalDateTime, zonedDateTime 등 제공되는 불변 클래스가 많다. Date 는 낡은 API이니 새로운 코드를 작성할때는 더이상 사용하면 안된다. 

 

 

해결방안 - 방어적 복사

외부 공격으로부터 Period 인스턴스의 내부를 보호하려면 생성자에서 받은 가변 매개변수 각각을 방어적으로 복사(defensive copy) 해야한다.

 

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

 

방어적 복사본 생성한 Period.java
package com.java.effective.item50;

import java.util.Date;

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

    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();
            
        }
//        if (start.compareTo(end) > 0) {
//            throw new IllegalArgumentException();
//        }
//
//        this.start = start;
//        this.end = end;
    }

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

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

새로 작성한 생성자를 사용하면 위 불변식을 깨던 코드의 공격으로부터 방어할 수 있다. 매개변수의 유효성 검사하기 전에 방어적 복사본을 만들고, 이 복사본으로 유효성을 검사했다. 이는 멀티스레드 환경에 대응하기 위해서다. 원본 객체의 유효성을 검사한 후 복사본을 만드는 그 찰나의 취약한 순간에 다른 스레드가 원본 객체를 수정할 위험이 있기 때문이다. 방어적 복사를 매개변수 유효성 검사 전에 수행하면 이런 위험에서 해방될 수 있다.

 

clone 메서드 사용하지 않기

방어적 복사에 Date의 clone 메서드를 사용하지 않았다. Date는 final 이 아니므로 clone이 Date가 정의한게 아닐 수도 있다. 즉, clone이 악의를 가진 하위 클래스의 인스턴스를 반환할 수도 있다.

 

위 예제로 예를 들어보면, 매개변수 start, end 가 확장된 Date의 하위 클래스일 수도 있다. 그래서 clone()을 호출했을때 Date 클래스의 clone 메서드가 아닌 확장된 하위 클래스의 clone 메서드를 호출할 수도 있다는 얘기다.

 

이 하위 클래스는 start, end 필드의 참조를 pricate 정적 리스트에 담아뒀다가 공격자에게 이 리스트에 접근하는 길을 열어줄 수도 있다. 매개변수가 제 3자에 의해 확장될 수 있는 타입이라면 방어적 복사본을 만들때 clone을 사용해서는 안된다.

 

또다른 공격
package com.java.effective.item50;

import java.util.Date;

public class Main {
    public static void main(String[] args) {
        Date start = new Date();
        Date end = new Date();


        Period p = new Period(start, end);
        // end.setYear(72); // p 의 내부 수정
        p.getEnd().setYear(78);
    }
}

end() 메서드로 얻은 결과로 불변식을 깨뜨렸다. end() 메서드 내부를 수정하자.

 

package com.java.effective.item50;

import java.util.Date;

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

    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();

        }
//        if (start.compareTo(end) > 0) {
//            throw new IllegalArgumentException();
//        }
//
//        this.start = start;
//        this.end = end;
    }

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

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

이로써 Period.java 는 완벽한 불변으로 거듭났다. 모든 필드가 객체 안에 완벽하게 캡슐화 되었다. 생성자와 달리 접근자 메서드에서는 방어적 복사에 clone을 사용해도 된다. Period가 가지고있는 Date 객체는 java.util.Date 임이 확실하기 때문이다. 그렇더라도 아이템 13에서 설명한 이유 때문에 인스턴스를 복사하는 데는 일반적으로 생성자나 정적 팩터리를 쓰는게 좋다.

 

 

정리

매개변수를 방어적으로 복사하는 목적이 불변 객체를 만들기 위해서만은 아니다. 메서드든 생성자든 클라이언트가 제공한 객체의 참조를 내부의 자료구조에 보관해야할 때면 항시 그 객체가 잠재적으로 변경될 수 있는지를 생각해봐야한다. 변경될 수 있는 객체라면 그 객체가 클래스에 넘겨진 뒤 임의로 변경되어도 그 클래스가 문제없이 동작하는지 따져보라. 확신할 수 없다면 복사본을 만들어 저장해야한다. 

 

되도록 불변 객체들을 조합해 객체를 구성해야 방어적 복사를 할 일이 줄어든다. 예제에서 봤듯이 Date 보다는 Instant를 사용하라는 뜻이다. 방어적 복사에는 성능 저하가 따르고, 또 항상 쓸수있는 것은 아니다. 호출자가 컴포넌트 내부를 수정하지 않으리라 확신하면 방어적 복사를 생략할 수 있다. 이는 문서로 남기는게 좋다. 

 

방어적 복사를 생략해도 되는 상황은 해당 클래스와 그 클라이언트가 상호 신뢰할 수 있을때, 혹은 불변식이 깨지더라도 그 영향이 오직 호출한 클라이언트로 국한될때로 한정해야한다. 

 

오직 호출한 클라이언트로 영향이 한정될 경우

래퍼 클래스 패턴으로 예를 들어보자. 래퍼 클래스의 특성상 클라이언트는 래퍼에 넘긴 객체에 여전히 접근할 수 있다. 따라서 래퍼의 불변식을 파괴할 수 있지만 그 영향은 오직 클라이언트 자신만 받게된다.

 

 

 

반응형

Designed by JB FACTORY