Immutable Class (불변 클래스)

de_sj_awa·2021년 5월 10일
0

1. Immutable란?

Immutable을 사전적으로 찾아보면, 불변의, 변경할 수 없는 이라는 뜻임을 알 수 있다. 사전적인 의미에서도 알 수 있듯이 Immutable은 변경이 불가하다. 즉 Immutable Class는 변경이 불가능한 클래스이며, 가변적이지 않는 클래스이다. 만들어진 Immutable Class는 레퍼런스 타입의 객체이기 때문에 heap 영역이 생성된다.

자바에서 이런 Immutable Class로 어떤 것들이 있을까?
대표적으로 String, Boolean, Integer, Float, Long 등이 있다. 이러한 Immutable Class들은 heap 영역에서 변경 불가능한 것이지 재할당을 못하는 것은 아니다. 즉, String a = "aa"; a = "bb"로 재할당이 가능하다. a가 참조하고 있는 heap 영역의 객체가 바뀌는 것이지 heap 영역에 있는 값이 바뀌는 것이 아니다.

좀 더 풀어말하면, a가 처음에 참조하고 있는 "aa"값이 "bb"로 변경되는 것이 아니라 아예 "bb"라는 새로운 객체를 만들고 그 객체를 a가 참조하게 하는 것이다. 이렇게 했을 경우 "aa"값을 지니고 있는 객체는 이제 그 누구도참조하고 있지 않는 객체가 되며 gc 대상이 된다.

2. Immutable의 특징

장점

  • 생성자, 접근메서드에 대한 방어 복사가 필요없다.
  • 멀티쓰레드 환경에서 동기화 처리없이 객체를 공유할 수 있다. (Thread-safe) 불변이기 때문에 객체가 안전한다.

단점

  • 객체가 가지는 값마다 새로운 객체가 필요하다. 따라서 메모리 누수와 새로운 객체를 계속 생성해야 하기 때문에 성능저하를 발생시킬 수 있다.

3. 대표적인 Immutable인 String

자바에서 대표적인 Immutable Class 중 String을 보자.
String 클래스에서 기본적으로 제공해주는 concat 메소드는 문자열을 더해주는 역할을 한다.

String a = "Star";
a = a.concat("Craft");
System.out.println(a);

출력결과는 당연히 StarCraft이다. 여기서 a라는 String 객체는 처음 Star를 할당 받을 때에 참조된 메모리를 참조하고 있지 않고, concat 메소드를 통해 Craft 문자열을 붙인 StarCraft를 참조하는 메모리를 참조한다.

즉, 값을 변화시킨게 아니라 String 내부적으로는 아예 기존의 Star를 참조하는 객체는 그대로 두고, 새롭게 StarCraft String 객체를 만든 것이다.

 public String concat(String str) {
        if (str.isEmpty()) {
            return this;
        }
        return StringConcatHelper.simpleConcat(this, str);
    }
 
  @ForceInline
    static String simpleConcat(Object first, Object second) {
        String s1 = stringOf(first);
        String s2 = stringOf(second);
        if (s1.isEmpty()) {
            // newly created string required, see JLS 15.18.1
            return new String(s2);
        }
        if (s2.isEmpty()) {
            // newly created string required, see JLS 15.18.1
            return new String(s1);
        }
        // start "mixing" in length and coder or arguments, order is not
        // important
        long indexCoder = mix(initialCoder(), s1);
        indexCoder = mix(indexCoder, s2);
        byte[] buf = newArray(indexCoder);
        // prepend each argument in reverse order, since we prepending
        // from the end of the byte array
        indexCoder = prepend(indexCoder, buf, s2);
        indexCoder = prepend(indexCoder, buf, s1);
        return newString(buf, indexCoder);
    }

3. Immutable Class를 만드는 방법

  1. 멤버변수를 final로 선언
  2. 접근 메서드를 구현하지 않는다. (Setter 메서드)
public class ImmutableString {
     
    private final String name;
     
    ImmutableString(String name){
        this.name = name;
    }
     
    @Override
    public String toString(){
        return this.name;
    }
}
String name = "Amazing";
ImmutableString immutableString = new ImmutableString(name);
name.concat("Day");
System.out.println(immutableString);

이 메소드를 실행하면 Amazing이 출력된다. String name을 만들고 Amazing을 할당한다. 다음 이 name 객체를 참조하게끔 ImmutableString 생성자에 인자로 주입한다. concat 메소드를 통해 Day 문자열이 합쳐진 AmazingDay는 새로운 객체로 만들어져서 다시 name에게 할당된다.

우리가 만든 ImmutableString은 멤버변수가 final로 선언되었기 때문에 수정이 불가능하고, 또 setter 접근 메서드가 없기 때문에 기본적으로 변경시킬 수 있는 모든 상황을 막았다. Immutable Class 불변 규칙을 지키는 셈이다.

String vs StringBuilder

StringBuilder는 String과 달리 mutable하기 때문에 할당된 값을 변경하더라도, 새로운 객체를 만드는 방식이 아닌 기존 할당된 값을 수정하는 것으로 처리한다. 즉, 문자열 변경과 연산을 하는 경우 기존의 버퍼 크기를 늘리거나 줄이면서 유연하게 동작하는 셈이다.

위의 Immutable Class를 만든 예제에서 String 타입을 StringBuilder 타입으로 수정해보자.

public class ImmutableStringBuilder {
	
    private final StringBuilder name;
    
    ImmutableStringBuilder(StringBuilder name){
    	this.name = name;
    }
    
    @Override
    public String toString(){
    	return this.name.toString();
    }
}

출력 결과 Amazing이 아닌 AmazingDay가 출력됨을 알 수 있다.

StringBuilder name = new StringBuilder("Amazing");
ImmutableStringBuilder immutableString = new ImmutableStringBuilder(name);
name.append("Day");
System.out.println(immutableString);

StringBuilder는 불변이 아닌 가변적인 객체이기 때문에 append 메서드를 통해 문자열을 합치고 자기자신을 리턴한다. String의 append 메서드를 들여다보면 다음과 같이 자기자신(this)을 리턴한다.

    public AbstractStringBuilder append(String str) {
        if (str == null) {
            return appendNull();
        }
        int len = str.length();
        ensureCapacityInternal(count + len);
        putStringAt(count, str);
        count += len;
        return this;
    }

그럼 인위적으로 만든 Immutable Class 안에서 mutable한 멤버변수 타입일 경우 Immutable Class를 만들 수 없을까? 아니다. 방법을 생각해보면 ImmutableStringBuilder 생성자를 조금 수정하면 된다. This.name = new StringBuilder(name);으로 새로 객체를 생성해 필드값을 초기화해주면, 다시 결과는 Amazing으로 나온다.

하지만 알아둘 것은 JDK 1.5 버전 이전에는 String과 같은 Immutable한 타입에 많은 연산과 잘못된 사용으로 인한 성능이슈가 많았다. 따라서 JDK 1.5 버전 이후에는 컴파일 단계에서 String 객체를 선언했어도, StringBuilder로 치환시켜 컴파일하도록 변경되었다. 결국 JDK 1.5 버전 이후에는 String과 StringBuilder의 성능적인 차이가 거의 없어졌다고 볼 수 있다.

참고

profile
이것저것 관심많은 개발자.

0개의 댓글