본문 바로가기
JAVA/Effective Java

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

by Garonguri 2022. 7. 10.
728x90

자바 : 안전한 언어!

네이티브 메서드를 사용하지 않기 때문이라고 한다. 

 

네이티브 메서드를 사용하지 않기 때문에 버퍼/배열 오버런, 와일드 포인터와 같은 메모리 충돌 오류에서 안전하다고 한다.

자바로 작성한 클래스에서는 시스템의 다른 부분에서 무슨 짓을 하든 그 불변식이 지켜진다.

따라서 메모리 전체를 하나의 거대한 배열로 다루는 언어에서는 누릴 수 없는 강점이다. (자바의 강점이 뭘까? 라고 하면 얘를 대답하면 좋지 않을까 해서 적어봄)


네이티브 메서드가 뭘까?.?

네이티브 메소드란?

네이티브 메소드는 자바가 아닌 다른 언어로 구현된 메소드라고 한다.
네이티브 메소드 앞에는 native 키워드가 붙어있다!
더 찾아보려 했는데 item 66에서 다루더라? 그래서 더 찾아보지는 않았다.

시스템 보안을 뚫으려는 시도는 언제나 있을 수 있기 때문에, 최대한 방어적으로 프로그래밍하는 것이 좋다.

그렇지 않는다면 자신도 모르게 객체의 내부를 수정하도록 허락하는 경우가 생길 수 있다. (흔함)

 

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 + "가 " + end + "보다 늦다.");
        this.start = start;
        this.end = end;
    }

    public Date start() {
        return start;
    }

    public Date end() {
        return end;
    }
    //...
}

얼핏 보면 불변처럼 보인다. final 투성이니까 그렇게 느꼈던 것 같고

start 시각이 end 시각보다 늦으면 IllegalArgumentException이 발생하므로

start 시각이 무조건 빨라야 한다는 불변식이 지켜질 것 같다.

 

-> 그렇지만 Date가 가변이라는 사실을 이용하면 이를 쉽게 깰 수 있다고 한다.

이는 Date 대신 불변인 Instant를 사용하거나 LocalDateTime, ZonedDateTime을 사용하면 된다.

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

 


 

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

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

// 변경 전
public Period(Date start, Date end) {
        if (start.compareTo(end) > 0)
            throw new IllegalArgumentException(
                    start + "가 " + end + "보다 늦다.");
        this.start = start;
        this.end = 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 (
                this.start + "가 " + this.end + "보다 늦다.");
    }
}

 

매개변수의 유효성을 검사하기 전에 방어적 복사본을 만들고, 이 복사본으로 유효성을 검사하였다.

그러니 새로운 생성자를 사용하면 일단 start, end에 대한 불변식은 지켜질 수 있다.

 

반드시 이와 같은 순서로 작성해야만 멀티스레딩 환경에서도 객체 수정의 위험을 예방할 수 있다고 한다.


방어적 복사에 Date의 clone 메서드를 사용하지 않은 이유는,

Date가 final이 아니기 때문에 clone이 Date가 정의하지 않았을 수도 있기 때문이다. 따라서 다른 하위 클래스의 인스턴스를 반환할 가능성이 있기 때문에 사용하지 않은것이다.

 

즉, 매개변수가 제3자에 의해 확장될 수 있는 타입일 경우 방어적 복사본을 만들 때도 clone을 사용해서는 안된다.

 


또한 위에서 생성자를 위와 같이 변경해 start/end 에 대한 불변식을 깨트릴 수 있는 공격은 피할 수 있게 되었지만, 아직 Period 인스턴스는 변경 가능한 상태이다. 

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

따라서 접근자가 가변 필드의 방어적 복사본을 반환하게 해 이를 예방할 수 있다.

 

// 변경 전
public Date start() {
    return start;
}

public Date end() {
    return end;
}

// 변경 후
public Date start() {
    return new Date(start.getTime());
}
public Date end() {
    return new Date(end.getTime());
}

ㅎㅎ 따라서

  1. 아무리 악의적인, 부주의한 프로그래머라도 시작 시간이 종료 시간보다 나중일 수 없다는 불변식을 위배할 방법이 없다.
  2. Period 자신 말고는 가변 필드에 접근할 방법이 없다.
  3. 모든 필드가 객체 안에 완벽하게 캡슐화된다.

는 이점이 존재하게 되었다.


이 때 Period가 가지고있는 Date 객체는 java.util.Date임이 확실하다.

따라서 위 접근자 메서드에서는 clone을 사용해 방어적 복사를 해도 괜찮다.

그렇지만 item 13(clone 재정의는 주의해서 진행해라) 에서 알 수 있듯이 일반적으로 인스턴스를 복사하는데는 생성자나 정적 팩터리를 쓰는게 좋다. 그래서 생성자를 사용했다.

 


위의 예시를 통해 알 수 있듯이, 되도록 불변 객체를 조합해 객체를 구성해야 방어적 복사를 할 일이 줄어든다는 교훈을 얻었다.

 

"모든 복사를 다 방어적 복사로 해버리면 편하지 않을까?"

-> no!

방어적 복사에는 성능 저하가 따르고, 항상 쓸 수 있는것도 아니다.

ex) 호출자가 컴포넌트 내부를 수정하지 않으리라 확신하는 경우와 같을 때는 방어적 복사를 생략해도 된다.

 

핵심 정리

클래스가 클라이언트로부터 받는, 혹은 클라이언트로 반환하는 구성요소가 가변이라면
그 요소는 반드시 방어적으로 복사해야 한다.

하지만 복사 비용이 너무 크거나, 클라이언트가 그 요소를 잘못 수정할 일이 없음을 신뢰한다면
방어적 복사를 수행하는 대신 해당 구성요소를 수정했을 때의 책임이 클라이언트에 있음을 문서에 명시하도록 하자.

 

728x90

댓글