본문 바로가기

Java

불변 객체(Immutable Object)

불변 객체(Immutable Object)는 생성 후 그 상태를 변경할 수 없는 객체를 말한다.
불변 객체의 상태는 객체가 생성될 때 설정되며, 그 이후에는 변경될 수 없다. Java에서는 대표적으로 'String', 'BigInteger', 'BigDecimal' 등이 있다.
예를들어 'String'클래스의 객체는 한번 생성되면 그 값을 변경할 수 없으며, 문자열을 변경할 때마다 새로운 'String' 객체가 생성된다

 

불변 객체의 장점

  1. 스레드 안정성(Thread-Safety) : 불변객체는 스레드 안전성이 있어 병렬 프로그래밍에 유용하고 동기화를 고려하지 않아도 된다. 여러 스레드에 의해 동시에 사용되어도 상태가 변경되지 않기 때문이다. 멀티 스레드 환경에서 발생하는 주된 문제는 공유자원에 대해 서로 변경하다가 값이 덮어씌워지는 경우를 들 수 있는데, 불변객체는 항상 동일한 값을 보장하므로 동기화를 신경쓸 필요가 없다는 장점이 있다.
    예전에
    " 예를 들어 String객체가 여러 스레드에서 사용되다가 한 스레드에서 값을 변경하여 새로운 다른값을 지닌 String객체가 생성되어 문제가 생길 수 있지 않을까?
    라는 생각을 했던적이 있었는데, 이는 불변 객체의 문제가 아니라, 스레드 간 상태 공유와 동기화 방법에 주의를 기울여야 하는 문제라고 결론을 낸 기억이 있다.

  2. 가비지 컬렉션과 성능 : 불변객체는 재사용이 용이하므로, 필요할 때마다 새로운 객체를 생성하는 것보다 메모리 사용량을 줄일 수 있다. 불변 객체의 잦은 생성은 가비지 컬렉터에 영향을 줄 수 있어 성능에 부정적인 영향을 미칠 수도 있지 않을까 라는 의문이 생길 수도 있는데 이에 답변이 되는 내용을 ORACLE Java Documentation에서 찾을 수 있었다.
    다음은 ORACLE Java Documentation의 일부 내용이다.

    Programmers are often reluctant to employ immutable objects, because they worry about the cost of creating a new object as opposed to updating an object in place. The impact of object creation is often overestimated, and can be offset by some of the efficiencies associated with immutable objects. These include decreased overhead due to garbage collection, and the elimination of code needed to protect mutable objects from corruption.
    https://docs.oracle.com/javase/tutorial/essential/concurrency/immutable.html

  3. 캐시 가능 : 불변 객체는 그 내용이 변하지 않기 때문에, 같은 값을 가지는 인스턴스가 여러번 요청되더라도 처음 생성된 인스턴스를 재사용할 수 있다. 따라서 공통적으로 사용되는 값들에 대해 메모리 사용을 줄이고 성능 향상에 도움을 줄 수 있다.
    Java의 Integer.valueOf(int) 메서드를 예로 들 수 있다.
       private static class IntegerCache {
            static final int low = -128;
            static final int high;
            static final Integer[] cache;
            static Integer[] archivedCache;
            ....
        }
        
        @HotSpotIntrinsicCandidate
        public static Integer valueOf(int i) {
            if (i >= IntegerCache.low && i <= IntegerCache.high)
                return IntegerCache.cache[i + (-IntegerCache.low)];
            return new Integer(i);
        }​

    위 코드를 보면 알 수 있듯이, 메서드 내부적으로 -128에서 127까지(생략된 부분)의 Integer 인스턴스를 캐싱하고 있다. 그래서 이 범위 내의 Integer 객체를 요청할 때마다 동일한 인스턴스를 반환한다.

  4.  안전한 해시코드 캐싱 : 해시코드를 계산하는 비용이 큰 경우, 최초 계산된 해시코드를 내부적으로 저장해 두고 이후 저장된 값을 사용하는 방식으로 성능을 향상시킬 수 있다. 해시 기반의 컬렉션인 HashMap, HashSet 등에서 불변 객체를 사용할때 특히 유용하다.

 

불변 객체 생성 예

객체의 모든 필드를 final로 선언하여 객체 내부에서 변경 가능한 값을 허용하지 않아야 한다.

import java.util.Collections;
import java.util.List;
import java.util.ArrayList;

public final class BookCollection {
    private final List<Book> books;

    public BookCollection(List<Book> books) {
        // 컬렉션의 불변 복사본을 생성하여 저장
        this.books = Collections.unmodifiableList(new ArrayList<>(books)); //Java 8 이상
        this.books = List.copyOf(books); //Java 10 이상
    }

    public List<Book> getBooks() {
        // 외부에서 컬렉션을 변경할 수 없도록 불변 리스트를 반환
        return books;
    }

    // 새로운 책을 추가한 새로운 BookCollection 객체를 반환하는 메서드
    public BookCollection addBook(Book newBook) {
        List<Book> updatedBooks = new ArrayList<>(this.books);
        updatedBooks.add(newBook);
        return new BookCollection(updatedBooks);
    }
}

 

위 예제에서는  BookCollection 클래스가 List<Book> books를 갖고 있다. 그리고 생성자에서 List의 불변 복사본을 생성하여 저장한다. 그러므로 books필드의 직접적인 변경을 허용하지 않는 불변 리스트를 반환한다.

이러한 방법으로 외부에서 addBook 메서드로 books 리스트의  변경을 시도할 때,

BookCollection 객체의 books를 변경하지 않고 새로운 BookCollection 객체를 생성하는 방식으로 객체의 불변성을 유지한다.

Collections.unmodifiableList()를 사용할 때 주의할 점은 새로운 리스트를 복제해서 사용해야 한다는 점이다. 그 이유는 UnmodifiableList 클래스의 내부 구조와 생성자를 보면 알 수 있다.

static class UnmodifiableList<E> extends UnmodifiableCollection<E>
                                  implements List<E> {
        private static final long serialVersionUID = -283967356065247728L;

        final List<? extends E> list;

        UnmodifiableList(List<? extends E> list) {
            super(list);
            this.list = list;
        }
        ...
}

 

클래스 내부를 보면 list라는 필드를 갖고, 생성자에서 이 list에 파라미터를 대입하기만 한다. 내부의 list와 파라미터로 받은 원본 list는 동일한 객체를 바라보고만 있는 형태이다. 따라서 파라미터로 받은 원본 list에 변형이 가해지면 내부의 list도 변경이 가해진다.

 

ChatGPT4 DALL.E : 자바(커피)와 단단한 블록(불변성)

 

'Java' 카테고리의 다른 글

리플렉션(Reflection)  (0) 2024.03.14
Singleton  (0) 2024.03.09
Builder Pattern (빌더 패턴)  (0) 2024.03.02
BigInteger.valueOf와 정적 팩토리 메서드  (0) 2024.02.29
람다 표현식 - 2. 자바 함수형 인터페이스  (0) 2023.12.05