본문 바로가기
JAVA

[Java] 불변 객체(Immutable Object)와 final

by solar_youn 2022. 3. 3.

불변 객체(Immutable Object)란?

불변 객체는 생성 후 그 상태를 바꿀 수 없는 객체를 의미합니다. 즉, 참조하고 있는 데이터를 변경할 수 없어야 하며 read-only 메서드만을 제공해야 하고, 객체의 내부 상태를 제공하는 getter 등의 메서드를 제공하지 않거나 제공을 할 경우, 방어적 복사(defensive-copy)를 통해 제공되어야 합니다.

 

Java에서 대표적인 불변 객체로 String이 있습니다. Java는 String Pool이라는 공간을 가지고 있으며, 해당 공간을 이용해 매번 String 객체를 새로 생성하지 않고 같은 값의 String이라면 String Pool 내에 존재하는 객체를 재사용할 수 있도록 구현되어 있습니다.
String str1 = "Hello World";
String str2 = "Hello World";
현재 위 코드는 String Pool 내부의 하나의 String 객체를 바라보게 된다. 만일 String이 mutable 하다면? str1을 “Bye World”로 변경했을 때, 다른 값을 가진 str1, str2가 같은 참조를 가지게 되는 상황에 놓이게 된다.

Java는 기본적으로 배열이나 객체 등의 참조를 전달한다. 참조를 통해 값을 수정하면 내부의 상태가 변하기 때문에 String을 포함한 불변 객체는 내부를 복사하여 전달하는 방어적 복사를 통해 전달한다.

불변 객체의 장단점

장점:

  • 멀티 쓰레드 환경의 공유 자원을 불변으로 만들어 thread-safe하게 병렬 프로그래밍에 사용될 수 있다.
  • 객체에 대한 신뢰도가 높아진다.
  • 생성자, 접근 메소드에 대한 방어적 복사가 필요 없어진다.

단점:

  • 객체가 가지는 값마다 새로운 객체를 만들어야 하기 때문에 메모리 누수 및 성능 저하를 야기한다.

원시 타입에서의 불변

원시 타입은 참조 값이 존재하지 않기 때문에 값을 그대로 외부로 내보내도 내부 객체는 불변입니다. 하여 Setter를 만들지 않는 것만으로도 원시 타입으로 이루어진 객체를 불변으로 만들 수 있습니다.

참조 타입에서의 불변

원시 타입이 아닌 참초 타입을 불변으로 만들기 위해서는 앞서 원시 타입에서의 setter를 포함하지 않는 것과 동시에 getter 사용시 방어적 복사를 통해 값을 전달해야 합니다. 또한, 참조 변수 객체 내부 또한 불변이여야 불변이 성립합니다.

 

지금까지 적은 내용들을 예시를 통해 알아보도록 하겠습니다.

예제 1. 원시 타입의 불변

public class LottoNumber {
    private int number;

    public LottoNumber (int number) {
        this.number = number;
    }
    
    public setNumber(int number) {
        this.number = number;
    }
}

위 객체는 불변 객체가 아닙니다. 앞에서 말씀드린 것과 같이 우선 setter 메서드를 포함하고 있기 때문에 number가 변경될 우려가 있습니다. 하여, 참조 값이 없는 원시 타입에 final 키워드를 붙여 불변 객체로 변경할 수 있습니다. 이를 불변으로 만들기 위해서는 아래와 같이 코드를 수정해야 합니다.

 

public class LottoNumber {
    private final int number;

    public LottoNumber (int number) {
        this.number = number;
    }
}

필드에 final 키워드를 붙여 setter 사용을 막음으로써 상태를 바꿀 수 없는 불변 객체로 만들었으며 값을 변경하기 위해서는 재할당하는 방법 밖에 존재하지 않게 됩니다.

 

예제 2. 참조 타입의 불변 (1)

앞서 말씀드린 것처럼 참조 타입이 존재하는 경우, final을 붙여 setter를 막는 것만으로는 불변 객체를 생성할 수 없습니다. 이는 다음 예시를 통해 확인할 수 있습니다.

public class LottoNumbers {
    private final List<LottoNumber> lottoNumbers;

    public LottoNumbers(List<LottoNumber> lottoNumbers) {
        this.lottoNumbers = lottoNumbers;
    }

    public List<LottoNumber> getLottoNumbers() {
        return lottoNumbers;
    }
}

public class LottoNumber {
    private int number;

    public LottoNumber (int number) {
        this.number = number;
    }

    public int getNumber() {
        return number;
    }

    public void setNumber(int number) {
        this.number = number;
    }
}

LottoNumber를 참조하는 LottoNumbers라는 클래스는 final을 붙였고 setter도 존재하지 않지만 불변 객체가 아닙니다. 이는 LottoNumber의 setNumber() 함수를 통해 필드의 number 값을 변경할 수 있기 때문입니다.

public static void main(String[] args) {
    List<LottoNumber> lottoNumbers = new ArrayList<>();
    lottoNumbers.add(new LottoNumber(1));

    LottoNumbers lotto = new LottoNumbers(lottoNumbers);
    System.out.println(lotto.getLottoNumbers().get(0).getNumber());
    // 결과: 1

    lottoNumbers.get(0).setNumber(5);
    System.out.println(lotto.getLottoNumbers().get(0).getNumber());
    // 결과: 5
}

즉, 불변 객체 안의 참조 변수 또한 불변이어야 불변 객체가 성립됩니다.

 

그렇다면 만약 LottoNumber가 불변 객체였다면 LottoNumbers는 불변일까요?

그럼에도 여전히 LottoNumbers는 불변 객체가 아닙니다. 이는 아래 예시에서 확인해볼 수 있습니다.

예제 3. 참조 타입의 불변 (2)

public static void main(String[] args) {
    List<LottoNumber> lottoNumbers = new ArrayList<>();
    lottoNumbers.add(new LottoNumber(1));
    LottoNumbers lotto = new LottoNumbers(lottoNumbers);

    for (LottoNumber lottoNumber : lotto.getLottoNumbers()) {
        System.out.println(lottoNumber.getNumber());
    }
		// 결과: 1

    lottoNumbers.add(new LottoNumber(5));

    for (LottoNumber lottoNumber : lotto.getLottoNumbers()) {
        System.out.println(lottoNumber.getNumber());
    }
		// 결과: 1, 5
}

현재 외부에서 LottoNumbers의 생성자로 List를 넘겨줄 때, 해당 주소 값을 넘겨주고 있기 때문에 외부(Main)에서 List가 변경된다면 (현재 예시에서는 새로운 로또 번호 5번이 추가됨), LottoNumbers 자체에도 값에 변경이 생겨 상태를 바꿀 수 없는 객체라는 불변의 의미를 지키지 못하게 됩니다.

그럼 참조 타입에서 불변은 어떻게 만드는데?

우선 생성자를 통해 값을 전달받을 때 아래와 같이 new ArrayList<>(lottoNumbers)를 통해 새로운 List를 만들어 값을 복사하도록 변경해야 합니다.

public class LottoNumbers {
    private final List<LottoNumber> lottoNumbers;

    public LottoNumbers(List<LottoNumber> lottoNumbers) {
        this.lottoNumbers = new ArrayList<>(lottoNumbers);
    }

    public List<LottoNumber> getLottoNumbers() {
        return Collections.unmodifiableList(lottoNumbers);
    }
}

그리고 추가적으로 getter를 통한 값 추가/삭제가 불가능하도록 Collections가 제공하는 API인 unmodifiableList()를 이용한다면 참조 타입에서도 불변 객체를 만들 수 있게 됩니다. 해당 방식을 이용하면 재할당 외에는 외부에서 LottoNumbers의 값을 변경할 수 없게 됩니다.

 

결론

  • 불변 객체는 상태를 바꿀 수 없는 객체여야 한다.
  • 불변 객체의 이점은 객체의 자율성이 보장하고 값이 변하지 않음을 명시해주어 프로그램의 신뢰도를 높여준다.
  • 원시 타입의 경우 final을 이용해 간단하게 불변 객체를 만들 수 있지만, 참조 타입의 경우 다른 작업들이 추가된다.

댓글