티스토리 뷰

java

[JAVA] immutable object (불변객체)

sehyeona 2022. 3. 1. 17:25

최근 Thread 를 이용해서 비동기로 처리해야하는 작업이 생기면서 비동기시 사용하는 메서드의 객체들을 어떻게 다룰 수 있을까에 대해서 고민해야 되는 상황이 생겼습니다. 그동안 추상적으로만 알고 있던 immutable object 불변객체에 대해서 정리해 보았습니다.


1. 불변객체란 

객체 지향 프로그래밍에 있어서 불변객체(immutable object)는 생성 후 그 상태를 바꿀 수 없는 객체를 말한다. 반대 개념으로는 가변(mutable) 객체로 생성 후에도 상태를 변경할 수 있다. 객체 전체가 불변인 것도 있고, C++에서 const 데이터 멤버를 사용하는 경우와 같이 일부 속성만 불변인 것도 있다. 또, 경우에 따라서는 내부에서 사용하는 속성이 변화해도 외부에서 그 객체의 상태가 변하지 않은 것 처럼 보인다면 불변 객체로 보기도 한다. 예를 들어, 비용이 큰 계산의 결과를 캐시하기 위해 메모이제이션(Memoization)을 이용하더라도 그 객체는 여전히 불변하다고 볼 수있다. 불변 객체의 초기 상태는 대개 생성 시에 결정되지만 객체가 실제로 사용되는 순간까지 늦추기도 한다.
불변 객체를 사용하면 복제나 비교를 위한 조작을 단순화 할 수 있고, 성능 개선에도 도움을 준다. 하지만 객체가 변경 가능한 데이터를 많이 가지고 있는 경우엔 불변이 오히려 부적절한 경우가 있다. 이 때문에 많은 프로그래밍 언어에서는 불변이나 가변 중 하나를 선택할 수 있도록 하고 있다.

위키 백과에서는 불변객체를 다음과 같이 정의하고 있습니다. 

java 에서는 불변객체를 다음과 같이 정의할 수 있을것 같습니다. 

불변객체에 다른 참조값을 재할당은 가능하지만 불변객체는 한번 할당하면 내부 데이터를 변경할 수 없다. 


위키 백과의 정의를 확인해보면 "경우에 따라서는 내부에서 사용하는 속성이 변해도 외부에서 그 객체의 상태가 변하지 않은 것처럼 보인다면 불변객체로 보기도 한다." 라는 부분이 있습니다. 이 부분은 java 에서의 hash 와 equals 를 이용하는 경우와 비슷하다고 생각이 들지만, 프로그래머마다 equals 를 구현하는 방식에 따라서 그 결과가 크게 달라질 수 있기때문에, 위와 같은 경우는 불변객체가 아니라고 판단했습니다. 

 

 즉 불변객체란 값을 한번 할당하면 내부 데이터를 변경할 수 없는 객체를 의미합니다. 대표적으로 wrapper class 들에 해당되는 String, Integer, Boolean 같은 객체들이 불변객체에 해당됩니다. String str = "a" 로 초기화 하고 str = "b" 로 바꾸는 것은 str 객체의 내부 값이 변하는 것이 아닌 str 객체에 "b" 라는 primitive 값을 새로 할당하는 것입니다. 
 

 반면 내부 데이터가 얼마든지 변할 수 있는 가변객체에는 대표적으로 Collection Framework 에 속하는 List, Set, Map 등이 있습니다. 

또한 아래와 같이 setter 를 이용하여 내부 값을 바꿀 수 있도록 허용한 객체들도 가변객체로 분류됩니다. 

class Member {
    private final Long id;
    private String name;
    
    public Member(Long id, String name) {
    	this.id = id;
        this.name = name;
    }
    
    public void setName(String name) {
    	this.name = name;
    }
}

 위의 Member 클래스는 id 값은 변경할 수 없지만 name 은 언제든지 변경할 수 있기때문에 가변객체에 해당됩니다. 

그렇다면 이 Member 클래스를 불변객체로 바꿔보겠습니다. 

 

class Member {
    private final Long id;
    private final String name;
    
    public Member(Long id, String name) {
    	this.id = id;
        this.name = name;
    }
}

새롭게 정의한 Member 클래스는 생성자를 제외하고는 id 와 name 을 변경할 수 있는 방법이 없습니다. 

따라서 위와 같이 정의한 객체는 불변객체가 됩니다.


2. 불변객체의 장단점

장점

  • 객체가 생성된 순간부터 같은 참조값을 가지는 객체는 transaction 주기 안에서 항상 같은 값을 가지는 것이 보장됩니다. 따라서 객체에 대한 신뢰도가 높아집니다.
  • 생성자, getter 에 대한 방어 복사가 필요없습니다.
  • 멀티스레드 환경에서 복잡한 동기화 처리없이 객체를 공유할 수 있습니다.

단점

  • 객체의 값이 변할때마다 새로운 객체를 생성하고 할당해야 합니다. 혹은 같은 값을 가지는 객체가 어플리케이션 안에 중복되어 존재할 수도 있습니다. 이런 중복되는 데이터는 메모리의 비효율적인 사용과 새로운 객체의 생성은 성능저하를 야기할 수 있습니다.

3. 불변객체 만들기

불변객체를 만드는 기본적인 아이디어는 모든 필드에 final 을 추가하고 setter 와 같이 객체 내부의 값을 변경할 수 있는 메서드를 구현하지 않는 것입니다. 

이 방법은 불변객체의 필드가 모두 primitive 타입인 경우에만 가능하고, reference 타입의 경우에는 추가작업이 필요합니다. 

필드가 모두 primitive 인 경우, reference 타입이 섞여있는 경우에 대해서 불변객체를 만들어 보겠습니다.

 

primitive 타입만 있는경우

변경전

class Person {

    private int age;
    
    public Person(int age) {
    	this.age = age;
    }
    
    public void setAge(int age) {
    	this.age = age;
    }
}

위 객체는 setter 를 이용해 내부데이터인 age 를 변경할 수 있기때문에 불변객체가 아닙니다. 또한 생성자 안에서 어떤 로직이 실행되고 있는지 모르다면 생성자의 파라미터로 넘겨준 age 값이 똑같이 적용된다고 확신할 수 없습니다.

 

변경후

class Person {

    private final int age;
    
    public Person(final int age) {
    	this.age = age;
    }
}

위의 객체처럼 setter 를 없애고, 생성자의 paramter 와 필드에 final 키워드를 추가해 불변객체로 만들 수 있습니다. 이 객체의 내부데이터를 변경해서 사용하고 싶다면 객체를 새로 생성하는 방법밖에 없습니다. 

 

reference 타입이 있는경우

참조 타입이 있는 경우는 참조타입까지 불변성을 만족해야합니다. 단순히 객체에서 setter 와 final 을 추가한다고 해서 불변객체를 생성할 수 없습니다.
아래 예를 보겠습니다.

public class Name {
    private String firstName;
    private String lastName;
    
    public Name(String firstName, String lastName) {
    	this.firstName = firstName;
        this.lastName = lastName;
    }
    
    public void setFirstName(String firstName) {
    	this.firstName = firstName;
    }
}


class Person {

    private final int age;
    private final Name name;
    
    public Person(final int age, final Name name) {
    	this.age = age;
        this.name = name;
    }
    
    public Name getName() {
    	return this.name;
    }
}

Person 클래스는 필드와 생성자에 final 을 사용하고 setter 를 구현하지 않았지만 불변객체가 될 수 없습니다. 

Name 클래스의 내부 데이터를 다음과 같이 변경할 수 있기때문입니다. 

public static void main(String[] args) {
    Name name = new Name("killdong", "hong")
    Person person = new Person(16, name);
    
    System.out.println(person.getName().getFirstName());
    // Output: killdong
	
    person.getName().setFristName("kill ddong")
    System.out.println(person.getName().getFirstName());
    // Output: kill ddong
}

즉 reference 타입의 내부 데이터를 가지고 있는 객체를 불변객체로 만들기 위해서는, 모든 내부 reference 타입을 불변객체로 만들어야합니다.

필드에 reference 타입이 있는 경우중에 대표적인 예시 3가지를 확인해보겠습니다. 

(1) 객체를 필드로 가지는 경우

(2) 배열을 필드로 가지는 경우

(3) Collections Framework 의 객체들을 필드로 가지는 경우 (List, Set 등등)

 

(1) 일반 객체를 필드로 가지는 경우

위의 예시를 수정해서 Person 객체를 불변객체로 만들어보겠습니다.

Person 의 필드인 Name 객체도 불변타입으로 만들어 주어야 합니다. 

public class Name {
    private final String firstName;
    private final String lastName;
    
    public Name(final String firstName, final String lastName) {
    	this.firstName = firstName;
        this.lastName = lastName;
    }
}

class Person {
    private final int age;
    private final Name name;
    
    public Person(final int age, final Name name) {
    	this.age = age;
        this.name = name;
    }
    
    public Name getName() {
    	return this.name;
    }
}

Name 의 필드인 firstName, secondName 은 불변객체인 String 타입이기 때문에 Name 은 불변객체가 됩니다. 모든 필드가 불변객체이며, 생성 이후에 필드값을 변경할 수 없는 Person 객체도 불변객체라고 할 수 있습니다.

 

(2) 배열을 필드로 가지는 경우

public class ImmutableArray {
    private final int[] array;

    public ArrayObject(final int[] array) {
        this.array = Arrays.copyOf(array, array.length);
    }

    public int[] getArray() {
        return (array == null) ? null : array.clone();
    }
}

배열인 경우에는 생성자에서 배열을 받아 copy 해서 저장하도록 하고, getter 로 반환할때는 clone()을 이용하여 반환합니다. 

생성자에서 copy 함으로서 생성자로 넘겨준 배열이 다른곳에서 수정되었을때 내부데이터가 변하는 것을 방지하고, getter 로 반환할때 clone을 사용해서 반환한 배열이 수정되어 내부데이터가 변경되는 것을 막을 수 있습니다.

 

만약 배열의 객체들이 primitive 타입이 아닌경우 배열의 모든 객체가 불변성을 만족해야 배열을 내부데이터로 가지고 있는 객체도 불변성을 보장하게 됩니다. 

 

(2) Collections Framework 의 객체인 경우

Collections Framework 의 객체를 참조하는 경우에도 배열의 경우와 비슷하게 생성자에서 값을 복사해서 받고, 반환할때는 Collecitons.unmodifiedList 메서드를 사용하여 반환합니다. 참고로 Collections.copy 는 얕은 복사로 참조값만을 전달하기 때문에 불변객체에서 사용하기에 부적합합니다. 

import java.util.Collections;
import java.util.List;

public class ListObject {

    private final List<Person> people;

    public ListObject(final List<Person> people) {
        this.people = new ArrayList<>(people);
    }

    public List<Person> getPeople() {
        return Collections.unmodifiableList(people);
    }
}

Collections.unmodifiableList(people) 로 반환된 List 객체를 수정하려고 시도하면 아래 에러가 발생합니다.

UnsupportedOperationException

 


4. 결론

  • 불변객체를 한번 초기화 하면 필드값을 변경할 수 없습니다.
  • 생성자와 필드에 final 키워드를 이용하여 내부 필드의 재할당을 막을 수 있습니다. primitive 타입의 필드만 가지는 객체의 경우는 final 키워드만으로 불변객체로 만들 수 있습니다.
  • reference 타입의 필드를 가지는 객체의 경우 모든 내부 필드가 불변성을 만족해야 불변객체로 분류될 수 있습니다. 
  • 배열과 collections 을 내부 필드로 가지는 경우 생성자에서 값을 복사해 새로운 객체를 만들고 반환할때 clone / unmodifiedable 을 이용해 수정을 막을 수 있습니다.

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함