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

무한성장개발자·2025년 9월 25일

package me.whiteship.chapter08.item50;

import java.util.Date;

// 코드 50-1 기간을 표현하는 클래스 - 불변식을 지키지 못했다. (302-305쪽)
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 String toString() {
        return start + " - " + end;
    }

    // 코드 50-3 수정한 생성자 - 매개변수의 방어적 복사본을 만든다. (304쪽)
//    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());
//    }

    // 나머지 코드 생략
}
package me.whiteship.chapter08.item50;

import java.util.Date;

// '불변'인 Period의 내부를 공격하는 두 가지 예 (303-305쪽)
public class Attacks {
    public static void main(String[] args) {
        // 코드 50-2 Period 인스턴스의 내부를 공격해보자. (303쪽)
        Date start = new Date();
        Date end = new Date();
        Period p = new Period(start, end);
        end.setYear(78);  // p의 내부를 변경했다!
        System.out.println(p);

        // 코드 50-4 Period 인스턴스를 향한 두 번째 공격 (305쪽)
        start = new Date();
        end = new Date();
        p = new Period(start, end);
        p.end().setYear(78);  // p의 내부를 변경했다!
        System.out.println(p);
    }
}

Date라는 객체가 변경이 가능한 객체다.
외부에서 값의 변경이 가능해진다.

package me.whiteship.chapter08.item50;

import java.util.Date;

// 코드 50-1 기간을 표현하는 클래스 - 불변식을 지키지 못했다. (302-305쪽)
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 String toString() {
        return start + " - " + end;
    }

    // 코드 50-3 수정한 생성자 - 매개변수의 방어적 복사본을 만든다. (304쪽)
    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());
//    }

    // 나머지 코드 생략
}

방어적 복사를 한다.

package me.whiteship.chapter08.item50;

import java.util.Date;

// '불변'인 Period의 내부를 공격하는 두 가지 예 (303-305쪽)
public class Attacks {
    public static void main(String[] args) {
        // 코드 50-2 Period 인스턴스의 내부를 공격해보자. (303쪽)
        Date start = new Date();
        Date end = new Date();
        Period p = new Period(start, end);
//        end.setYear(78);  // p의 내부를 변경했다!
//        System.out.println(p);

        // 코드 50-4 Period 인스턴스를 향한 두 번째 공격 (305쪽)
        start = new Date();
        end = new Date();
        p = new Period(start, end);
        p.end().setYear(78);  // p의 내부를 변경했다!
        System.out.println(p);
    }
}

setYear은 Period가 제공해 준 메서드가 아닌데 외부에서 바뀐게 period에 적용이 되어 버렸다.

package me.whiteship.chapter08.item50;

import java.util.Date;

// 코드 50-1 기간을 표현하는 클래스 - 불변식을 지키지 못했다. (302-305쪽)
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 String toString() {
        return start + " - " + end;
    }

    // 코드 50-3 수정한 생성자 - 매개변수의 방어적 복사본을 만든다. (304쪽)
    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());
    }

    // 나머지 코드 생략
}

이렇게 new Date로 해서 넘겨준다.

이렇게 하면 둘다 78년으로 안바뀌고 현재 시간으로 그대로 준다.

방어적 복사 안해도 되는 경우

  • 전부 다 불변객체를 사용하면 된다.
  • 클라이언트에서 변경하지 않을 것을 확신하면 방어적 복사를 안해도 된다.
  • 값을 정하는 걸 클라이언트에서 통제권이 가지고 있으면 굳이 방어적 복사를 안해도 된다.
  • 의도하진 않았지만 바뀌더라도 바뀐 것에 대한 영향을 필요한 곳이 client에서 국한적인 코드라면 방어적 복사를 안해도 된다.

0개의 댓글