[Java] 불변 객체의 이해와 구현예제

슈퍼대디·2024년 12월 23일

CS면접대비

목록 보기
1/13

Java 불변 객체의 이해와 구현 - 안전하고 신뢰할 수 있는 객체 설계

목차

  1. 불변 객체란?
  2. 불변 객체가 필요한 이유
  3. 불변 객체 생성 방법
  4. 주의사항과 모범 사례
  5. 성능과 최적화
  6. 면접 예상 질문

1. 불변 객체란?

불변 객체(Immutable Object)는 생성 후 그 상태를 변경할 수 없는 객체를 의미합니다. Java에서 가장 대표적인 불변 객체는 String 클래스입니다. 불변 객체는 한번 생성되면 그 내부의 상태가 절대로 변하지 않음을 보장합니다.

불변 객체의 특징

  • 모든 필드가 final로 선언됨
  • 클래스가 상속을 막기 위해 final로 선언됨
  • 모든 필드가 private으로 선언됨
  • setter 메소드가 존재하지 않음
  • 가변 객체를 참조하는 필드의 경우, 참조값을 외부로 노출시키지 않음

2. 불변 객체가 필요한 이유

스레드 안전성

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;
    }
}

불변 객체는 생성 시점 이후 상태가 변하지 않기 때문에, 동기화 없이도 여러 스레드에서 안전하게 공유할 수 있습니다.

캐시 활용

동일한 값을 가지는 객체는 항상 동일한 상태를 유지하므로, 캐시를 통해 객체 재사용이 가능합니다.

방어적 복사 불필요

불변 객체는 복사해도 원본과 동일한 객체이므로, 방어적 복사가 필요 없습니다.

3. 불변 객체 생성 방법

3.1 전통적인 방식

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); // 불변 컬렉션 반환
    }
}

3.2 Record 클래스 활용 (Java 14+)

public record PersonRecord(
    String name,
    int age,
    List<String> hobbies
) {
    // 생성자에서 방어적 복사
    public PersonRecord {
        hobbies = List.copyOf(hobbies);
    }
}

Record 클래스는 Java 14에서 도입된 새로운 형태의 불변 데이터 클래스입니다.
주요 특징:

  • 모든 필드가 private final
  • public 생성자 자동 생성
  • equals(), hashCode(), toString() 자동 구현
  • getter 메소드 자동 생성 (필드명과 동일)

3.3 Lombok @Value 활용

@Value는 Lombok에서 제공하는 불변 클래스를 위한 애노테이션입니다. @Data의 불변 버전이라고 생각하면 됩니다.

@Value 애노테이션이 자동으로 생성하는 것들:

  • 모든 필드를 privatefinal로 만듦
  • 클래스를 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();
    }
}

4. 주의사항과 모범 사례

깊은 불변성 확보

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) {}

Collection 불변성 보장

  • List.copyOf(), Set.copyOf(), Map.copyOf() 활용
  • Collections.unmodifiableList(), unmodifiableSet(), unmodifiableMap() 활용
  • ImmutableList, ImmutableSet, ImmutableMap (Guava 라이브러리) 활용

5. 성능과 최적화

메모리 사용

불변 객체는 값이 변경될 때마다 새로운 객체를 생성하므로, 변경이 빈번한 경우 메모리 사용량이 증가할 수 있습니다.

// 비효율적인 예
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);
    }
}

6. 면접 예상 질문

Q: 불변 객체의 장점과 단점을 설명해주세요.
A: 장점으로는 스레드 안전성, 캐시 활용 가능, 방어적 복사 불필요, 실패 원자성 보장 등이 있습니다. 단점으로는 값이 변경될 때마다 새로운 객체 생성이 필요하여 메모리 사용량이 증가할 수 있다는 점입니다.

Q: Java의 String이 불변인 이유는 무엇인가요?
A: 보안성(문자열 풀 사용), 동기화 이슈 방지, 해시코드 캐싱, 문자열 풀을 통한 메모리 절약 등의 이유로 String은 불변으로 설계되었습니다.

Q: Record와 일반 클래스로 구현한 불변 객체의 차이점은 무엇인가요?
A: Record는 불변성을 기본으로 제공하며, equals, hashCode, toString 등의 메서드를 자동으로 생성합니다. 또한 직렬화가 자동으로 구현되며, 상속을 허용하지 않습니다. 반면 일반 클래스는 이러한 기능들을 직접 구현해야 합니다.

참고 자료

profile
성장하고싶은 Backend 개발자

0개의 댓글