왜 불변 객체가 중요한가?
개발을 하다 보면 객체의 상태를 바꾸는 일이 매우 흔하다.
그러나 이 상태 변경이 언제, 어디서, 왜 발생했는지를 추적하다 보면 복잡해지고,
대부분의 버그는 “예상하지 못한 상태 변화”에서 비롯된다.
이 때문에 많은 개발자들이 다음과 같이 말한다.
“가능한 한 객체는 불변(immutable)하게 만들어라.”
그렇다면 불변 객체란 무엇이며,
왜 그렇게들 불변을 강조하는가?
✅ 불변 객체란?
한 번 생성되면 내부 상태가 절대로 바뀌지 않는 객체를 의미한다.
String name = "gugbab2";
String upper = name.toUpperCase(); // name은 그대로, upper는 "경호"의 대문자
→ String은 대표적인 불변 객체이다.
toUpperCase()를 호출해도 원본은 변경되지 않는다.
🔍 불변 객체를 사용해야 하는 이유
🔒 스레드 안전(Thread-Safe)하다
불변 객체는 상태가 변하지 않기 때문에,
멀티 스레드 환경에서도 락(lock) 을 걸 필요 없이 안전하게 공유할 수 있다.
LocalDate date = LocalDate.now(); // LocalDate도 불변 객체이다
서버에서 여러 요청이 동시에 이 객체를 참조하더라도 문제가 발생하지 않는다.
🔍 상태 추적이 용이하다
불변 객체는 값이 절대로 바뀌지 않기 때문에,
"어디서 값이 바뀌었는가?"를 고민할 필요가 없다.
Money price = new Money(1000);
Money discounted = price.applyDiscount(10); // price는 1000, discounted는 900
→ applyDiscount()는 원본을 변경하지 않고, 새로운 인스턴스를 반환한다.
→ 디버깅과 유지보수가 쉬워진다.
🚫 부작용(side effect)이 없다
불변 객체는 내부 상태를 변경하지 않기 때문에,
함수를 순수 함수(pure function)처럼 만들 수 있다.
순수 함수란,
동일한 입력값에 대해 항상 같은 결과를 반환하고, 외부 상태에 영향을 주지 않는 함수이다.
이러한 함수는 예측 가능하며, 테스트하기 쉽다.
🧩 컬렉션에서 안전하게 사용할 수 있다
자바의 HashMap, HashSet과 같은 컬렉션은 내부적으로
hashCode()와 equals()를 이용하여 객체를 관리한다.
그러나 객체를 key로 넣은 후 내부 값을 바꾸게 되면
해시값이 바뀌고, 조회나 삭제가 불가능해지는 문제가 발생한다.
User u = new User("gugbab2");
Set<User> set = new HashSet<>();
set.add(u);
u.setName("변경됨"); // 해시값 변경
set.contains(u); // false
set.remove(u); // false
→ 불변 객체라면 이러한 문제는 발생하지 않는다.
♻️ 캐싱, 공유, 재사용에 유리하다
불변 객체는 내부 상태가 바뀌지 않기 때문에,
객체 풀, 캐시 등의 전략을 통해 메모리 효율성과 성능을 높일 수 있다.
Integer a = Integer.valueOf(100);
Integer b = Integer.valueOf(100);
System.out.println(a == b); // true
→ 같은 값을 재사용하기 때문에 불필요한 객체 생성을 줄일 수 있다.
✅ 언제 불변 객체를 사용해야 하는가?
다음과 같은 경우에는 불변 객체를 사용하는 것이 강력하게 권장된다.
📦 DTO, 응답 객체를 설계할 때
- 클라이언트에 반환되는 응답 값은 변경되면 안 되는 값이다.
- 데이터를 수신한 이후에는 읽기만 가능해야 신뢰할 수 있는 응답이 된다.
record UserResponse(String name, int age) {} // Java 16+ Record는 불변
🧩 해시 기반 컬렉션에서 key나 요소로 사용할 때
- HashMap, HashSet 등은 객체의 hashCode()와 equals() 값을 기반으로 내부 버킷을 구성한다.
- 객체를 key 또는 요소로 넣은 후 내부 상태가 변경되면,
→ 해시값이 달라져서 조회, 삭제, 수정이 모두 실패할 수 있다. - 특히 equals()와 hashCode()를 오버라이딩한 객체는 내부 필드가 바뀌면
동등성 판단 기준 자체가 바뀌므로 매우 위험하다.→ 따라서 컬렉션에 넣을 객체는 동등성 기준이 되는 필드가 변하지 않아야 하며,
이를 위해 불변 객체로 설계하는 것이 가장 안전하다.
User u = new User("경호");
Set<User> set = new HashSet<>();
set.add(u);
u.setName("다른 이름"); // ❌ hashCode 변경
set.contains(u); // false
set.remove(u); // false
🧱 도메인 모델의 Value Object
- DDD(Value Object)는 의미 있는 값을 표현하는 객체이며, 식별자(ID)가 없고,
오직 내부 값 자체로 동등성을 판단한다. - 상태가 변하게 되면 VO 간의 비교 자체가 불가능해지거나 의미가 퇴색된다.
→ 그래서 VO는 반드시 불변으로 만들어야 설계 의미를 온전히 지킬 수 있다.
public class Money {
private final int amount;
public Money add(Money other) { return new Money(this.amount + other.amount); }
}
🧪 테스트에서의 예측 가능성이 중요할 때
- 테스트는 입력에 대해 결과가 항상 같아야 신뢰할 수 있다.
- 불변 객체는 상태가 변하지 않기 때문에 테스트 케이스가 안정적이고,
외부 조건이나 실행 순서에 의존하지 않는 테스트 구성이 가능하다.
✨ 마무리하며
불변 객체는 처음에는 다소 불편하게 느껴질 수 있다.
그러나 프로젝트가 커지고 복잡해질수록,
유지보수성과 안정성 면에서 압도적인 장점을 제공한다.
“이 객체의 상태가 바뀌면 안 되는가?”
“이 값을 누가 언제 바꿀 가능성이 있는가?”
이런 질문이 들 때, 불변 객체를 사용하는 것을 적극 고려해야 한다.
'언어 > Java' 카테고리의 다른 글
왜 List<String>은 List<Object>가 될 수 없을까? (제네릭과 와일드카드가 공존하는 이유) (0) | 2025.05.03 |
---|---|
happens-before란 무엇인가? (0) | 2025.05.02 |
ABA 문제란? - CAS 에서 터질 수 있는 진짜 함정 (0) | 2025.05.02 |
Executor 프레임워크2 - Executor 프레임워크가 등장한 이유 (0) | 2025.04.30 |
Executor 프레임워크1 - 스레드를 직접 사용할 때 문제점 (0) | 2025.04.30 |