불변 객체(Immutable Object)는 생성 후 그 상태를 변경할 수 없는 객체를 의미합니다. Java에서 가장 대표적인 불변 객체는 String 클래스입니다. 불변 객체는 한번 생성되면 그 내부의 상태가 절대로 변하지 않음을 보장합니다.
public final class ThreadSafeCounter {
private final int value;
public ThreadSafeCounter(int value) {
this.value = value;
}
public ThreadSafeCounter increment() {
return new ThreadSafeCounter(value + 1);
}
public int getValue() {
return value;
}
}
불변 객체는 생성 시점 이후 상태가 변하지 않기 때문에, 동기화 없이도 여러 스레드에서 안전하게 공유할 수 있습니다.
동일한 값을 가지는 객체는 항상 동일한 상태를 유지하므로, 캐시를 통해 객체 재사용이 가능합니다.
불변 객체는 복사해도 원본과 동일한 객체이므로, 방어적 복사가 필요 없습니다.
public final class Person {
private final String name;
private final int age;
private final List<String> hobbies;
public Person(String name, int age, List<String> hobbies) {
this.name = name;
this.age = age;
this.hobbies = new ArrayList<>(hobbies); // 방어적 복사
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public List<String> getHobbies() {
return Collections.unmodifiableList(hobbies); // 불변 컬렉션 반환
}
}
public record PersonRecord(
String name,
int age,
List<String> hobbies
) {
// 생성자에서 방어적 복사
public PersonRecord {
hobbies = List.copyOf(hobbies);
}
}
Record 클래스는 Java 14에서 도입된 새로운 형태의 불변 데이터 클래스입니다.
주요 특징:
@Value는 Lombok에서 제공하는 불변 클래스를 위한 애노테이션입니다. @Data의 불변 버전이라고 생각하면 됩니다.
@Value 애노테이션이 자동으로 생성하는 것들:
private과 final로 만듦final로 만듦 (상속 방지)getter 메소드toString(), equals(), hashCode() 메소드@Value
public class PersonLombok {
String name;
int age;
List<String> hobbies;
public PersonLombok(String name, int age, List<String> hobbies) {
this.name = name;
this.age = age;
this.hobbies = List.copyOf(hobbies);
}
}
위 코드는 다음과 동일한 보일러플레이트 코드를 자동으로 생성합니다:
public final class PersonLombok {
private final String name;
private final int age;
private final List<String> hobbies;
public PersonLombok(String name, int age, List<String> hobbies) {
this.name = name;
this.age = age;
this.hobbies = List.copyOf(hobbies);
}
public String getName() { return name; }
public int getAge() { return age; }
public List<String> getHobbies() { return hobbies; }
@Override
public boolean equals(Object o) { ... }
@Override
public int hashCode() { ... }
@Override
public String toString() { ... }
}
추가적인 Lombok 기능:
@Value.Accessors(fluent = true): getter 메소드에서 'get' 접두사 제거@NonNull: null 체크 자동 생성@Builder: 빌더 패턴 자동 생성@Value
@Builder
public class PersonLombokWithBuilder {
@NonNull String name;
int age;
@Builder.Default List<String> hobbies = List.of();
// 빌더 사용 예시
public static void main(String[] args) {
PersonLombokWithBuilder person = PersonLombokWithBuilder.builder()
.name("John")
.age(30)
.hobbies(List.of("reading", "gaming"))
.build();
}
}
public final class DeepImmutablePerson {
private final String name;
private final Address address; // Address도 불변 객체여야 함
public DeepImmutablePerson(String name, Address address) {
this.name = name;
this.address = address; // Address가 불변이므로 방어적 복사 불필요
}
public String getName() {
return name;
}
public Address getAddress() {
return address; // Address가 불변이므로 그대로 반환 가능
}
}
public record Address(String street, String city, String zipCode) {}
불변 객체는 값이 변경될 때마다 새로운 객체를 생성하므로, 변경이 빈번한 경우 메모리 사용량이 증가할 수 있습니다.
// 비효율적인 예
String result = "";
for (int i = 0; i < 1000; i++) {
result += i; // 매번 새로운 String 객체 생성
}
// 효율적인 예
StringBuilder result = new StringBuilder();
for (int i = 0; i < 1000; i++) {
result.append(i); // 기존 객체 재사용
}
String finalResult = result.toString();
자주 사용되는 값들은 캐싱하여 재사용할 수 있습니다.
public final class CachedImmutable {
private static final Map<String, CachedImmutable> CACHE = new ConcurrentHashMap<>();
private final String value;
private CachedImmutable(String value) {
this.value = value;
}
public static CachedImmutable valueOf(String value) {
return CACHE.computeIfAbsent(value, CachedImmutable::new);
}
}
Q: 불변 객체의 장점과 단점을 설명해주세요.
A: 장점으로는 스레드 안전성, 캐시 활용 가능, 방어적 복사 불필요, 실패 원자성 보장 등이 있습니다. 단점으로는 값이 변경될 때마다 새로운 객체 생성이 필요하여 메모리 사용량이 증가할 수 있다는 점입니다.
Q: Java의 String이 불변인 이유는 무엇인가요?
A: 보안성(문자열 풀 사용), 동기화 이슈 방지, 해시코드 캐싱, 문자열 풀을 통한 메모리 절약 등의 이유로 String은 불변으로 설계되었습니다.
Q: Record와 일반 클래스로 구현한 불변 객체의 차이점은 무엇인가요?
A: Record는 불변성을 기본으로 제공하며, equals, hashCode, toString 등의 메서드를 자동으로 생성합니다. 또한 직렬화가 자동으로 구현되며, 상속을 허용하지 않습니다. 반면 일반 클래스는 이러한 기능들을 직접 구현해야 합니다.