불변 객체 설계
불변 객체는 생성 이후 관찰 가능한 상태가 바뀌지 않습니다.
여러 곳에서 같은 객체를 공유해도 누가 먼저 수정할지 걱정하지 않아 동시성과 캐시에 유리합니다.
클래스와 필드를 final로 선언하는 것만으로 충분하지 않고 가변 값이 내부로 들어오고 밖으로 나가는 경로를 막아야 합니다.
컬렉션이나 배열은 방어적 복사가 필요합니다.
이 절에서는 얕은 불변과 깊은 불변의 차이를 확인합니다.
생성자에서 모든 상태를 확정한다
필수 값을 검증해 final 필드에 저장하고 상태를 바꾸는 setter를 제공하지 않습니다.
변경이 필요하면 새 객체를 반환합니다.
가변 입력은 방어적으로 복사한다
호출자가 가진 List나 배열 참조를 그대로 저장하면 생성 뒤 외부 변경이 내부 상태를 바꿀 수 있습니다.
List.copyOf나 배열 복사를 사용합니다.
ImmutableSchedule.java는 가변 리스트를 입력으로 받되 객체 안에는 독립된 불변 사본을 저장합니다. 생성 전후에 어느 쪽을 바꾸어도 일정 객체의 관찰값이 달라지지 않아야 합니다.
List 구현 선택은 11장의 주제입니다. 여기서는 컬렉션 종류보다 List.copyOf가 외부 참조와 객체 내부 상태를 분리하고, withTask가 원본 대신 새 Schedule을 만든다는 객체 경계에 집중합니다.
import java.util.ArrayList;
import java.util.List;
public class ImmutableSchedule {
static final class Schedule {
private final String owner;
private final List<String> tasks;
Schedule(String owner, List<String> tasks) {
this.owner = java.util.Objects.requireNonNull(owner);
this.tasks = List.copyOf(tasks);
}
List<String> tasks() { return tasks; }
Schedule withTask(String task) {
List<String> changed = new ArrayList<>(tasks);
changed.add(task);
return new Schedule(owner, changed);
}
}
public static void main(String[] args) {
List<String> source = new ArrayList<>(List.of("read"));
Schedule first = new Schedule("Ada", source);
source.add("external change");
Schedule second = first.withTask("code");
System.out.println(first.tasks());
System.out.println(second.tasks());
// first.tasks().add("break"); // 실행 시 UnsupportedOperationException
}
}생성자에 넘긴 리스트를 밖에서 수정한 뒤 객체의 값을 다시 출력해 보세요. 방어적 복사가 되어 있다면 외부 리스트만 바뀌고 이미 만든 객체의 상태는 유지됩니다.
[read]
[read, code]입력 List를 복사하고 불변 List를 반환하므로 외부 변경이 first의 상태로 들어오지 않습니다.
불변 객체의 경계는 필드에 final을 붙이는 것보다 넓습니다. 생성자에서는 가변 입력을 복사하고, 접근자에서는 내부 가변값을 직접 내보내지 않으며, 상태 변화가 필요하면 기존 객체를 건드리지 않는 새 객체를 반환합니다.
이 세 경계 가운데 하나라도 열리면 외부 코드가 객체의 관찰값을 바꿀 수 있습니다. 특히 컬렉션 필드는 입력과 출력 양쪽에서 같은 참조가 새지 않는지 확인해야 합니다.
가변 내부값을 그대로 반환하지 않는다
getter가 내부 배열을 반환하면 외부가 내용을 수정할 수 있습니다.
복사본이나 변경 불가능한 뷰를 반환하고 그 계약을 명시합니다.
생성할 때만 복사하고 getter에서 가변 내부 리스트를 그대로 돌려주면 호출자가 다시 상태를 바꿀 수 있습니다. List.copyOf로 변경 불가능한 사본을 저장하거나 접근할 때마다 사본을 반환해 입력 경계와 출력 경계를 모두 닫아야 합니다.
final class MutableLeak {
private final int[] values;
MutableLeak(int[] values) { this.values = values; } // 외부 배열 공유
int[] values() { return values; } // 내부 배열 노출
}// 안전한 경계
// this.values = values.clone();
// return values.clone();값 기반 동등성을 함께 설계한다
불변 값 객체는 보통 같은 구성 값이면 같다고 판단합니다.
equals와 hashCode를 일관되게 구현하거나 record가 적합한지 비교합니다.
with 메서드는 변경 대신 새 값을 만든다
일부 필드만 다른 값을 원하면 기존 객체를 바꾸지 않고 새 객체를 반환하는 메서드를 제공할 수 있습니다.
이름에서 새 객체가 생긴다는 사실을 드러냅니다.
불변성은 공유 비용을 낮춘다
락 없이 읽을 수 있고 방어적 스냅샷으로 전달하기 쉬워집니다.
다만 큰 구조를 매번 복사하는 비용은 영속 자료구조나 별도 모델과 비교합니다.
같은 불변 객체를 두 변수와 여러 스레드가 공유해도 중간 변경을 관찰하지 않습니다. withTask 호출 전후 객체를 함께 출력해 원본은 그대로이고 새 객체만 작업 하나를 더 갖는지 확인하면 불변 갱신의 흐름이 선명해집니다.
불변성은 final의 개수가 아니라 객체 경계를 통과하는 모든 가변 참조를 통제했을 때 만들어집니다.