반응형

 

자바 방패를 들고 있는 아기 바다표범

 

개요

자바는 C, C++ 에 비해 메모리 관리 측면에서 안전하다. 또 자바로 작성한 클래스는 불변식이 지켜진다. 개발자가 원한다면 어떤 객체 안의 멤버필드를 특정 조건을 만족하도록 클래스를 설계하고, 불변성을 갖도록 할 수 있다.

하지만 아무런 노력 없이 이러한 클래스를 만들 수 있는건 아니다. 프로그래머의 실수로 인해 의도하지 않는 변경이 일어날 수 있다. 외부에 의해 불변식이 깨질 수도 있다는 뜻이다.

 

일반적으로 객체를 생성할 때 수정자와 같은 장치가 없다면 외부에서 내부를 수정하는 것을 막아놨음을 의미한다. 하지만 자기도 모르게 내부를 수정하도록 허락하는 경우가 생긴다.


불변식을 지키고 싶었던 클래스

반응형

Period 클래스를 통해 객체를 생성할 경우 end 값이 start 보다 느리도록 설정된다. 유효성 검사도 하기때문에 문제가 없어보이지만 start나 end 값을 변경하여 불변식을 깨트릴 수 있다.

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

 

 

불변식을 헤치고 싶었던 외부 클래스

Date start = new Date();
Date end = new Date();

Period p = new Period(start, end);

// start 와 end 는 변경되....엔다
start.setTime(10);

 

외부 클래스에 의해 불변식이 깨져버렸다.

위와 같이 변경이 가능한 이유는 Date 클래스가 가변 객체이기 때문이다.

 

가변객체
클래스의 인스턴스가 생성된 이후 내부 상태 변경이 가능한 객체

 


Date 는 Instant or LocalDateTime 을 사용하자

이와 같은 문제는 자바 8에서 해결됐다. Date 대신 사용 가능한 불변 객체인 Instant나 LocalDateTime이 등장했기 때문이다. 가변적 성질을 가진 Date 사용은 지양하자.

그럼 모든 멤버필드를 불변객체로 사용하는건 어떨까??

 


모든 멤버필드를 불변객체로 사용하는 건 좀...

 위와 같이 불변식을 지키려고 모든 멤버필드를 불변객체로 사용하는 건 힘.들.다. 멤버필드는 충분히 가변 객체일 수도, 커스텀 클래스 타입일 수도 있는데, 이러한 객체를 모두 불변객체로 대체하거나 만드려면 너모 힘들기 때문이다.

그럼 Date 와 같은 가변객체를 사용하는 상황에서 Period 인스턴스의 내부를 보호하려면 어떻게 해야할까?

바로 방어적 복사, defensive copy 를 사용해야 한다.

 

다시금 자바 방패를 들고 있는 아기 바다표범

 

 


Defensive Copy 를 적용하여 인스턴스 내부 보호하기

 

Defensive Copy는 인스턴스 내부 필드를 설정할 때, 파라미터로 온 값을 그대로 할당하는 게 아닌 복사하는 방법이다.

아래와 같이 매개변수로 받은 가변객체에 대해 똑같은 타입의 새로운 객체 생성(복사)한 후 멤버필드에 할당하고 있다.

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

    public Period(Date start, Date end){

        this.start = new Date(start.getTime()); // defensive copy
        this.end = new Date(end.getTime()); // defensive copy

        if(start.compareTo(end) > 0){
            throw new IllegalArgumentException(start +" 가 "+ end +" 보다 늦다.");
        }
    }

    public Date start(){
        return start;
    }

    public Date end(){
        return end;
    }
}

 

 

불변식을 헤쳤던 코드를 적용해보니 아래의 start와 Period 내부에 생성된 start는 엄연히 다른 인스턴스이므로 불변식 지킬 수 있게 되었다.

public static void main(String[] args) {

    Date start = new Date();
    Date end = new Date();

    Period p = new Period(start, end);

    // start는 변경되지 않는다!!
    start.setTime(10);
}

 

 

 

라고 생각했지만 가변 객체는 가변 객체. 어떻게든 멤버필드를 외부에서 가져간다면, 불변식은 깨지게 된다. 아래처럼 말이다.

// start 는 변경된다.
p.start().setTime(10);

 

 

이런 상황에서 불변성을 지키려면 어떻게 해야할까? 멤버 필드에 할당할 때와 마찬가지로, 멤버 필드를 리턴할 때도  defensive copy 를 적용하면 된다. 이로써 Period 인스턴스는 불변식을 지키게 되었다.

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

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

 


방어적 복사본을 유효성 검사 이전에 하자!

 

눈치챘을 지 모르지만 위 코드에서 특이한 부분이 하나 있다. 유효성 검사보다 방어적 복사본을 만드는 코드가 먼저 위치한다. 유효성 검사를 먼저하는게 당연하다 생각할 수 있지만 이유가 있다.

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 +" 보다 늦다.");
    }
}

 

 

 

이유?!

유효성 검사를 먼저 할 경우 멀티스레드 환경에서 불변식을 헤치는 상황이 발생할 수 있기 때문이다.

유효성 검사를 끝낸 직후 방어적 복사를 하기 전에 다른 스레드에서 파라미터로 들어온 가변객체의 값을 변경이라도 한다면 불변식이 깨져버린다. 

유효성 체크를 먼저하다가 불변식이 깨져버린 상황

 

 

만약 방어적 복사를 유효성 검사보다 선행한다면 어떨까? start.setTime(23) 이 호출되어도 인스턴스 내부에 영향을 미치지 않게 되는 것이다.


방어적 복사의 사용 시기

 

매개변수를 방어적으로 복사하는 목적은 외부로부터 들어오는 매개변수가 가변객체이고, 이를 인스턴스 내부에서 갖게 될때  인스턴스의 불변식을 지키기 위함이다. 인스턴스 내에 멤버필드를 추가할 때에는 '변경될 수 있는 멤버필드인가'를 항상 고려해야한다. 변경되도 상관 없다면 방어적 복사를 할 필요가 없지만, 불변식을 가져야 한다면 방어적 복사를 사용해야 한다. 멤버필드의 불변성을 확신할 수 없을 때도 방어적 복사를 사용하는 것이 좋다.

 


되도록 불변 객체를 조합해 객체를 구성하자

 

불변식을 지키기 위한 코드를 작성하려면 생각보다 많은 고민과 시간을 쏟아야 할것 같지 않은가? 또한 방어적 복사는 멤버필드를 새로 생성하기 때문에 성능 저하가 수반된다. 즉, 불변식을 지키는 클래스를 설계할 때에는 가변 객체보다는 불변 객체를 조합해 구성하는 것이 좋다.

 


정리

불변식을 지키고자 하는 클래스가 매개변수로 받거나 반환하는 멤버필드가 가변객체라면 반드시 방어적 복사(defensive copy)를 해야한다. 클래스 설계시 의도적으로 수정자 메서드를 생성하지 않았다면 '방어적 복사'를 떠올려야 할 때이다.

 

 

 

반응형

+ Recent posts