JAVA

8. Mutable & Immutable

ggomjiu 2025. 3. 28. 13:33

- 자바는 new 연산자로 객체를 생성할 수 있음

-> 이때 heap 영역에 할당되고 stack영역에서 참조 타임 변수를 통해 데이터에 접근

- 이때 자바의 객체의 타입은 2가지가 존재

- Mutable(가변), Immutable(불변)

 

Immutable(불변) 객체

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

- EX) String, Boolean, Integer, Float, Long

- String을 제외하고는 원시 타임의 wrapper타입

String name = "정윤";
name = "jeongyoon";
System.out.println(name); // jeongyoon

- 실제로는 객체의 값이 변경된 것이 아니라 새로운 객체를 생성하고 이 객체에 대한 참조값을 변경한 것

 

cf) 불변 객체를 사용하는 이유

- 클래스들은 가변적이어야 하는 매우 타당한 이유가 있지 않는 한 반드시 불변이어야 함

- 만약 클래스를 불변으로 만드는 것이 불가능하다면, 가능한 변경 가능성을 최소화해야 함

1) 단순하다

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

2) 일반적으로 스레드의 안정성 보장

  • multi-thread환경에서 동기화 문제가 발생하는 이유는 공유 자원에 동시 쓰기 연산 때문 -> 이때, 불변 객체라면 항상 동일한 값만 반환
  • multi-thread 호나경에서 동기화 처리없이 객체 공유가 가능

3) 값의 변경을 방지

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

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

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

1. String(Immutable 객체)

- 대표적인 Immutable 객체로 읽을 수만 있고 변경은 할 수 없음 (ReadOnly)

- 이러한 특징 때문에 Mutable 객체인 StringBuilder, StringBuffer를 자루 사용함

 

2. StringBuilder(Mutable 객체)

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

 

3. StringBuffer(Mutable 객체)

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

 

불변 객체 구현

  1. 생성자를 제외하고 객체의 상태를 변경하는 메서드(ex. setter)를 사용하지 않음
  2. 클래스를 확장할 수 없도록 함 -> final 클래스로 선언 등의 방법으로 상속을 막음
  3. 모든 필드를 private final로 선언
  4. 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 함 -> 클래스에 가변 객체를 참조하는 필드가 하나라도 있다면, 클라이언트가 그 객체의 참조를 그대로 반환하도록 하지 말고 방어적 복사를 수행해야 함

방어적 복사

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

- 장점 )

  • 방어적 복사를 사용하면 외부에서 객체를 변경해도 내부의 객체는 변경되지 않음

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

import java.util.ArrayList;
import java.util.List;
public class Application { public static void main(String[] args) {
List<Name> originalNames = new ArrayList<>();
originalNames.add(new Name("judy"));
originalNames.add(new Name("hash"));
Names crewNames = new Names(originalNames); // crewNames의 names: judy, hash
originalNames.add(new Name("neo")); // crewNames의 names: judy, hash, neo } }

- 주소 값을 공유하고 있기 때문에 crewNames에 이름이 추가가 됨

 

2. 방어적 복사를 한 경우

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

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

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

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

- 복사본으로 초기화하여 원본 값과 주소 공유를 끊었기 때문에 더 이상 외부 값 변경에 따라 변하지 않음- 자바는 new 연산자로 객체를 생성할 수 있음

 

cf) 얕은 복사 vs 깊은 복사

▶ 얕은 복사
: 주소만 복사하는 것
- 외부에서 names 리스트를 수정하면 this.names도 같이 바뀜
- this.names와 생성자 인자로 받은 name는 같은 리스트 객체를 가리키고 있기 때문

▶ 방어적 복사
: names의 내용만 복사한 새로운 리스트를 만드는 것
- 외부에서 names를 수정해도 this.names는 영향을 안받음
- new ArraysList<>(names)는 리스트 안의 원소들을 복사해서 새로운 리스트를 생성하는 것
-> 이 리스트의 참조를 this.names에 담기 때문에 주소를 공유하지 않게 됨

 

  • 방어적 복사는 깊은 복사일까 
    • 아님
    • 컬렉션의 주소만 바뀌었을 뿐, 내부 요소들은 여전히 주소를 공유하고 있음
    • 원본의 내부 요소를 바꾸면 복사본도 바뀌게 됨
    • 외부로부터의 변경에 취약하지 않도록 객체를 불변으로 만들고자 한다면 내부 요소들 또한 불변이어야 함
  • reference 타입이나 collection은 final이면 불변일까
    • 아님
    • 재할당이 불가능한 것은 맞지만 불변은 아님

Unmodifiable Collection

: 읽기 전용 컬렉션

- 컬렉션을 외부에서 변경하지 못하도록 막는 용도

- Why )

  • 불변성을 보장하고 싶을 때
  • 캡슐화 유지 : 외부에서 객체의 내부 컬렉션을 함부러 바꾸지 못하게 막음
  • 방어적 복사와 함께 사용하면 아주 안전한 코드가 됨

- 외부에서 변경 시 예외처리되기 때문에 안전하게 보장할 수 있음

- 즉, getter로 값을 꺼내도 데이터를 수정할 수 없음