[Effective Java 3/E] ITEM 50. 적시에 방어적 복사본을 만들어라

    Effective Java 표지. Source : https://blog.insightbook.co.kr/

    *알림 : *
    Effective Java 3판은 Java 9까지 도입된 언어적 기능을 중심으로 서술되어 있습니다. 10버젼 이후의 Java 개발을 하시는 분들은 우회적인 접근법 대신 Java 언어 내 새로 도입된 기능이 더 간결하고 좋을 수 있습니다.

    해당 포스팅은 SSAFY 내 책읽기 스터디의 활동을 통해 작성된 포스팅입니다.
    https://github.com/kjsu0209/JavaBook
    https://medium.com/javabook

     

    JavaBook – Medium

    Documentation space of our book study.

    medium.com

     

    kjsu0209/JavaBook

    책읽기 스터디. Contribute to kjsu0209/JavaBook development by creating an account on GitHub.

    github.com

    자바는 C/C++보다 안전한 언어라는 점이 큰 장점이다. C/C++처럼 배열이나 struct, class 밖에서 안을 수정할 수 있는 언어와 달리 Java는 불변식을 지킬 수 있다. 하지만 아무 노력없이 이를 성취할 수 있는 것은 아니며 클라이언트가 불변식을 깨뜨리는 악마라고 가정하고 방어적으로 코드를 작성해야 한다. 보안적인 측면에서도, 오작동을 방지하기 위해서라도 말이다.

    객체의 허락없이 객체 외부에서 내부를 수정하도록 두어서는 안되지만, 실수로 내부를 수정할 수 있도록 여는 일이 생긴다.

    예제 1 : Date를 이용한 Class

    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;
        }
    
            public static void main(String args[]){
                    Date start = new Date();
                    Date end = new Date();
                    Period p = new Period(start, end);
                    end.setYear(78); // Modifies internals of p!
            }
    
        public String toString() {
            return start + " - " + end;
        }
    }
    

    start가 end보다 늦다는 불변식을 지키려고 했지만, Date는 불변이 아니며 문제가 많은 객체다. 불변인 Instant를 사용하면 되지만 Date가 오래 널리 쓰인 탓에 API의 내부 구현에 잔재가 남아있으며 Date를 모두 덜어내기는 불가능하다. 만약 외부로부터 내부를 보호하려면,

    1. 외부에서 받은 매개변수를 복사해서 저장한다.
    2. 내부에서 외부로 데이터를 전달할 때 복사해서 반환한다.

    예제 2: Date를 이용했지만 방어적 복사를 통해 불변식을 지킨다.

    
        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 + "보다 늦다.");
        }
    
        // 코드 50-5 수정한 접근자 - 필드의 방어적 복사본을 반환한다. (305쪽)
        public Date start() {
            return new Date(start.getTime());
        }
    
        public Date end() {
            return new Date(end.getTime());
        }

    앞에서 다뤘던 매개변수의 유효성을 검사할 때도 방어적 복사를 이용하여야 한다. 멀티쓰레드 환경에서는 다른 쓰레드가 race condition에서 원본을 변경하는 일이 벌어질 수도 있다. 이를 방지하기 위해서 유효성 검사를 하기 전에 복사한 후에 유효성 검사를 수행ㅐ야한다.

    clone을 사용하지 않은 이유는, Date가 재정의한 것이 아닐수도 있기 때문에 악의적인 하위 클래스의 인스턴스를 반환할 수도 있다. 만약 악의적인 clone 함수가 참조를 가지고 있도록 설계되었다면 공격자가 clone을 호출한 인스턴스 내부로 접근할 수 있는 길을 깔아줄 것이다.

    start()와 end()도 Date 인스턴스의 복제를 외부에 전달한다. 클라이언트가 반환된 Date를 아무리 수정해도 내부의 Date는 변경되지 않을 것이다. 이제 외부에서 내부를 수정할수 있는 방법이 없으며 불변식을 지킬 수 있다.

    불변 객체를 받는다면 방어적 복사에 드는 비용을 아낄 수 있다.

    통제권과 약속

    매개변수나 생성자를 통해서 넘겨받은 가변 인스턴스를 모두 방어적으로 복사해야할까? 가변 인스턴스를 넘기는 행위 자체가 '나는 더 이상 이 인스턴스를 수정하지 않을 것'이라는 의미를 내포할 수도 있다. 이러한 경우에는 클라이언트는 해당 객체를 수정할 일이 없어야 하며 그러한 기대를 하는 메서드나 생성자의 문서에 그러한 약속을 기술하여야 한다.

    방어적 복사를 생략하고자 한다면,

    1. 약속을 지킬 수 있는 신뢰할 수 있는 클라이언트인지 확인해야하며,
    2. 불변식이 깨져 문제가 생기더라도 클라이언트만 그 책임을 지는 상황으로 한정지어야 한다.

    변할 수 있는 것들

    • 길이가 1 이상인 배열
    • 컬렉션
    • 불변이 아닌 클래스 인스턴스

    댓글