article thumbnail image
Published 2017. 11. 8. 01:13


Immutable이란?

Immutable이란 생성후 변경 불가능한 객체를 의미한다. 그래서 Immutable에는 set 메소드가 존재하지 않고, 멤버 변수를 변경할 수 없다. return type이 void인 메소드도 없다. 주로 void 메소드는 뭔가를 하고(하지 않을 수도 있다.) 멤버 변수를 변경하는 역할을 하는 것이기 때문에 쓸 일은 많지 않다. Immutable을 쓰면 멀티 스레드 환경에서 좀 더 신뢰할 수 있는 코드를 만들어 내기가 쉽다. 멀티 스레드 프로그램을 짜봤다면 멀티 스레드 환경에서는 에러보다 비정상적으로 작동하는 경우가 많다. 에러도 아니기 때문에 찾아내기도 어려운 편. 게다가 항상 발생하는 이슈가 아니라 100번에 한번 1000번에 한번 꼴로 문제가 생기는 경우가 많아 정말 머리 아프게 하는 경우가 많다. 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과 별 차이가 없어보인다. 그러나 객체를 새로 만들어 내는 것이 아니다.  아래의 코드를 참고하라.


1
2
3
StringBuffer b = new StringBuffer();
StringBuffer a = b.append("test");
System.out.println(a == b);
cs

위의 코드를 실행하게 나오면 결과 값이 true가 나온다. 객체를 새로 만드는 것이 아니라 return this로 되어있다. 굳이 리턴을 하는 이유는 아래와 같은 코딩을 가능하게 해주기 때문이다.

1
2
StringBuffer test = new StringBuffer();
test.append("a").append("b").append("c").append("d");
cs


위와 같은 방식으로 여러줄의 코딩을 한 줄로 할 수 있게 된다.


Immutable의 유용성과 위험성

멀티 스레드 환경에서 하나의 객체에 접근을 하는데 각각의 스레드끼리는 영향을 받으면 안 되는 경우가 있다. 그럴때 한 번 만들어진 객체의 값이 변하지 않는다는게 보장되면 굉장히 편해진다.


1
2
3
4
5
6
7
String a  = "";
while(어떤 조건문){
    a += "머시기";
    if(딴 조건문){
        break;
    }
}
cs


위와 같은 코드는 쓰면 안된다. a += "머시기" 구문이 객체를 계속 생성해낸다. Immutable은 값이 계속 변경될 수 있는 곳에 쓰면 메모리를 엄청나게 잡아먹는다. 값이 완전히 정리된 후 한 큐에 Immutable로 만들어 내야 한다. (물론, 가비지 컬렉션이 돌면서 정리를 하기 때문에 치명적인 위험 요소는 아니다.)


java.util.Collections에 unmodifiable로 시작하는 메소드들이 있다. 애네들을 이용하면 Set, List, Map등을 Immutable로 사용할 수 있다. 단, add, put과 같은 메소드를 호출할 경우에는 UnsupportedOperationException이 발생한다. 예외 상황을 고려하면서 코딩해야한다.


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

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


잘못된 Immutable의 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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);
    }
}
cs


위의 코드를 보면, date라는 변수가 final로 정의 되어있다. setter가 없어 오직 생성자에서만 값을 지정할 수 있는 것 같아 보인다. 그러나 아래와 같은 테스트 클래스를 실행시켜보면, 위의 코드가 잘못되었음을 알 수 잇다.


1
2
3
4
5
6
7
8
9
10
11
12
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());
    }
}
cs


WrongImmutable의 생성자에 인자로 넣은 Date를 외부에서 값을 변경시키면 WrongImmutable의 멤버 변수의 값이 변경 되고 만다. Immutable에서는 멤버 변수가 외부로 공개되어 변경이 가능하면 안된다. 그럼 처음에 원했던 대로 정상적인 Immutable이 되도록 하려면 인자로 받은 Date 객체를 그대로 사용하는 것이 아니라, 어떤 식으로든 복사를 해서 써야 한다. 생성자에서 멤버 변수의 값을 할당하는 부분을 


1
this.date = new Date(date.getTime());
cs

와 같이 바꿔야 한다.


Java에서 진짜 Immutable이란 건 없다!

Java의 reflection을 이용하면 변경이 가능하다. 다음 코드를 실행시켜 보면 알 수 있다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
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());
    }
}
cs


String 객체는 내부적으로 value라는 이름의 char[]로 그 값을 관리한다. 물론 private로 선언되어있다. reflection을 이용하면 멤버 변수에 걸려있는 private를 무시하고 접근할 수 있다. 결국 reflection을 이용하면 모든 멤버 변수에 대한 수정이 가능하다. Immutable이란 건 reflection을 제외시켜 놓고 생각할 때만 가능하다.


참고 블로그


복사했습니다!