Java

[Java] mutable과 Immutable

ukkkk7 2023. 12. 6. 23:38
728x90
반응형

 

☝mutable과 Immutable?

사전적 정의

mutable : 변할 수 있는, 잘 변하는

immutable : 변경할 수 없는, 불변의

 

 

Immutable object

정의: 불변 객체는 데이터 변경이 불가능한 객체라고 말한다.

종류: String, Boolean, Integer, Float, Long, Double 등 String을 제외하면 기본형 타입의 wrapper 클래스이다.

Integer i = 1;
i = 3;

System.out.println(i); // 3출력

 

이같은 상황에서 우리는 i의 값을 변경해주었다고 생각했을 것이다. 하지만 실제로는 값이 변경된것이 아니라 heap영역에 새로운 객체를 생성하고 객체에 대한 참조값을 변경한 것이다.

 

위의 그림과 같이 heap영역에 새로운 객체를 생성하고 참조값을 변경했다. 그리고 기존 할당되었던 객체는 Garbage Collection에 의해 사라지게 된다.

 

🧐불변객체를 왜 사용하며 장단점은 무엇이 있을까??

 

스레드의 안정성(Thread Safety)보장

multi-thread 환경에서 동기화 문제가 발생하는 이유는 공유 자원에 동시 쓰기(write) 연산 때문이다. 만약 공유 자원이 불변 객체라면? 항상 동일한 값만 반환하기 때문에 동기화를 고려하지 않아도 된다. 이는 안정성을 보장해주며 동기화를 하지 않아 성능상의 이점도 가져다 준다.

 

값의 변경을 방지

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

 

But

객체의 값이 할당될 때마다 새로운 객체가 필요하다. 따라서 메모리 누수와 성능저하를 발생시킬 수 있다. 

 

 

✔️ String, StringBuilder, StringBuffer

String은 Immutable 즉 불변 객체라고 했다. 바꿔 말하면 멀티쓰레드 환경에서 Thread-safe하다고 할 수 있지만 새로운 값을 할당할 때마다 기존 객체가 heap메모리 영역에 삭제될 때까지 남아있어 메모리 관리 측면에서 좋지않다.

 

만일 문자열추가,삭제,수정 등이 빈번하게 일어난다면  StringBuilder 혹은 StringBuffer를 사용한다.

 

StringBuilder는 mutable 객체로 가변 객체이다. 동기화를 지원하지 않기 때문에 멀티 쓰레드 환경이 아닌 단일 쓰레드 환경에서만 사용하는 것이 적합하다.

 

StringBuffer는 각 메소드 별로 Synchronized keyword가 존재하기 때문에 멀티 스레드 상태에서 동기화를 지원한다.

 

Synchronized?

Synchronized 키워드는 여러개의 스레드가 한 개의 자원에 접근하려고 할 때, 현재 데이터를 사용하고 있는 스레드를 제외하고 나머지 스레드들이 데이터에 접근할 수 없도록 막는 역할을 수행

 

 

☝ 불변 객체 만드는 법

  • 클래스를 확장할 수 없도록 한다 -> 클래스를 final로 선언한다.
  • 모든 클래스 변수를 private final로 선언한다.
  • 객체를 생성하기 위한 생성자 또는 정적 팩토리 메소드를 추가한다.
  • 자신 외에는 내부의 가변 컴포넌트에 접근할 수없도록 한다.
    • 참조에 의한 변경이 있는 경우 방어적 복사를 이용하여 전달한다.

 

☝방어적 복사?

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

 

👀 방어적 복사를 사용하지 않았을 때

public class Name {
    private final String name;

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

//이름을 의미하는 Name 클래스

import java.util.List;

public class Names {
    private final List<Name> names;

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

//List<name>을 가지는 Names클래스(일급 컬렉션)이 있다. 생성자의 인자로 List<Name>을 받는다.

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("jay"));
        originalNames.add(new Name("ann"));
        
        Names crewNames = new Names(originalNames); // crewNames의 names: jay, ann
        originalNames.add(new Name("go")); // crewNames의 names: jay, ann, go
    }
}

 

위와같이 originalNames에 go라는 이름을 추가하게 된다면  crewNames에도 go가 추가된다. 주소값을 공유하고 있기 때문에 이러한 상황이 발생한다.

 

👀 방어적 복사를 한 경우

 

public Names(List<Name> names) {
    this.names = 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);
    }
}

위와같이 매개변수로 받은 names의 값을 new ArrayList<>()를 이용해 복사본으로 names필드를 초기화 해주었기 때문에

원본 값과 주소 공유를 끊고 더 이상 외부 값 변경에 따라 변하지 않게 된다.

 

❗잠깐 깊은 복사와 얕은 복사의 개념을 정리하고 가보자

 

🔍얕은복사

원본 객체를 복사할 때, 새로운 객체를 만들지만 원본 객체의 '주소 값'을 참조하는 복사이다.

따라서 원본이나 복사한 객체나 변경이 되면 서로 영향을 미친다(call-by-reference와 유사한 개념)

 

🔍깊은복사

원본 객체를 복사할 때, 새로운 객체를 만들고 원본 객체의 모든 값을 복사해서 원본 객체로부터 독립적인 객체를 생성

따라서❗원본복사한 객체독립적이므로 변경이 되어도 서로 영향을 미치지 않는다 (call-by-values와 유사한 개념)

 

방어적 복사는 깊은 복사일까

NO - 컬렉션의 주소만 바뀌었을 뿐 내부 요소들은 여전히 주소를 공유하고 있다. , 원본의 내부 요소를 바꾸면 복사본도 바뀐다. 

 

collection과 참조 타입은 final이면 불변일까❓

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

 

근데 왜? 

재할당은 불가능 하지만 불변은 아니다.

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

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

System.out.println(baseNames.toString()); //jay ann go
System.out.println(getNames.toString());  //[jay ann go]

 

baseNames 객체를 생성할 때 방어적 복사를 했다. 그리고 외부에 getter로 꺼낸 names는 add 메소드를 통해 값이 변한다.

getter로 꺼낸 getNames를 변화시키면 기존 객체의 상태가 변하게 된다.

 

이를 해결하기 위해선 반환할 때도 방어적 복사를 하거나, List자체를 불변으로 만드는 Collections.unmodifiableList와 같은 불변 자료구조로 만들어야 한다.

 

unmodifable Collection

외부에서 변경 시 예외처리 되기 때문에 안전하게 불변성 보장

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

public List<Name> getNames() {
    return Collections.unmodifiableList(names);
}

//이렇게 반환한 값을 아래처럼 수정하려고 한다면

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

 

 

위와같은 에러가 발생한다.

 

불변객체에 대해 알아보며 깊은 복사, 얕은 복사, 방어적 복사에 대해 간략히 알아보았다. 이것만으론 정리가 부족하기에 추후에 복사에 대해 정리해서 포스팅 해야겠다.

728x90
반응형