peaches-book-study / effective-java

이펙티브 자바 3/E
0 stars 2 forks source link

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

Open hyunsoo10 opened 2 months ago

hyunsoo10 commented 2 months ago

Chapter : 8. 메서드

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

Assignee : hyunsoo10


🍑 서론

자바는 C, C++ 와 달리 메모리 관리가 자동화되어 있기 때문에 네이티브 메서드를 사용함으로써 발생하는 여러가지 메모리 충돌 문제(버퍼 오버런, 배열 오버런, 와일드 포인터)들로부터 비교적 안전하다.

하지만, 아무리 자바라 해도 다른 클래스로부터의 침범을 아무런 노력 없이 막을 수 있는 것은 아니다. 따라서 클라이언트가 우리의 불변식을 깨트리려 혈안이 되어있다고 가정하고 방어적으로 프로그래밍해야 한다.

🍑 본론

어떤 객체든 그 객체의 허락 없이는 외부에서 내부를 수정하는 일은 불가능하다. 하지만, 주의를 기울이지 않으면 내부를 수정하게 하는 경우가 생긴다.

📍 Period 클래스

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;
    }

    // 중략... 

}

✏️ 해당 문제는 다행히 자바 8 이후로는 Date 대신 불변객체인 Instant를 사용하면된다.(LocalDateTime, ZonedDateTime 도 가능)

    // Instant 객체 예시
    Instant start = Instant.now();
    Instant end = Instant.now();

    //LocalDateTime, ZonedDateTime
    LocalDateTime a = LocalDateTime.now();
    ZonedDateTime b = ZonedDateTime.now();

❗Date는 낡은 API이니 새로운 코드를 작성할 때는 더 이상 사용하면 안된다. 하지만, 앞으로 쓰지 않는다고 해서 문제가 없는건 아니다. Date처럼 가변인 낡은 객체를 사용하던 시절이 워낙길었던 탓에 여전히 많은 API와 내부 구현에 그 잔재가 남아있다.

✏️ 외부 공격으로부터 Period 인스턴스의 내부를 보호하려면 생성자에서 받은 가변 매개변수 각각을 방어적으로 복사해야 한다. 그런 다음 Period 인스턴스 안에서는 원본이 아닌 복사본을 사용한다.

📍 수정한 Period 클래스


public final 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(start.compareTo(end) > 0)
            throw new IllegalArgumentException(start + "가" + end + "보다 늦다.");
    }

    public Date start() {
       return start;
    }

    public Date end() {
       return end;
    }

    // 중략 ...

}

✏️ 새로 작성한 생성자를 사용하면 앞에서 당했던 공격을 막을 수 있다. 매개변수의 유효성을 검사(아이템49)하기 전에 방어적 복사본을 만들고 이 복사본으로 유효성을 검사했다.

✏️멀티스레딩 환경이라면 원본 객체의 유효성을 검사한 후 복사본을 만드는 그 찰나의 취약한 순간에 다른 스레드가 원본 객체를 수정할 위험이 있기 때문에 방어적 복사를 매개변수 유효성 검사전에 수행하면 이런 위험에서 해방될 수 있다.
❗컴퓨터 보안 커뮤니티에서는 이를 검사시점/사용시점(time-of-check/time-of-use) 공격 혹은 줄여서 TOCTOU 공격이라 한다.

📍 여전히 취약한 Period 클래스

    public static void main(String[] args) {

        Date start = new Date();
        Date end = new Date();
        Period period = new Period(start, end);

        //period 내부 변경 가능
        period.end().setTime(1010);

        System.out.println("period = " + period);

    }

✏️ 생성자를 수정했을 때 앞의 공격은 막을 수 있지만, Period 인스턴스는 아직도 변경 가능하다. 접근자 메서드가 내부의 가변 정보를 직접 드러내기 때문이다.
✏️ 두 번째 공격까지 막으려면 접근자가 가변 필드의 방어적 복사본을 반환하면된다.

📍 Period의 접근자 수정


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

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

✏️ 접근자까지 위처럼 수정하면 Period는 완벽한 불변으로 거듭난다. Period 자신말고는 가변 필드에 접근할 방법이 없다. 모든 필드가 객체 안에 완벽하게 캡슐화되었다.

🍑 결론

클래스가 클라이언트로부터 받는 혹은 클라이언트로 반환하는 구성요소가 가변이라면 그 요소는 반드시 방어적으로 복사해야 한다. 복사 비용이 너무 크거나 클라이언트가 그 요소를 잘못 수정할 일이 없음을 신뢰한다면 방어적 복사를 수행하는대신 해당 구성요소를 수정했을 때의 책임이 클라이언트에 있음을 문서에 명시하도록 하자.

Referenced by

-