CS/Java

Mutable & Immutable

leah-only 2025. 3. 26. 17:03

🔍 Java에서 객체 생성 

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

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

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


👉🏻 Summary 

일단 둘의 차이에 대해 간단히 알아보자. 

Mutable Immutable
객체 생성 후 내부 값 변경 가능 객체 생성 후 내부 값 변경 불가능
값을 변경할 수 있는 메서드 O (setter 존재) 값을 변경할 수 있는 메서드 X (setter 존재X)
Thread safety 하지 않을 수 있음 (멀티스레드 환경에서 동기화 필요) Thread safety(스레드 안정성) 보장
StringBuilder, StringBuffer, java.util.Date, List, ArrayList, HashMap 등 String, Wrapper Class(Integer, Boolean, Float, Long 등) 

 


String vs (StringBuffer, StringBuilder) 비교

1️⃣ String은 불변

기본적으로 자바에서는 String 객체의 값은 변경할 수 없다. 

대표적인 불변 객체로 읽을 수만 있고 변경은 할 수 없다. (read-only)

 

이 코드를 보면 str의 값이 변경된 것처럼 보인다. 하지만 실제로는 메모리에 새로 "Hello World" 값을 저장한 영역을 따로 만들고 변수 str을 다시 참조하는 식으로 작동한다. 

String str = "hello";
str = str + " world";
System.out.println(str); // hello world

 

 

자바에서 String을 불변으로 설정한 이유

String 객체를 불변하게 설계한 이유는 캐싱, 보안, 동기화, 성능 측면 이점을 얻기 위해서다.

 


2️⃣ StringBuffer / StringBuilder는 가변

StringBuffer와 StringBuilder는 문자열 데이터를 다룬다는 점에서 String 객체와 같지만, 객체의 공간이 부족해지는 경우 버퍼의 크기를 유연하게 늘려주어 가변적이다.

 

String 객체는 한번 생성되면 불변적인 특성에 의해 값 업데이트 시 새로운 문자열 String 인스턴스가 생성되어 메모리 공간을 차지하지만 StringBuffer/StringBuilder는 가변성을 가지기 때문에 .append(), .delete() 등의 API를 사용하여 동일 객체내에서 문자열 크기를 변경하는 것이 가능하다.

 

❓StringBuilder vs StringBuffer

StringBuilder는 동기화를 지원하지 않아 단일 스레드 환경에서 성능이 뛰어나다.

StringBuffer는 모든 메서드에 synchronized 키워드가 적용되어 멀티 스레드 환경에서도 안전하게 사용 가능


불변 객체는 왜 사용할까? 

 

  • 단순하다.
    • 불변 객체의 상태는 생성 시점으로부터 파괴되는 시점까지 그대로 유지된다. 
  • 일반적으로 스레드 안정성(Thread Safety) 보장
    • 멀티스레드 환경에서 문제가 발생하는 주된 원인은 공유 자원에 대한 동시 쓰기(write) 연산 때문이다. 
    • 하지만 불변 객체는 상태가 변하지 않기 때문에 동기화 없이도 여러 스레드에서 공유 가능
  • 값 변경 방지
    • 객체의 값을 변경할 수 없기 때문에 의도치 않은 값 변경을 방지할 수 있다. 
  • 불변 객체는 필드로 사용할 때 안전
    • 다른 객체의 필드로 사용할 때 방어적 복사를 할 필요가 없다. 
    • 이는 코드의 간결함과 성능 향상으로 이어진다. 

✔️ 불변 객체 구현 조건

  • final class로 선언하여 상속을 막는다. 
  • 모든 필드를 private final로 선언해서 외부 접근 막고 수정 불가능하도록 한다. 
  • setter 함수를 제거한다. 
  • 가변 객체를 참조하는 필드가 있다면 방어적 복사를 수행해야 한다. 

✔️ 방어적 복사

외부에서 전달받거나 반환할 객체를 복사해서 사용하는 것 

이렇게 하면 외부에서 객체 내부 상태를 직접 변경하지 못하게 막을 수 있다. 

 

왜 필요할까? 

  • 어떤 객체의 필드로 참조 타입 (ex. List, Date 등)이 들어올 때, 그 참조가 외부에서 변경될 수 있다. 
  • 이걸 그대로 저장하거나 반환하면 내부 상태가 의도치 않게 바뀔 위험이 있다. 
  • 이를 막기 위해 복사본을 만들어 사용한다. 이것이 방어적 복사다. 

🧨 1. 방어적 복사를 하지 않았을 때 (문제 발생)

import java.util.List;

public class Team {
    private final List<String> members;

    public Team(List<String> members) {
        this.members = members; // 방어적 복사 ❌
    }

    public List<String> getMembers() {
        return members; // 그대로 반환 ❌
    }
}
---------------------------------------------------------------
List<String> originalList = new ArrayList<>();
originalList.add("Alice");
originalList.add("Bob");

Team team = new Team(originalList);

// 외부에서 원본을 수정
originalList.add("Charlie");

// 내부 상태가 바뀜!
System.out.println(team.getMembers()); // [Alice, Bob, Charlie] 😱
  • 내부의 members 필드가 외부에서 직접 수정되어 버림
  • 캡슐화가 깨지고 클래의 불변성이 무너지게 됨 

✅ 2. 생성자에서 방어적 복사 적용 (외부 참조 끊기)

public class Team {
    private final List<String> members;

    public Team(List<String> members) {
        this.members = new ArrayList<>(members); // 생성자 방어적 복사 ✅
    }

    public List<String> getMembers() {
        return new ArrayList<>(members); // (getter) 반환 시 방어적 복사 ✅
    }
}
---------------------------------------------------------------------
List<String> originalList = new ArrayList<>();
originalList.add("Alice");
originalList.add("Bob");

Team team = new Team(originalList);
originalList.add("Charlie");

System.out.println(team.getMembers()); // [Alice, Bob] ✅
  • 생성자에서 복사본을 저장해서 이후 원본을 바꿔도 Team 내부는 안전함
  • 참조 주소를 공유하지 않음
  • getter에서도 방어적 복사

❓ 방어적 복사는 얕은 복사

방어적 복사는 깊은 복사가 아니다. 

this.members = new ArrayList<>(members);
  • List 자체는 새로 만들어진 객체다. (외부 리스트와 주소 분리됨)
  • 하지만 그 안에 들어있는 String 객체들은 그대로 같은 참조가 복사된다. 
  • 즉, 리스트 안에 들어있는 요소들은 그대로 공유되고 있는 상태
  • 즉, 원본의 내부 요소가 바뀌면 복사본도 바뀌게 된다. 

✔️ Unmodifiable Collection

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

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

public List<String> getMembers() {
    return Collections.unmodifiableList(members);
}

 

 


참고 : https://velog.io/@dabeen-jung/Java-Mutable%EA%B3%BC-Immutable-%EC%B0%A8%EC%9D%B4

참고 : https://inpa.tistory.com/entry/JAVA-%E2%98%95-String-StringBuffer-StringBuilder-%EC%B0%A8%EC%9D%B4%EC%A0%90-%EC%84%B1%EB%8A%A5-%EB%B9%84%EA%B5%90

참고 : https://github.com/devSquad-study/2023-CS-Study/blob/main/java/java_mutable_immutable.md