객체가 생성된 뒤로 내부 상태가 변하지 않는 객체. 반대는 생성 이후에도 상태를 바꿀 수 있는 가변 객체다.

바뀌지 않아서 얻는 것

객체가 변하지 않는다는 것은 그만큼 믿고 쓸 수 있다는 뜻이다.

값이 바뀌지 않으니 여러 스레드가 동시에 읽어도 문제가 없다. 동기화를 고려하지 않아도 되고, 필드로 들고 있어도 누가 바꿀 수 없으니 넘겨줄 때 복사해둘 일도 없다.

Map의 키와 Set의 요소로도 적합하다. 값이 바뀌면 해시가 달라져 넣어둔 자리를 다시 찾지 못하는데, 불변이면 그런 일이 없다. 해시 테이블

한 번 메모리에 할당한 뒤로는 같은 객체를 계속 호출해도 새로 할당하지 않으므로 가비지 컬렉터의 부담도 준다.

자바가 기본으로 주는 불변 객체 가운데 String이 대표적이다. 변수에 할당된 뒤로는 참조를 바꾸거나 내부 상태를 손댈 방법이 없고, 그 덕분에 상수 풀에서 같은 값을 나눠 쓸 수 있다. 문자열과 String Pool Integer 같은 래퍼 클래스도 불변이다.

불변으로 만드는 조건

setter를 제공하지 않는 것이 출발점이다. 내부 상태값을 바꿀 통로를 아예 열지 않는다.

클래스에 final을 붙이면 다른 클래스가 상속받지 못하고, 부모에 선언된 메서드를 재정의할 수도 없다. 다만 final을 붙였다고 불변이 보장되지는 않는다. 객체 내부 상태는 여전히 바뀔 수 있으므로 setter를 없애는 것과 함께 써야 한다.

모든 필드는 private final로 선언한다. 여기서 기본형과 참조형이 갈린다. 기본형은 final만으로 불변이 유지되지만, 참조형에 붙은 final은 참조 대상을 다시 대입할 수 없다는 뜻일 뿐이라 가리키는 객체의 내부는 얼마든지 바뀐다. 접근 제어자를 private으로 좁혀 클래스 밖에서 그 필드에 손대지 못하게 막는 이유다. 기본형과 참조형

객체는 생성자나 정적 팩토리 메서드로만 만든다. 생성자로 초기화되는 필드가 참조형이면 깊은 복사로 참조 대상을 새로 잡고, getter도 실제 참조 대신 복사본을 돌려준다. 필드에 가변 객체가 섞여 있을 때 경계에서 복사해 전달하는 이 방식이 방어적 복사이고, 어떤 복사를 언제 쓰는지는 얕은 복사와 깊은 복사에 모아두었다.

새 객체를 만드는 비용

상태를 바꿔야 할 때마다 새 객체를 만들어야 한다. 새 객체를 많이 만들면 성능 문제가 생길 수 있다. 다만 오라클은 객체 생성 비용의 영향이 종종 과대평가되며 불변 객체를 쓸 때의 이점이 이 단점을 상쇄한다고 본다.

필드 캐싱

생성 비용이 큰 객체가 반복해서 필요할 때는 지연 초기화나 Double-checked Locking을 떠올리기 쉽다. 실무에서 많이 쓰는 쪽은 static final 필드에 담아두는 필드 캐싱이다. 지연 초기화는 코드만 복잡해지고 성능은 크게 나아지지 않으며, Double-checked Locking은 JVM이 out of order writes를 허용해 자바와 잘 맞지 않는다.

관련

출처