
불변 객체는 setter를 없애는 코딩 스타일 정도로 이해하면 핵심을 놓치기 쉽습니다. 진짜 목적은 객체가 만들어진 뒤 상태가 마음대로 바뀌지 않게 해서, 코드를 읽는 사람이 더 적은 가능성만 추적하게 만드는 것입니다.
이 글에서는 불변 객체가 왜 캡슐화, 동시성, 테스트, 도메인 모델에서 중요한지 설명하겠습니다. 핵심은 객체를 경직되게 만드는 것이 아니라 변경 지점을 의식적으로 줄여 예측 가능성을 높이는 것입니다.

불변 객체란 무엇인가
불변 객체는 생성된 뒤 내부 상태가 바뀌지 않는 객체입니다. 값이 달라져야 한다면 기존 객체를 고치는 대신 새 객체를 만듭니다.
public final class Money {
private final int amount;
public Money(int amount) {
if (amount < 0) {
throw new IllegalArgumentException("amount must be positive");
}
this.amount = amount;
}
public Money plus(Money other) {
return new Money(this.amount + other.amount);
}
public int amount() {
return amount;
}
}plus 메서드는 현재 Money를 바꾸지 않습니다. 더한 결과를 가진 새 Money를 돌려줍니다. 그래서 기존 객체를 들고 있는 다른 코드가 갑자기 영향을 받지 않습니다.
setter가 많으면 왜 추적이 어려워질까
변경 가능한 객체는 언제 어디서 상태가 바뀌는지 계속 추적해야 합니다. 작은 코드에서는 문제가 없어 보이지만, 서비스, 컨트롤러, 테스트, 이벤트 핸들러가 같은 객체를 주고받기 시작하면 변경 경로가 빠르게 늘어납니다.
Order order = orderRepository.findById(id);
discountService.apply(order);
shippingService.updateAddress(order, address);
paymentService.pay(order);
// 이 시점의 order 상태를 확신하려면 위 메서드들이 무엇을 바꿨는지 모두 알아야 한다.이런 코드는 객체가 여러 곳을 지나며 조금씩 바뀝니다. 불변 객체를 쓰면 최소한 값 자체는 중간에 몰래 바뀌지 않는다는 확신을 얻을 수 있습니다.
불변 객체의 장점
1. 코드 읽기가 쉬워진다
객체가 만들어진 뒤 변하지 않는다면, 특정 시점의 값을 이해하기 위해 전체 호출 흐름을 끝까지 뒤질 필요가 줄어듭니다. 이것만으로도 유지보수 비용이 크게 줄어듭니다.
2. 테스트가 단순해진다
입력이 변하지 않으므로 테스트에서 준비한 객체가 중간에 오염될 가능성이 낮습니다. 같은 입력에 같은 결과를 기대하기 쉬워집니다.
3. 동시성에서 안전해진다
여러 스레드가 같은 객체를 읽어도 상태가 바뀌지 않으면 경쟁 조건이 줄어듭니다. 물론 모든 동시성 문제가 사라지는 것은 아니지만, 공유 값 자체는 훨씬 안전해집니다.
4. 도메인 규칙을 생성 시점에 모을 수 있다
잘못된 값을 가진 객체가 만들어지지 않도록 생성자나 정적 팩터리에서 검증을 걸 수 있습니다. 그러면 객체가 존재한다는 사실 자체가 일정한 규칙을 통과했다는 의미가 됩니다.
불변 객체와 값 객체
불변 객체는 값 객체와 자주 함께 이야기됩니다. Martin Fowler의 Value Object 설명처럼 값 객체는 식별자보다 속성 값으로 비교되는 객체입니다. Money, Email, Period 같은 타입이 대표적입니다.
다만 모든 불변 객체가 값 객체인 것은 아닙니다. 중요한 것은 값으로 비교해야 하는 개념일수록 불변으로 만드는 편이 훨씬 자연스럽다는 점입니다.
그렇다고 모든 객체를 불변으로 만들어야 할까
아닙니다. 엔티티처럼 시간에 따라 상태가 바뀌는 것이 자연스러운 객체도 있습니다. 주문은 결제 전, 결제 완료, 배송 중처럼 상태가 바뀔 수 있습니다. 이런 객체까지 억지로 전부 새 객체로만 다루면 설계가 불편해질 수 있습니다.
중요한 기준은 변경이 도메인 의미를 갖는지입니다. 변경 자체가 업무 흐름의 일부라면 엔티티 메서드로 통제하고, 단순 값이라면 불변으로 두는 편이 낫습니다.
실무 판단 기준
- 값 자체가 의미를 가진다면 불변 객체 후보로 본다
- 객체가 여러 곳에 전달되고 공유된다면 불변이 유리하다
- setter가 많아 상태 추적이 어렵다면 변경 메서드를 줄인다
- 변경이 도메인 행위라면 무조건 막지 말고 명시적인 메서드로 통제한다
불변 객체의 목표는 변경을 금지하는 것이 아니라, 변경이 필요한 곳과 필요 없는 곳을 분리하는 것입니다.
마무리
불변 객체가 중요한 이유는 코드가 더 예측 가능해지기 때문입니다. 상태가 중간에 바뀌지 않는다는 사실 하나만으로도 읽기, 테스트, 공유, 도메인 규칙 관리가 쉬워집니다.
setter를 없애는 것 자체가 목적은 아닙니다. 변경 가능한 상태를 줄이고, 꼭 필요한 변경은 이름 있는 도메인 메서드로 통제하는 것이 더 중요한 설계 기준입니다.
관련해서 상태와 행위를 객체 안에 어떻게 둘지 더 넓게 보고 싶다면 캡슐화는 getter를 숨기는 것일까, 빈혈 도메인 모델 글도 이어서 읽기 좋습니다.