[Java] 불변 객체(Immutable Object)

이재민·2024년 1월 22일

Java

목록 보기
2/3

Immutable Object

불변 객체란 인스턴스의 내부 값을 수정할 수 없는 객체를 말합니다.
불변 인스턴스의 정보는 생성되는 순간 고정되어 해당 객체가 소멸되는 순간까지 절대 달라지지 않습니다.

  • 대표적인 Java Library 불변 클래스
    • String 클래스
    • 기본 타입(Primitive type)의 박싱된 클래스
    • BigInteger, BigDecimal

불변 클래스 설계 원칙

1. 객체의 상태를 변경하는 메서드를 제공하지 않는다.
2. 클래스를 확장할 수 없도록 한다.
3. 모든 필드를 final로 선언한다.
4. 모든 필드를 private로 선언한다.
5. 자신 외에는 내부 가변 컴포넌트에 접근할 수 없도록 한다.(방어적 복사)

불변 클래스의 장점

1. 객체에 대한 신뢰도가 높아집니다. 객체가 생성된 후 변경되지 않기에 믿고 쓸 수 있기 떄문입니다.

  • 이는 유지보수성이 높은 코드를 작성할 수 있습니다.
  • 변경이 이뤄질 것 같은 코드를 하나씩 보지 않아도 확인하지 않아도 되기 때문입니다.

2. 멀티스레드 환경에서 동기화 처리 없이 객체를 공유할 수 있습니다.

  • 멀티스레드 환경에서는 여러 스레드가 공유 자원에 대한 접근과 변경이 가능하기에 데이터 정합성 문제가 발생합니다.
  • 하지만, 불변객체는 항상 같은 값을 보장하기에 동기화 처리에 신경 쓰지 않아도 되는 장점이 있습니다.

3. Map, Set, Cache 요소로 활용하기 적합

  • Map, Set, Cache 요소들은 주로 해시 함수를 이용합니다. 해시 알고리즘으로 부터 얻은 해시 키는 변경되지 않아야 하기 때문에 불변 객체가 용이합니다.

4. 가비지 컬렉션의 성능 향상

  • 가변 객체에 비해 불변 객체는 상태가 변경되지 않아 객체의 생명주기가 비교적 짧을 수 있습니다. 때문에 가비지 컬렉션의 오버헤드가 감소하기에 GC 성능 향상에 도움이 됩니다.
  • 불변 객체를 사용하면 상태 변경으로 인해 새로운 객체를 생성하여 반환해줘야 하는데, 이 과정에서 객체 생성에 대한 비용이 많이 발생할 것이라고 우려하는 목소리가 많습니다. 하지만 아래 oracle 문서를 확인하면 불변객체를 이용한 효율로 상쇄가능하다는 것을 확인할 수 있습니다.
    https://docs.oracle.com/javase/tutorial/essential/concurrency/immutable.html
    또한, GC는 대부분의 개체가 짧은 기간 동안만 생존한다는 Weak Generational Hypothesis 가설에 맞춰 설계되어 있다고 합니다.
    https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/generations.html#distribution_lifetimes

불변 객체 만들기

원시 타입이 있는 경우

  • 원시 타입이 존재하는 경우는 객체의 속성이 primitive type, setter 대신 생성자 or 정적 팩토리 메소드를 사용하면 됩니다.

참조 타입이 있는 경우

public class Member {

    // 가변 객체를 property 갖고 있음
    private final Age age;

    public Member(Age age) {
        this.age = age;
    }

    public Age getAge() {
        return age;
    }
}

// 불변 객체가 아님
class Age {

    private int value;

    public Age(final int age) {
        this.value = age;
    }

    public void setValue(final int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }
}
  • Member 객체는 final 속성을 갖고 있고, setter가 없지만 불변 객체가 아닙니다.
  • 아래와 같은 코드로 값을 변경할 수 있기 때문입니다.
public class ImmutableTest {

    @DisplayName("불변 객체에서 참조 타입(가변)이 있는 경우")
    @Test
    void immutableTest1() {
        // given
        Age age = new Age(10);
        Member member = new Member(age);

        System.out.println("before: " + member.getAge().getValue()); // 10

        member.getAge().setValue(20);

        System.out.println("after: " + member.getAge().getValue()); // 20
    }
}
  • 때문에 참조 변수도 불변 객체이어한다는 결론이 도출됩니다.

Array의 경우

public class ArrayObject {

    private final int[] array;

    public ArrayObject(final int[] array) {
        this.array = Arrays.copyOf(array, array.length);
    }

    public int[] getArray() {
        return array == null ? null : array.clone();
    }
}
  • 생성자로 copyOf해서 저장하였고, getter 메소드를 호출 시 clone을 반환하도록 하였기에 외부에서 값을 변경할 수 없습니다.
  • 하지만 원시 타입 배열이 아닌 객체 타입 배열의 경우 해당 객체는 불변 객체이어야 합니다.
@DisplayName("Array의 경우 clone을 통해 불변을 유지할 수 있다.")
@Test
void immutableTest2() {
    int[] array = {1, 2, 3};
    ArrayObject arrayObject = new ArrayObject(array);

    // 1, 2, 3
    for (int number : arrayObject.getArray()) {
        System.out.println("number = " + number);
    }

    array[0] = 123123123;

    // 1, 2, 3
    for (int number : arrayObject.getArray()) {
        System.out.println("number = " + number);
    }
}

List의 경우

public class ListObject {

    private final List<Member> members;

    public ListObject(final List<Member> members) {
        this.members = new ArrayList<>(members);
    }

    public List<Member> getMembers() {
        return Collections.unmodifiableList(members);
    }
}
  • 생성시 새로운 List를 만들어 값을 복사하여 제공해야 합니다.
  • 또한 getter를 통해 값 추가/삭제가 불가능 하도록 unmodifiableList() 메소드를 이용하였습니다.
  • 하지만 unmodifiableList 와 같은 unmodifiable Collection을 사용하여 값을 변경할 경우 예외가 발생하여 직접 예외처리를 진행해야 합니다.
@DisplayName("List의 경우 unmodifiableList 사용을 통해 불변을 유지할 수 있다.")
@Test
void immutableTest3() {
    ArrayList<Member> members = new ArrayList<>();
    members.add(new Member(new Age(10)));

    ListObject listObject = new ListObject(members);

    // 10
    for (Member member : listObject.getMembers()) {
        System.out.println("member.getAge().getValue() = " + member.getAge().getValue());
    }

    members.add(new Member(new Age(20)));

    // 10
    for (Member member : listObject.getMembers()) {
        System.out.println("member.getAge().getValue() = " + member.getAge().getValue());
    }
}
profile
문제 해결과 개선 과제를 수행하며 성장을 추구하는 것을 좋아합니다.

0개의 댓글