Computer Science/JAVA

Mutable, Immutable

꽃요미 2024. 11. 26. 16:21

* Mutable, Immutable

 - 자바는 new 연산자로 객체를 생성할 수 있고, 이때 heap 영역에 할당되고 stack 영역에서 참조 타입 변수를 통해 데이터에 접근한다.

 이때 자바의 객체의 타입은 두 가지 있다.

 - Mutable (가변) 객체와 Immutable (불변) 객체이다.

 

* Immutable (불변) 객체

  - 정의 : 객체 생성 이후에는 객체의 상태가 바뀌지 않는 객체

  - 불변 객체 종류 : String, Boolean, Integer, Float, Long 등...

  - String을 제외하고 원시 타입의 wrapper 타입이다

 

Q. 불변 객체는 왜 사용할까요?

A. 클래스들은 가변적이어야 하는 매우 타당한 이유가 있지 않는 한 반드시 불변으로 만들어야 한다. 만약 클래스를 불변으로 만드는 것이 불가능하다면, 가능한 변경 가능성을 최소화 해야함.

 

 1. 단순하다.

   - 불변 객체의 상태는 생성된 시점으로부터 파괴되는 시점까지 그대로 유지된다.

 2. 일반적으로 스레드 안정성(Thread Safety) 보장

   - multi-thread 환경에서 동기화 문제가 발생하는 이유는 공유 자원에 동시 쓰기 연산 때문이다.

   - multi-thread 환경에서 동기화 처리 없이 객체 공유가 가능하다.

 3. 값의 변경을 방지

   - 불변객체는 생성 시점에 값을 설정한 후, 변경할 수 없기 때문에 예기치 않은 값 변경을 방지할 수 있다.

 4. 불변 객체는 객체의 필드로 사용

   - 불변 객체를 필드로 사용하면 방어적 복사를 할 필요가 없다.

 

 

* String, StringBuilder, StringBuffer

  1. String ( Immutable 객체 )

    - String은 대표적인 Immutable 객체로 읽을 수만 있고 변경 불가능.

 

  2. StringBuilder (Mutable)

    - StringBuilder는 단일 스레드 환경에서만 사용하도록 설계되어 있다.

 

  3. StringBuffer (Mutable)

    - StringBuffer는 각 메서드 별로 synchronized keyword가 존재하여 멀티 스레드 상태에서 동기화를 지원함. 

 

 

* 불변 객체 구현

 1. 생성자를 제외하고 객체의 상태를 변경하는 메서드 (ex.setter)를 사용하지 않는다.

 2. 클래스를 확장할 수 없도록 한다. - final 클래스로 선언 등의 방법으로 상속을 막는다.

 3. 모든 필드를 private final로 선언한다.

 4. 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.

  - 클래스에 가변 객체를 잠조하는 필드가 하나라도 있다면, 클라이언트가 그 객체의 참조를 그대로 반환받도록 하지 말고

  방어적 복사를 수행해야 한다.

 

* 방어적 복사 vs Unmodifiable Collection

 

[ 방어적 복사 ]

  - 의미 : 생성자의 인자로 받은 객체의 복사본을 만들어 내부 필드를 초기화 하거나, getter 메서드에서 내부의 객체를 반환할때, 객체의  복사본을 반환하는 것을 말한다.

  - 장점 : 방어적 복사를 사용하면 외부에서 객체를 변경해도 내부의 객체는 변경되지 않는다.

 

1. 방어적 복사를 하지 않을 때

 - Name 클래스

public class Name {
    private final String name;

    public Name(String name) {
        this.name = name;
    }
}

 

 - Names 클래스 (일급 컬렉션) : List<Name>을 가지고 있다.

import java.util.List;

public class Names {
    private final List<Name> names;
    
    public Names(List<Name> names) {
        this.namse = names;
    }
}

 

 

 

2. 방어적 복사를 한 경우

- 기존에는 생성자에서 인자를 받고 바로 초기화했다면

public Names(List<Name> names) {
    this.names = names;
}

 

방어적 복사를 하는 경우에는 생성자에서 인자를 받으면서 new ArrayList<>()를 이용해 만든 복사본으로, 필드 names를 초기화 함.

 

import.java.util.*;

public class Names {
    private final List<Name> names;
    
    public Names(List<Name> names) {
        /* 방어적 복사 */
        this.names = new ArrayList<>(names);
    }
}

- 복사본으로 초기화하여 원본 값과 주소 공유를 끊었기 때문에 더 이상 외부 값 변경에 따라 변하지 않는다.

 

3. Q.방어적 복사는 깊은 복사일까?

- 아니다. 컬렉션 주소만 바뀌었을 뿐. 내부 요소들은 여전히 주소를 공유하고 있다.

즉, 원본의 내부 요소를 바꾸면 복사본도 바뀌게 된다.

- 그래서 외부로부터의 변경에 취약하지 않도록 객체를 불변으로 만들고자 한다면 내부 요소들 또한 불변이어야 한다.

 

 

4. Q.reference 타입이나 collection은 final이면 불변일까?

A. 아니다.

- 위의 예시에서 객체를 생성할 때 방어적 복사를 통해 외부 참조를 끊었다. 또한 names 필드를 private final로 선언하여 재할당이 불가능하다. 또한 현재 상태를 변화시킬 수 있는 로직 또한 없다.

- 재할당이 불가능한 것은 맞지만 불변인 것은 아니다.

 

List<Name> names = new ArrayList<>();
names.add(new Name("judy"));
names.add(new Name("neo"));
Names baseNames = new Names(names);

List<Name> getNames = baseNames.getNames();
getNames.add(new Name("hash"));

System.out.println(baseNames.toString());
System.out.println(getNames.toString());

 

 

[Unmodifiable Collection]

 

 

 

* 결론

 - 클래스들은 가변적이어야 하는 매우 타당한 이유가 있지 않는 한 반드시 불변으로 만들어야 한다. 만약 클래스를 불변으로 만드는 것이 불가능하다면, 가능한 변경 가능성을 최소화해야함.

댓글수0