불변 객체


  • 생성 후 그 상태를 바꿀 수 없는 객체
  • 재할당은 가능하지만, 한 번 할당하면 내부 데이터를 변경할 수 없는 객체

    ex) String, Integer, Boolean, BigDecimal…

    아래와 같은 경우라면,

    (1) heap 영역에 str이 참조하고 있는 “a” 객체 생성

    (2) “b” 객체가 새로 생성되고, str은 “b”객체를 가리킨다.

    (3) “Imuutable” 객체가 새로 생성되고, str은 “Immutable” 객체를 가리킨다.

    “a”, “b” 객체는 아무것도 참조하고 있지 않는 객체가 되어 GC의 대상이 된다.

    이렇듯, 불변 객체는 객체가 변하는 것이 아니라 새로운 객체를 생성한다.


      String str = "a"; //(1) 
      str = "b"; //(2) 
      str = "Immutable"; //(3)
    


불변 객체 생성하는 방법



1. 필드에 원시 타입만 있는 경우


기본적인 아이디어는 필드에 final 사용하고, Setter를 구현하지 않는 것

public class Car {

    private final String name;

    private final int position;

    public Car(String name, int position) {
        this.name = name;
        this.position = position;
    }

    // 필요하다면 getter만 사용. setter는 금지
}

원시 타입은 참조 값이 존재하지 않는다.

그래서 값을 그대로 외부로 내보내는 경우에도 내부 객체는 불변이다.

따라서, Setter가 없고, 원시 타입 필드에 대해 final을 설정하였다면 해당 객체인 Car는 불변 객체가 된다.


2. 필드에 일반 객체의 참조 타입이 있는 경우


다음과 같은 두 클래스가 있을 때 Car 클래스는 불변 객체일까?

Car클래스의 position필드에 단순히 final만 붙이면 Car 객체는 불변 객체가 되는 것처럼 보인다.

하지만, Car 클래스의 getPosition() 메서드로 position을 가져오고 Position 객체의 setVaue() 메서드로 position의 상태를 바꿀 수 있다.

따라서, 불변 객체의 참조 타입 객체도 불변 객체여야 한다.

public class Car {

    private final String name;

    private final Position position;

    public Car(String name, Position position) {
        this.name = name;
        this.position = position;
    }

    // 필요하다면 getter만 사용. setter는 금지
}
public class Position {

    private int value;

    public Position(int value) {
        this.value = value;
    }

    public void setValue(int value) {
        this.value = value;
    }
}


아래와 같이, 참조 타입 객체인 Position을 불변 객체로 만들어 해결하면 된다.

public class Position {

    private final int value;

    public Position(int value) {
        this.value = value;
    }

		// getter
}


3. 필드에 참조 타입 컬렉션이 있는 경우


3-1. 생성자 방어적 복사

아래 첫번째의 Car 클래스는 모든 필드에 final을 추가하고, Setter를 사용하지 않았으니 불변 객체라고 생각할 수 있으나 불변 객체가 아니다.

이유는 Car 생성자로 넘어오는 monthlyMileages의 주소가 공유되기 때문이다.

생성자로 monthlyMileages를 그냥 넘기면 안되고 방어적 복사를 거쳐서 넘겨야 한다.

public class Car {

    private final String name;

    private final int position;

    private final List<Integer> monthlyMileages;

    public Car(String name, int position, List<Integer> monthlyMileages) {
        this.name = name;
        this.position = position;
        this.monthlyMileages = monthlyMileages;
    }

    // 필요하다면 getter만 사용. setter는 금지
}


이렇게 new ArrayList<>() 와 같이 메모리를 새로 할당하여 monthlyMileages를 넘겨주어 기존 List와의 참조 주소를 끊어내면 불변 객체가 된다.

public class Car {

    private final String name;

    private final int position;

    private final List<Integer> monthlyMileages;

    public Car(String name, int position, List<Integer> monthlyMileages) {
        this.name = name;
        this.position = position;
        this.monthlyMileages = new ArrayList<>(monthlyMileages);
    }

    // 필요하다면 getter만 사용. setter는 금지
}


3-2. getter 방어적 복사

다음으로, getter()가 추가되었다고 생각해보자.

아래 첫 번째 Car 클래스는 생성자에서 방어적 복사는 했으나 불변적 객체가 아니다.

생성자에서 방어적 복사를 했기에 파라미터로 넘어올 monthlyMileages와 Car객체를 생성하면서 얻는 monthlyMileages는 다른 객체가 된다.

하지만, getter로 반환한 monthlyMileages는 최초의 monthlyMileages와 주소를 공유하므로 getter를 통해 가져올 monthlyMileages가 변경되면 최초의 monthlyMileages도 변경된다.

public class Car {

    private final String name;

    private final int position;

    private final List<Integer> monthlyMileages;

    public Car(String name, int position, List<Integer> monthlyMileages) {
        this.name = name;
        this.position = position;
        this.monthlyMileages = new ArrayList<>(monthlyMileages);
    }

    public List<Integer> getMonthlyMileages() {
        return monthlyMileages;
    }
}


따라서, 이렇게 아래의 Car 클래스처럼 getter에 대해서도 방어적 복사를 거치면 비로소 불변 객체라고 할 수 있다.

방어적 복사 말고 Collections.unmodifiableList()를 이용하여 요소의 수정이 발생하면 예외를 던져 상태의 변화를 막을 수도 있다.

하지만, 생성자에서 방어적 복사를 하지 않고, Collections.unmodifiableList() 만 사용한다고 해서 불변을 보장하지는 않는다.

생성자, getter 모두 방어적 복사를 하던가 아니면 생성자는 방어적 복사를 하고, getter는 Collections.unmodifiableList() 를 이용해야 한다.

public class Car {

    private final String name;
    private final int position;

    private final List<Integer> monthlyMileages;

    public Car(String name, int position, List<Integer> monthlyMileages) {
        this.name = name;
        this.position = position;
        this.monthlyMileages = new ArrayList<>(monthlyMileages);
    }

    public List<Integer> getMonthlyMileages() {
        return new ArrayList<>(monthlyMileages);
        // return Collections.unmodifiableList(monthlyMileages);
    }
}


✔️ 결론은, 불변 객체를 만들고 싶은데, 필드에 참조 타입 컬렉션이 있는 경우엔 생성자와 getter의 방어적 복사 뿐 아니라 Collections.unmodifiableList() 를 사용하면 된다.

하지만 더 중요한 부분은 ‼️무조건 방어적 복사를 맹신하지 말고, 참조 타입 자체를 불변 객체로 보장해주고, 방어적 복사까지 해주어야 완벽한 불변 객체를 만들 수 있다.


불변 객체 생성 방법

  • 모든 필드에 대해 final을 설정한다.
  • 필드에 참조 타입이 있을 경우, 해당 객체도 불변성을 보장해야 한다.
  • 필드에 컬렉션이 존재할 경우, 생성자 및 getter에 대해 방어적 복사를 수행해야 한다.


방어적 복사와 Unmodifiable의 차이점?

방어적 복사는 A 리스트와 B 리스트 사이의 참조를 끊는 행위이지만, Unmodifiable은 참조를 끊지 않고 단순히 특정 리스트에서 요소의 변경이 일어날 경우 예외를 던진다.

그래서 생성자 단계에서 방어적 복사를 취하지 않고, getter에서 Unmodifiable만 취할 경우 초기 생성자로 주입한 컬렉션의 변화가 생기면 불변성이 깨진다.


방어적 복사를 사용하면 항상 불변성을 보장하는가?

방어적 복사는 컬렉션의 요소에 대해 얕은 복사를 수행하므로 컬렉션의 참조 타입이 가변 객체라면, 방어적 복사만을 가지고는 복사하려는 컬렉션의 요소가 변경될 경우 불변성이 깨진다.


References

Tags:

Categories:

Date:

Leave a comment