자바 Immutable 이란

Immutable이란..

Immutable이란 생성후 변경 불가한 객체입니다. 그래서 immutable에는 set 메쏘드가 없습니다. 멤버 변수를 변경할 수 없습니다. return type이 void인 메쏘드도 없습니다. 주로 void 메쏘드는 뭔가를 하고(하지 않을 수도 있고.. ) 멤버변수를 변경하는 역할을 하는 것이기 때문에 쓸 일이 거의 없습니다. (물론, 콘솔에 뭔가를 찍는 것과 같은 예외적인 void는 있을 수 있습니다.) Immutable을 쓰면, 멀티 쓰레드 환경에서 좀 더 신뢰할 수 있는 코드를 만들어 내기가 쉽습니다. 멀티 쓰레드 프로그램을 짜보셨다면 아시겠지만, 멀테 쓰레드 환경에서는 에러보다 비정상적 작동의 경우가 많습니다. 에러도 아니기 때문에 찾아내기도 어렵습니다. 게다가 항상 생기는 것도 아니고 백번에 한번 천번에 한번 식으로 문제가 생겨 정말 머리 아픈 경우가 한 두번이 아닙니다. Immutable을 쓰게 되면, 이런 요소들을 많이 줄일 수 있습니다.

대표적인 Immutable 클래스

String, Boolean, Integer, Float, Long 등등이 있습니다. 여기서 주의할 점은 변경불가라는 것은 heap 영역에서의 변경불가라는 뜻입니다. String a="a"; a="b"; 와 같이 재할당은 가능합니다. 이는 a가 reference하고 있는 heap 영역의 객체가 바뀌는 것이지 heap영역에 있는 값이 바뀌는 것이 아닙니다.

String vs StringBuffer

String과 StringBuffer에는 비슷한 메쏘드들이 많이 있어서 비슷해 보이지만, 결정적인 차이가 있습니다. String은 Immutable 입니다. StringBuffer는 아닙니다. StringBuffer가 String에 비해서 훨씬 빠르다는 얘기를 들어보셨나요? 그건 객체를 새로 생성할 필요가 없기 때문입니다.
String에는 없고 StringBuffer에만 있는 대표적인 메쏘드는 append, delete 등일 겁니다. 멤버 변수를 변화시켜 값을 바꿀 수 있는 거죠. 그런데 잘 보며 이들의 리턴은 StringBuffer 타입입니다. 어차피 얘네도 객체를 새로 만들어낸다면, String과 별 차이가 없어보입니다. 그러나 객체를 새로 만들어 내는 것이 아닙니다. 아래 코드를 실행시켜보세요.
StringBuffer b = new StringBuffer();
StringBuffer a = b.append("test");
System.out.println(a == b);
true가 나옵니다. 객체를 새로 만드는 것이 아니라 return this 로 되어있습니다. 굳이 리턴을 하는 이유는 아래와 같은 코딩을 가능하게 해주려는 것 같습니다.
StringBuffer test = new StringBuffer();
test.append("a").append("b").append("c").append("d");
와 같은 식으로 여러줄 코딩을 한 줄로 할 수 있게 해주려는 것 같습니다.. 아님 말구요.-_-;

Immutable의 유용성과 위험성

멀티쓰레드 환경에서 하나의 객체에 접근을 하는데 각각의 쓰레드끼리는 영향을 받으면 안 되는 경우가 있습니다. 그럴 때 한 번 만들어진 객체의 값이 변하지 않는다는 게 보장이 되면 편하겠죠.
String a  = "";
while(어떤 조건문){
    a += "머시기";
    if(딴 조건문){
        break;
    }
}
위와 같은 코드는 쓰면 안 됩니다. a+= "머시기" 구문이 문젭니다. 객체를 계속 생성해 냅니다. Immutable은 값이 계속 변경될 수 있는 곳에 쓰면 메모리 왕창 잡아먹습니다. 값이 완전히 정리된 후에 한 큐에 immutable로 만들어 내셔야 합니다. (물론, 가비지 컬레션이 돌면서 정리를 하기 때문에 치명적으로 위험한 수준의 행동은 아닙니다.)
java.util.Collections 에 unmodifiable머시기 하는 메쏘드들이 있습니다. 얘네들을 이용하면, Set, List, Map 등을 immutable로 사용할 수 있습니다. 다만, add, put과 같은 메쏘드를 호출할 경우에는 UnsupportedOperationException 가 발생합니다. 예외 상황을 고려해야 합니다.

Immutable은 보통 final 클래스로 정의합니다.

처음에 자바를 할 때 String이란 객체에 ltrim이란 메쏘드를 추가하고 싶었습니다. (왼쪽만 trim하는 메쏘듭니다.) 상속을 받아 새로운 클래스를 만들어 해결하려고 했습니다. 헛, 그런데 final로 정의가 되어있어서 상속을 받을 수가 없더군요. final로 정의가 되지 않으면, 상속을 받은 클래스가 Immutable을 깨버릴 수가 있기 때문입니다.

잘못된 Immutable의 구현

package immutable;

import java.text.SimpleDateFormat;
import java.util.Date;

public final class WrongImmutable {
    private final Date date;
    private final SimpleDateFormat dateFormat;
    public WrongImmutable(Date date){
        this.date = date;
        dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    }
    public String getMessage(){
        return dateFormat.format(date);
    }
}
위의 소스를 보세요. date라는 변수가 final로 정의 되어있으며, setter가 없어 오직 생성자에서만 값을 지정할 수 있는 것 같아보입니다. 그러나 아래와 같은 테스트 클래스를 실행시켜보시면, 위의 코드가 잘못되었음을 알 수 있습니다.
package immutable;

import java.util.Date;

public class WrongImmutableTest {
    public static void main(String[] args) {
        Date testDate = new Date();
        WrongImmutable wrongImmutable = new WrongImmutable(testDate);
        testDate.setTime(testDate.getTime() + 10000000);
        System.out.println(wrongImmutable.getMessage());
    }
}
WrongImmutable의 생성자에 인자로 넣은 Date를 외부에서 값을 변경시키면 WrongImmutable의 멤버 변수의 값이 변경이 되고 맙니다. Immutable에서는 멤버 변수가 외부로 공개되어 변경이 가능하면 안 됩니다. 그럼 처음에 원했던 대로 정상적인 Immutable이 되도록 하려면 인자로 받은 Date 객체를 그대로 사용하는 것이 아니라, 어떤 식으로든 복사를 해서 써야 합니다. 생성자에서 멤버 변수의 값을 할당하는 부분을
this.date = new Date(date.getTime()); 와 같이 바꿔야 합니다.

자바에 진짜 Immutable이란 건 없다!

java의 reflection을 이용하면 변경 가능합니다. 다음 코드를 실행시켜 보세요.
import java.lang.reflect.Field;

public class EditUsingReflection {
    public static void main(String[] args) throws IllegalArgumentException, SecurityException, IllegalAccessException, NoSuchFieldException {
        String s = "string!";
        edit(s);
        System.out.println(s);
    }
    public static void edit(String s) throws IllegalArgumentException, IllegalAccessException, SecurityException, NoSuchFieldException {
        Field stringValue = String.class.getDeclaredField("value");
        stringValue.setAccessible(true);
        stringValue.set(s, s.toUpperCase().toCharArray());
    }
}
String 객체는 내부적으로 value라는 이름의 char[]로 그 값을 관리합니다. 물론 private으로 선언되어있지요. reflection을 이용하면 멤버변수에 걸려있는 private을 무시하고 접근해 버릴 수가 있습니다. 결국 reflection을 이용하면 모든 멤버변수에 대한 수정이 가능합니다. Immutable이란 건 reflection을 제외시켜 놓고 생각할 때만 가능합니다.

댓글

가장 많이 본 글