불변 VS 가변 객체
불변객체 (Immutable Object)
불변 객체: 객체 생성 이후 외부에서 관찰 가능한 상태가 절대 변하지 않는 객체.
상태 변경이 필요하면 기존 객체를 수정하지 않고 새로운 객체를 생성한다.
왜 사용하는가?
- 멀티스레드 환경에서 동기화 없이 안전하게 공유 가능
- 예측 가능한 상태로 버그 방지
- 함수형 프로그래밍 패턴에 적합
장점
| 항목 |
설명 |
| 스레드 안전 |
상태 변경 불가 → 동기화 불필요 |
| 예측 가능 |
외부에서 상태 변경 불가 → 버그 감소 |
| 캐싱/공유 |
안전하게 여러 곳에서 참조 가능 |
| 방어적 복사 불필요 |
전달해도 변경 걱정 없음 |
단점
| 항목 |
설명 |
| 객체 생성 비용 |
상태 변경 시마다 새 객체 생성 |
| 메모리 사용 |
GC 대상 객체 증가 |
특징
- 상태 변경 메서드(setter)가 없다
- 내부 상태를 변경할 수 있는 참조를 외부에 노출하지 않는다
- 클래스를
final로 선언하고, 필드를 final로 선언하는 것이 원칙
예시 코드
public class Main {
public static void main(String[] args) {
Money money = new Money(1000);
System.out.println(money.add(500).subtract(200).getAmount()); // 1300
}
}
final class Money {
private final int amount;
public Money(int amount) {
this.amount = amount;
}
public int getAmount() {
return amount;
}
public Money add(int value) {
return new Money(this.amount + value); // 새 객체 반환
}
public Money subtract(int value) {
return new Money(this.amount - value); // 새 객체 반환
}
}
String (대표적인 불변객체)
- 문자열 상수 풀(String Constant Pool) 사용
- 문자열 리터럴은 힙 영역 내부의 문자열 상수 풀에 저장
- 같은 리터럴은 하나의 객체만 생성되어 공유
new 키워드 사용 시 상수 풀과 무관하게 항상 새로운 객체 생성
- 동일성 비교(
==): 상수 풀 공유 시 true 가능, 하지만 equals() 사용이 원칙
- 문자열 연산: 더하면 새로운
String 객체가 생성됨 (컴파일 타임 상수 결합 제외)
String a = "TEST";
String b = "TEST"; // 같은 상수 풀 객체
String c = a + b; // 런타임 결합 → 새 객체 생성
String d = "TEST" + "TEST"; // 컴파일 타임 결합 → 상수 풀 객체
// a == b → true (같은 상수 풀)
// a + b → 런타임 시 StringBuilder 사용 후 새로운 String 생성
가변객체 (Mutable Object)
- 객체 생성 이후에도 내부 상태를 변경할 수 있는 객체
왜 사용하는가?
- 반복적인 상태 변경이 필요할 때 매번 객체를 생성하는 비용을 줄이기 위해
- 성능이 중요하고 단일 스레드 환경에서 사용할 때
장점
| 항목 |
설명 |
| 성능 |
객체 재사용 → GC 부담 감소 |
| 직관적 |
상태 변경이 자연스러움 |
단점
| 항목 |
설명 |
| 스레드 위험 |
공유 시 동기화 필요 |
| 예측 어려움 |
외부에서 상태 변경 가능 |
특징
- 상태 변경 메서드를 가진다
- 하나의 객체를 계속 재사용
- 성능상 이점이 있으나 공유 시 주의 필요
StringBuilder (대표적인 가변객체)
String은 불변 → 반복적인 문자열 결합 시 객체가 계속 생성됨
StringBuilder는 내부 버퍼를 직접 변경 → 성능 우수
- 스레드 안전하지 않음 (단일 스레드 환경 권장)
- 스레드 안전 버전:
StringBuffer
StringBuilder sb = new StringBuilder();
sb.append("Hello ").append("World");
String result = sb.toString();
System.out.println(result); // Hello World
메서드 체이닝 (Method Chaining)
- 메서드가 자기 자신의 참조(
this)를 반환하여 연속 호출이 가능한 패턴
목적
예시 코드
StringBuilder sb = new StringBuilder();
sb.append("A").append("B").append("C"); // 체이닝
// 직접 구현
class Builder {
private int value;
public Builder set(int v) {
this.value = v;
return this; // 자기 자신 반환
}
}
어떨 때 많이 쓰는가?
| 상황 |
선택 |
| 멀티스레드 공유 객체 |
불변 객체 |
| 단일 스레드, 반복 변경 |
가변 객체 (StringBuilder 등) |
| 반복적인 문자열 결합 |
StringBuilder |
| 스레드 안전한 문자열 결합 |
StringBuffer |
| 값 객체 (VO), 금액 등 |
불변 객체 |