요약

방어적 복사를 해야하는 경우와 할 필요가 없는 경우는 다음과 같다.

방어적 복사를 해야하는 경우

  • 클라이언트로 반환하거나 클라이언트로 받는 구성요소가 가변일 때
  • 객체의 잠재적 변경 가능성이 있고, 변경이 되면 안되는 경우

방어적 복사를 할 필요가 없는 경우

  • 성능 저하가 예상되는 경우 (ex. 매개변수 복사 비용이 너무 클 때)
  • 클라이언트가 구성요소를 수정할 일이 없다는 신뢰가 있는 경우
  • 불변식이 깨지더라도 그 영향이 호출한 클라이언트로 국한되는 경우 (ex. 래퍼 클래스)

→ 이런 경우에는 구성요소에 대한 변경의 책임이 클라이언트에 있음을 문서화 하자

 

불변식을 지키는 코드 작성

코드를 작성할 때, 외부 클라이언트가 내부 클래스를 수정하지 못하도록 보호하는 코드를 작성해야 한다. 이는 곧 클래스의 캡슐화를 구현하는 방법이다.

불변식을 지키지 못한 예시

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

    public Period1(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; }
}

// Period 인스턴스 내부 공격
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // period 인스턴스 값 수정
  • 언뜻 보기에는 불변식이 지켜질 것 같지만, 실제로는 Date가 가변 객체이기 때문에 외부에서 start, end를 수정할 수 있어 불변식을 깨는 코드이다.
  • 따라서 Date 대신 Instant, LocalDateTime, ZonedDateTime을 사용해야 한다.

새로운 코드는 Date 외 다른 객체를 사용하면 되지만, 예전에 작성된 낡은 코드에 Date가 있을 때에는 방어적 복사로 대처할 수 있다.

방어적 복사

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

예시1 - 생성자 방어적 복사

public Period2(Date start, Date end) {
	// 1. 방어적 복사 
	this.start = new Date(start.getTime()); // 매개변수 start 방어적 복사
	this.end = new Date(end.getTime()); // 매개변수 end 방어적 복사
        
        // 2. 매개변수의 유효성 검사
	if (start.compareTo(end) > 0) {
		throw new IllegalArgumentException(
				start + "가 " + end + "보다 늦다.");
  }
}

방어적 복사 시 주의할 점

  • 순서
    1. 방어적 복사본을 먼저 만든다.
    2. 매개변수의 유효성 검사 수행 (아이템49)
    → 위 순서대로 해야하는데, 그 이유는 유효성 검사를 한 후 복사본을 만드는 그 순간에 다른 스레드가 원본 객체를 수정할 위험이 있기 때문이다. 반대가 되면 안된다.
  • clone()
    • 매개변수가 제3자에 의해 확장될 수 있는 타입일 경우, 방어적 복사본을 만들 때 clone() 메서드를 사용하면 안된다
    • 다른 곳에서 정의된 clone()일 경우, 악의를 가진 하위 클래스의 인스턴스가 반환될 가능성이 있기 때문이다

 

예시2 - 접근자 방어적 복사

예시1을 통해 생성자에 대한 공격은 예방했지만, 아직 접근자는 내부 가변 정보(Date객체)를 직접 반환하기 때문에 Period2 인스턴스는 변경가능하다.

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

public Date end() {return new Date(end.getTime()); }
  • 직접 가변 필드(start, end)를 반환하는 것이 아닌 가변 필드의 방어적 복사본을 반환하면 Period2 인스턴스를 변경하는 방법은 없다.
  • 즉 모든 필드가 객체 안에 완벽하게 캡슐화 되었다

 

방어적 복사가 필요한 경우

  1. 불변객체를 만들어야 하는 경우
  2. 객체의 잠재적 변경 가능성이 있고, 변경이 되면 안되는 경우
    1. 객체의 잠재적 변경 가능성이 있고 변경이 되어도 문제 없다면 방어적 복사를 꼭 하지 않아도 된다
  3. 가변인 내부 객체를 클라이언트에 반환 하는 경우
    1. 원본을 노출하지 말고 방어적 복사본을 반환함으로써 혹시 모를 원본 객체 변경 상황을 예방할 수 있다
    2. 길이가 1이상인 배열은 무조건 가변이기에 배열은 항상 방아적 복사를 하거나 배열의 불변 뷰를 반환해야 한다

코틀린 → 불변, Null 관련 해서 잘 되어 있음

+ Recent posts