Computer Science/Java

Mutable vs Immutable

eunnnn 2023. 4. 17. 02:16

Immutable 객체

자바에서 객체는 기본적으로 heap영역에 할당되고 stack영역에  래퍼런스 값을 갖는 참조 변수들로 접근 가능하다.

Immutable 객체란 불변 객체를 말하는 것으로, 이 객체의 값을 heap 영역에서 바꿀 수 없다는 뜻이다. 오직 새 객체를 만들어 래퍼런스 값을 주는 재 할당만이 가능하다.

Integer i = 1;
i = 3;

위와 같이 우리는 i라는 Integer 타입 변수를 1에서 3으로 변경할 수 있다.

그러나 i가 가리키는 객체의 값이 1에서 3으로 변경된 것이아니라 실제로는 3의 값을 가지는 새로운 객체를 생성하고 이 객체를 가리키도록 i의 참조값을 변경한 것이다. 기존 1로 할당되어 있던 객체는 Garbage로 남아있다가 GC(Garbage collection)에 의해 사라지게 된다.  

 

immutable 객체의 종류에는 대표적으로 String, Boolean, Integer, Float, Long 등이 있다.

 

Mutable 객체

Mutable 객체는 가변 객체로, 불변 객체와는 다르게 힙 영역에 생성 된 객체를 변경할 수 있다. 우리가 자바에서 사용하는 대부분의 객체는가변 객체이다. 가변 객체는 멀티 스레드 환경에서 사용하기 위해 별도의 동기화 처리가 필요하다.   

대표적인 가변 객체는 List, ArrayList, HashMap,StringBuilder,StringBuffer 등이 있고, 동기화 처리까지 완료 된 것이 StringBuffer이다.


Immutable 객체를 사용해야 하는 이유

1. Thread-Safe하여 병렬 프로그래밍에 유용하며, 동기화를 고려하지 않아도 된다.

멀티 쓰레드 환경에서 동기화 문제가 발생하는 이유는 공유 자원에 동시에 쓰기(Write) 때문이다. 하지만 만약 공유 자원이 불변이라면 더 이상 동기화를 고려하지 않아도 될 것이다. 왜냐하면 항상 동일한 값을 반환할 것이기 때문이다. 이는 안정성을 보장할 뿐만 아니라 동기화를 하지 않음으로써 성능상의 이점도 가져다준다.

 

2. 실패 원자적인(Failure Atomic) 메소드를 만들 수 있다.

가변 객체를 통해 작업을 하는 도중 예외가 발생하면 해당 객체가 불안정한 상태에 빠질 수 있고, 불안정한 상태를 갖는 객체는 또 다른 에러를 유발할 수 있다. 하지만 불변 객체라면 어떠한 예외가 발생하여도 메소드 호출 전의 상태를 유지할 수 있을 것이다. 그리고 예외가 발생하여도 오류가 발생하지 않은 것 처럼 다음 로직을 처리할 수 있다.

 

3. Cache나 Map 또는 Set 등의 요소로 활용하기에 더욱 적합하다.

만약 캐시나 Map, Set 등의 원소인 가변 객체가 변경되었다면 이를 갱신하는 등의 부가 작업이 필요할 것이다. 하지만 불변 객체라면 한 번 데이터가 저장된 이후에 다른 작업들을 고려하지 않아도 되므로 사용하는데 용이하게 작용할 것이다.

 

4. 부수 효과(Side Effect)를 피해 오류가능성을 최소화할 수 있다.

불변 객체는 기본적으로 값의 수정이 불가능하기 때문에 변경 가능성이 적으며, 객체의 생성과 사용이 상당히 제한된다. 그렇기 때문에 메소드들은 자연스럽게 순수 함수들로 구성될 것이고, 다른 메소드가 호출되어도 객체의 상태가 유지되기 때문에 안전하게 객체를 다시 사용할 수 있다. 이러한 불변 객체는 오류를 줄여 유지보수성이 높은 코드를 작성하도록 도와줄 것이다.

 

5. 다른 사람이 작성한 함수를 예측가능하며 안전하게 사용할 수 있다. 

불변성이 보장된 함수라면 다른 사람이 개발한 함수를 위험없이 이용할 수 있다.

 

6. 가비지 컬렉션의 성능을 높일 수 있다.

 


Immutable 객체를 사용해야 하는 이유

final 키워드

Java에서 변수들은 기본적으로 가변적인데, 변수에 final 키워드를 붙이면 참조값을 변경 못하도록 하여 불변성을 확보할 수 있다. final이 붙은 변수의 값을 변경하려고 하면 컴파일 에러가 발생한다.

final String name = "Old";
name = "New";  // 컴파일 에러

그러나 이렇게 final 키워드를 이용하여 Immutable 객체를 만드는 것에는 한계가 존재한다.

예를 들어 아래와 같이 final로 선언된 List에는 새로운 객체가 더해져도(상태가 변해도) 문제가 없다. 그렇기 때문에 Java에서는 참조에 의해 값이 변경될 수 있는 점들을 유의해야 하는데, 이를 방지하려면 불변 클래스로 만들어야 한다.

final List<String> list = new ArrayList<>();
list.add("a");

 

불변 클래스

Java에서 불변 객체를 생성하기 위해서는 다음과 같은 규칙에 따라서 클래스를 생성해야 한다.

  1. 클래스를 final로 선언하라
  2. 모든 클래스 변수를 private와 final로 선언하라
  3. 객체를 생성하기 위한 생성자 또는 정적 팩토리 메소드를 추가하라
  4. 참조에 의해 변경가능성이 있는 경우 방어적 복사를 이용하여 전달하라
public final class ImmutableClass {
    private final int age;
    private final String name;
    private final List<String> list;

    private ImmutableClass(int age, String name) {
        this.age = age;
        this.name = name;
        this.list = new ArrayList<>();
    }

    public static ImmutableClass of(int age, String name) {
        return new ImmutableClass(age, name);
    }
    
    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }

    public List<String> getList() {
        return Collections.unmodifiableList(list);
    }
    
}

위의 코드에서 특히 주목해야 하는 부분은 내부 생성자를 만드는 대신 객체의 생성을 위해 정적 팩토리 메소드를 제공하고 있다는 점과 참조를 전달하여 클라이언트에 의해 수정가능성이 있는 list를 방어적 복사하여 제공하고 있다는 것이다.

Java에서는 생성자를 선언하지 않으면 기본 생성자가 자동으로 생성되는데, 그러면 다른 클래스에서 해당 객체를 자유롭게 호출할 수 있다. 그렇기 때문에 내부 생성자를 만드는 대신 정적 팩토리 메소드를 통해 객체를 생성하도록 강요하는 것이 좋다.

또한 배열이나 다른 객체 또는 컬렉션은 참조가 전달되어 수정가능성이 있다. 그렇기 때문에 참조를 통해 변경이 가능한 경우에는 방어적 복사를 통해 값을 반환해야 한다. 마지막으로 클래스의 변수에 가능하다면 final을, final이 불가능하다면 Setter를 최소화해야한다.

 

정적 팩토리 메소드와 방어적 복사에 관한 내용은 해당 포스팅에서 설명할 내용은 아닌 것 같아, 잘 정리되어 있는 링크를 함께 첨부해두겠다.

https://ttl-blog.tistory.com/1206

 

[Java] 얕은 복사, 방어적 복사, 깊은 복사

객체를 복사하는 방법에는 크게 얕은 복사와 깊은 복사, 그리고 방어적 복사가 있습니다. 이러한 방법들이 각각 어떻게 동작하는지, 어떤 상황에서 사용하는 것이 적절한지 알아보도록 하겠습

ttl-blog.tistory.com

https://velog.io/@cjh8746/%EC%A0%95%EC%A0%81-%ED%8C%A9%ED%86%A0%EB%A6%AC-%EB%A9%94%EC%84%9C%EB%93%9CStatic-Factory-Method

 

정적 팩토리 메서드(Static Factory Method)

정적 팩토리 메서드란 무엇인가?

velog.io

 

 

 

출처

더보기