fragile and resilient

카테고리 없음

[Java] 불변 객체(Immutable Object)란 무엇일까?

Green Lawn 2022. 2. 15. 14:53

불변 객체란 무엇일까요 ?

불변 객체란 객체가 생성된 이후에 상태를 바꿀 수 없는 객체를 말합니다.
즉, 힙 영역에서 객체가 가리키고 있는 데이터 자체의 변화가 불가능한 것을 말합니다.
그럼 불변 객체 설명에 앞서, 이해를 돕기 위해 간단히 자바 메모리 구조의 힙 영역에 대해 알아보겠습니다.

사진 출처: JVM 구조
자바 메모리 영역(Runtime Data Area)은 크게 메소드 영역, 힙 영역, 스택 영역으로 나뉘게 됩니다.

Method
우선 메소드 영역은 클래스 로더에서 클래스들을 읽어 클래스 별로 런타임 상수풀, 필드, 메소드, 생성자 등을 분류해서 저장하게 됩니다.
메소드 영역은 JVM(Java Virtual Machine)이 시작할 때 생성되고, 모든 Thread가 공유하는 영역입니다.

Heap Area
힙 영역은 객체와 배열이 생성되는 영역입니다. 힙 영역에 있는 객체와 배열은 스택 영역의 변수나 다른 객체의 필드에서 참조하게 됩니다.
즉, 객체는 힙에 저장되어 있고, 스택에서 해당 값의 주소를 참조하고 있는 구조입니다. 여기서 참조하는 변수가 오랫동안 사용되지 않는다면, GC의 타깃이 되어 자동으로 제거되게 됩니다.

Stack Area
스택은 위에서 언급했듯이 Heap 영역에서 생성된 객체 데이터의 참조 값이 할당됩니다.
스택 영역은 각 스레드마다 하나씩 존재(스레드는 독립적으로 실행되어야 하기 때문에, 스택 영역을 각각 할당받고 있다.)하며 스레드가 시작될 때 할당되게 됩니다.
더 자세히 알고 싶으신 분은 ‘JVM 구조’ 또는 ‘Runtime Data Area’키워드를 검색하시면 좋을 것 같습니다.


다시 돌아와서, 불변 객체는 힙 영역에서 해당 객체가 가리키고 있는 데이터 자체의 변화가 불가능한 것을 말합니다.
두가지 예시로 불변 객체를 생성하는 방법에 대해 알아 봅시다.

1. 원시 타입만 있는 경우

public class Name {
    private String name;

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

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}
Name name = new Name("green");
System.out.println(name.getName());    //green

name.setName("woowa");
System.out.println(name.getName());    //woowa

위 결과에서 알 수 있듯이 Name은 불변 객체가 아닙니다.
그럼 불변 객체로 만들려면 어떻게 변경해야 할까요?

public class Name {
    private final String name;

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

    public String getName() {
        return name;
    }
}

위의 객체는 불변 객체입니다.
final 키워드를 사용해서 setter가 불가능하게 되었기 때문에 값을 변경할 방법이 없습니다.

2. List인 경우

step1.

public class Persons {
    private final List<Person> persons;

    public Persons(List<Person> persons) {
        this.persons = persons;
    }

    public List<Person> getPersons() {
        return persons;
    }
}

위의 코드를 보고, final을 사용하고 setter가 없기 때문에 외부에서 조작할 수 없는 불변 객체라고 생각할 수 있지만,

public static void main(String[] args) {
    List personNames = new ArrayList<>();
    personNames.add(new Person("green"));
    Persons persons = new Persons(personNames);

    for (Person person : persons.getPersons()) {
        System.out.println(person.getName());      //green
    }

    personNames.add(new Person("lawn"));

    for (Person person : persons.getPersons()) {
        System.out.println(person.getName());       //green
                                                    //lawn
    }
}

코드를 확인해 보면, Persons는 불변 객체가 아닌 것을 확인할 수 있습니다.


step2.

따라서 위의 문제를 해결하기 위해, Persons 생성자에서 new ArrayList로 새로운 변수를 참조하도록 했습니다.
이러면 외부에서 넣어 준 리스트(personNames)와 내부에서의 참조 변수가 다르기 때문에 외부에서 조작이 불가능하게 됩니다.

public class Persons {
    private final List persons;

    public Persons(List persons) {
        this.persons = **new ArrayList<>(persons);**
    }

    public List getPersons() {
        return persons;
    }
}

그럼 이제 해결된 것일까요?
View에서 값을 사용하기 위해 getter 메서드는 거의 존재할 수밖에 없는데요.
그래서 getPersons을 하게 된다면?

public static void main(String[] args) {
    List personNames = new ArrayList<>();
    personNames.add(new Person("green"));
    Persons persons = new Persons(personNames);

    for (Person person : persons.getPersons()) {
        System.out.println(person.getName());      //green
    }

    persons.getPersons().add(new Person("lawn"));

    for (Person person : persons.getPersons()) {
        System.out.println(person.getName());       //green
                                                    //lawn
    }
}

외부에서 조작할 수 있는 것을 확인할 수 있습니다.

step3.

그럼 어떻게 해결할 수 있을까요?
getPersons에서 리턴하는 Person 리스트를 외부에서 조작하지 못하도록 하면 좋을 것 같네요.
이는 Collections의 unmodifiableList를 활용하면 됩니다.

public class Persons {
    private final List persons;

    public Persons(List persons) {
        this.persons = new ArrayList<>(persons);
    }

    public List getPersons() {
        return **Collections.unmodifiableList**(persons);
    }
}

Collections.unmodifiableList() 는 read-only로만 사용할 수 있으며, remove나 add 명령어등을 사용하면 UnsupportedOperationException이 발생하게 됩니다.

public static void main(String[] args) {
    List personNames = new ArrayList<>();
    personNames.add(new Person("green"));
    Persons persons = new Persons(personNames);

    for (Person person : persons.getPersons()) {
        System.out.println(person.getName());      //green
    }

    persons.getPersons().remove(0);    //Exception
    persons.getPersons().add(new Person("lawn"));    //Exception
}

그럼 불변 객체는 왜 사용하는 것일까요?

1.Side Effect를 줄일 수 있다.
side effect란 상태나 값을 변화하는 행위를 의미합니다.
이러한 변화는 오류를 일으킬 수 있는데, 불변 객체는 값의 수정이 불가능하기 때문에 변경 가능성이 적어 side effect를 줄일 수 있습니다.
2.Thread-safe하다.
값을 수정하지 못하기 때문에 동기화 문제를 고려하지 않아도 됩니다.
즉, 애플리케이션의 안전성을 높여주는 방법입니다.


References

Tecoble-불변객체를 만드는 방법
https://velog.io/@kyle/불변-객체란-Java-Immutable-Object
https://velog.io/@conatuseus/Java-Immutable-Object불변객체
https://mangkyu.tistory.com/131
https://jeong-pro.tistory.com/148https://jeong-pro.tistory.com/148