Java의 Mutable과 Immutable

hjkim·2021년 11월 23일
2

Mutable과 Immutable

MutableImmutable
생성된 이후 수정 가능생성된 이후 수정 불가능
이미 존재하는 객체에 재할당(값 변경)이미 존재하는 객체이더라도 새로운 객체를 생성하여 재할당
값을 변경할 수 있는 메소드 제공값을 변경할 수 있는 메소드 제공 x
Mutable class일 경우 Getter와 Setter 존재Immutable class일 경우 Getter와 Setter 미존재
thread safe하지 않을 수 있음(병렬처리 시 값 보장할 수 없게 됨)thread safe(병렬처리 시 문제 없음)
StringBuffer, StringBuilder, java.util.Date 등이 해당Legacy classes, Wrapper classes, String class 등이 해당

정리하면 Mutable은 객체의 수정을 허용하나, Immutable인 경우 객체의 수정을 허용하지 않는다. 수정이 필요할 경우 Mutable 객체는 기존의 객체에 수정사항을 곧바로 반영한다. 하지만 Immutable 객체의 경우 기존의 객체는 그대로 두고 수정사항을 반영한 새로운 객체를 생성한다는 점에서 차이가 있다. Immutable의 대표적 예시로는 String class가 존재한다.


Mutable 속성 확인

public class Mutable {
    public static void main(String[] args) {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("abc");
        System.out.println(stringBuilder);
        System.out.println(System.identityHashCode(stringBuilder));

        stringBuilder.append("def");
        System.out.println(stringBuilder);
        System.out.println(System.identityHashCode(stringBuilder));
    }
}

System.identityHashCode(Object) 코드를 통해 String 객체의 메모리 주소값을 함께 출력하였다. 실행 결과는 아래와 같았다.

같은 stringBuffer에 할당되었고 stringBuilder의 값을 abc에서 abcdef로 변경하였을 때, 메모리 주소 값이 변경되지 않았다. 이는 곧 1785210046의 메모리 주소에 할당된 abc란 값이 abcdef로 변한 것임을 나타낸다. 따라서 StringBuffer class는 Mutable하게 동작한다.


Immutable 속성 확인

public class Immutable {
    public static void main(String[] args) {
        String str = "abc";
        System.out.println(str);
        System.out.println(System.identityHashCode(str));

        str += "def";
        System.out.println(str);
        System.out.println(System.identityHashCode(str));
    }
}

마찬가지로 System.identityHashCode(Object) 코드를 통해 String 객체의 메모리 주소값을 함께 출력하였다. 실행 결과는 아래와 같았다.

같은 str 변수에 할당되었지만 str의 값을 abc에서 abcdef로 변경하였을 때, 메모리 주소 값도 같이 변경되었다. 이는 곧 1785210046의 메모리 주소에 할당된 abc란 값이 abcdef로 변한 것이 아니라 1151020327의 메모리 주소에 abcdef란 값을 가진 String 객체가 새로 생성된 것임을 나타낸다. 따라서 String class는 Immutable하게 동작한다.


Mutable한 객체를 Immutable한 객체로 사용하기

방어적 복사(defensive copy)

mutable 객체는 생성 이후 값 변경이 자유롭게 가능하여 개발자가 의도하지 않은 객체의 변경이 일어날 수 있다는 단점이 존재한다. 단점의 주 원인은 "레퍼런스를 참조한 다른 객체에서 객체를 변경"하기 때문이다. 이를 해결하고자 객체의 변경이 필요할 경우 참조가 아닌 객체의 방어적 복사(defensive copy)를 통해 새로운 객체를 생성한 후 변경하도록 한다.

public Period(Date start, Date end) {
    validCheck(start, end);
    this.start = start;
    this.end = end;
}

Date의 경우 mutable 객체이므로 생성된 이후 값이 변경될 수 있다. 위의 코드의 경우 validCheck 함수 내에서 start와 end의 값이 변하게 될 경우 원본 start와 end 값을 알 수 없게 된다. 멀티쓰레딩 환경일 경우 이는 특히 더 중요한 문제점이 된다. 따라서 validCheck 함수를 수행하기 전 방어적 복사본을 만들고, 이 복사본으로 validCheck 함수를 수행하도록 코드를 변경한다. 변경한 코드는 아래와 같다.

public Period(Date start, Date end) {
    this.start = new Date(start.getTime());
    this.end = new Date(end.getTime());
    validCheck(start, end);
}

또한, 방어적 복사를 통해 객체의 복사본을 만들었어도 내부 요소들이 mutable하다면 해당 객체는 immutable할 수 없다.

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를 파라미터로 받는 생성자를 생성하고 있는 코드이다.

import java.util.ArrayList;
import java.util.List;

public class Application {
    public static void main(String[] args) {
        Name crew1 = new Name("Fafi");
        Name crew2 = new Name("Kevin");
        
        List<Name> originalNames = new ArrayList<>();
        originalNames.add(crew1);
        originalNames.add(crew2);

        Names crewNames = new Names(originalNames); // crewNames의 names: Fafi, Kevin

        crew2.setName("Sally"); // crewNames의 names: Fafi, Sally
    }
}

방어적 복사를 통해 originalNames와 Names 객체 내부의 names는 각기 다른 객체가 된다. 이때 마지막 줄에서 originalNames의 내부 요소인 crew2의 name 인스턴스 변수를 수정한다. crew1과 crew2는 Names 객체와 달리 mutable하다. 그렇다면 원본 객체인 originalNames는 여전히 immutable한가?
아니다. 원본 객체의 crew2가 변경되어 방어적 복사를 통해 생성된 객체의 crew2도 변경되어 immutable이었던 Names 객체는 mutable한 내부 요소들로 인해 mutable 객체로 변했다. 따라서 객체를 immutable로 만들기 위해서는 방어적 복사 외에도 객체 내부의 요소들까지 모두 immutable로 만들어주어야 한다.

Setter 메소드 제거

mutable 객체와 immutable 객체의 가장 큰 차이는 생성된 이후 수정이 가능한가, 가능하지 않은가에 있다. 따라서 mutable 객체를 immutable하게 사용하려면 생성된 이후 변경이 불가능하게 만들어주면 된다.
class 객체 생성 이후 값을 변경하는 역할은 setter 메소드가 맡고 있으므로 이를 제거하여 변경을 막는다. 마치 '읽기 전용'으로 파일을 여는 것과 비슷하다.

그 외

  • final 사용할 것
    : final class는 상속이 불가하며 final 메소드는 오버라이딩이 불가하다. 원치않는 class와 메소드 내의 수정을 막을 수 있다.
  • 모든 클래스 변수를 private과 final로 선언할 것
    : setter 메소드가 없을 경우 외부에서 접근할 수 없어 변경이 불가해진다. 변수들을 private으로 선언해두고 class 내부의 메소드만으로 변수를 접근 가능하게 하는 정보 은닉(Data Hiding)'을 떠올리면 이해가 쉽다.

[참조] https://www.educba.com/mutable-vs-immutable-java/
[참조] https://velog.io/@max9106/Java-%EB%B0%A9%EC%96%B4%EC%A0%81-%EB%B3%B5%EC%82%ACDefensive-copy
[참조] https://poiemaweb.com/js-immutability
[참조] https://tecoble.techcourse.co.kr/post/2021-04-26-defensive-copy-vs-unmodifiable/

profile
피드백은 언제나 환영입니다! :)

0개의 댓글