테스트 코드를 작성할 때면 List.of 를 무지성으로 사용하고는 했었다.
해당 정적팩토리 메소드를 통해 리턴되는 리스트는 불변 객체라고는 알고 있었지만, 어떤 의미에서의 불변인지.. 해당 컬렉션에 담겨있는 element는 변경이 가능한 것 같은데 왜 Immutable 이라는 이름이 붙어있는지 궁금해졌다.
그래서 교육 방학 첫날(?) release note 를 통해 알아보았다
ImmutableCollection
@jdk.internal.ValueBased
static abstract class AbstractImmutableCollection<E> extends AbstractCollection<E> {
// all mutating methods throw UnsupportedOperationException
@Override public boolean add(E e) { throw uoe(); }
@Override public boolean addAll(Collection<? extends E> c) { throw uoe(); }
@Override public void clear() { throw uoe(); }
@Override public boolean remove(Object o) { throw uoe(); }
@Override public boolean removeAll(Collection<?> c) { throw uoe(); }
@Override public boolean removeIf(Predicate<? super E> filter) { throw uoe(); }
@Override public boolean retainAll(Collection<?> c) { throw uoe(); }
}
- List, Set, Map 인터페이스에 대해 JDK9 에 추가된 정적 팩토리 메소드
- 어떤 객체가 불변이라는 것은
- 일단 생성되고 나면은 상태가 변경할 수 없는 것을 의미
- Collection 인스턴스를 생성하고 나면, 이 인스턴스에 대한 참조가 존재하고 있는동안 은 같은 데이터를 갖는 경우, 불변이라고 할 수 있겠죠?
- 따라서 , 이들은 자동으로 스레드 세이프 하다. ( 컬렉션...자체는..스레드 세이프합니다 )
- 여러 곳에서 해당 Collection 에 대한 참조를 갖고 있다고 하더라도, Collection 에 대한 변경은 불가능하니 스레드 세이프합니다.
- 어떤 추가,변경도 지원할 필요가 없기 때문에 “공간" 면에서도 효율적입니다.
- 따라서 Immutable collection 인스턴스들은 보통 , mutable 버전들에 비해 적은 메모리를 소비합니다
이렇게만 읽으면 마치, 완벽한 불변성을 보장하는 것 같습니다..
과연 그럴까요?
Immutable collection of objects ≠ a collection of immutable objects
JDK 9 에 추가된 이 편리한 팩토리 메소드가 리턴하는 컬렉션들은 기본적으로 불변입니다.
여기서 불변은
컬렉션에 대해 원소들을 add, set, remove 시도할 경우
- UnsupportedOperationException 을 던진다는 것입니다.
“immutable 객체들의 collection” 을 의미하는 것은 아닙니다.
- 즉, 내부의 원소 객체들에 대한 변경은 여전히 허용됩니다
테스트코드는 아래와 같습니다
@Test
@DisplayName("불변 리스트에서 꺼내온 원소의 값을 변경하면 리스트 내부 원소의 값이 변경된다")
public void test_modify_element(){
List<Person> immutableList = List.of(
Person.of(new Address("Well-ga", "Caffeez")),
Person.of(new Address("Something", "Caffeez"))
);
Person person = immutableList.get(0);
Address oldAddress = person.getAddress();
Address newAddress = new Address("NEW-Street", "Caffeez");
person.setAddress(newAddress); // 허용된다
Assertions.assertThat(immutableList.get(0).getAddress())
.isNotEqualTo(oldAddress);
Assertions.assertThat(immutableList.get(0).getAddress())
.isEqualTo(person.getAddress());
}
원소들이 mutable 하기 때문에, 결국 이 컬렉션은 일관되게 행동할 수가 없으며, 담고 있는 원소들이 변하는 것이나 다름없습니다.
따라서
immutable collection of objects ≠ a collection of immutable objects 입니다
ImmutableCollection 과 UnmodifiableCollection - ImmutableCollection 이 나온 이유
- JDK 8 때에는 , 수정할 수 없는 Collection 을 만들기 위해서는 아래와 같이 해야했습니다
List<String> stringList = Arrays.asList("a", "b", "c");
stringList = Collections.unmodifiableList(stringList);
즉, mutable list 를 먼저 만들고, 이를 인자로 전달하여 만들어줘야했습니다.
- 하지만 뒤에서 보겠지만 UnmodifiableCollection 은 실제로 불변 컬렉션이 아닙니다. 따라서 JDK 8 환경에서는 Guava 라이브러리의 ImmutableList 와 같은 것을 사용해야했다고 합니다.
- JDK 9 부터는 이렇게 하는 것이 가능해졌고, 이는 Java 에서 지원하는 ImmutableCollection 으로 불변 컬렉션입니다.
List stringList = List.of("a", "b", "c");
UnmodifiableCollection 은 불변 컬렉션이 아니다
immutable collection 은 Collection.unmodifiable wrapper들과 동일한 방식으로 동작합니다
하지만 immutable collection 은 wrapper 가 아닙니다
구현을 들여다 보면, UnmodifiableCollection 의 경우에는 아래와 같이 감싸는 대상 객체를 담고 있습니다. 그리고 메소드 구현을 보면, 해당 대상객체 에게로 메소드 호출을 위임하고 있습니다.
( Exception 을 던져야 하는 메소드 호출들에 대해서는 바로바로 exception 을 던지고 있다 )
반면 AbstractImmutableList 하위 클래스인 List12 를 보면, 이 클래스는 어떤 객체를 감싸고 있지도, 다른 객체에게 위임을 하는 것도 아닙니다. ( 물론 상위클래스에 정의되어있는 메소드 호출정도는 하겠죠 _ exception인 uoe() 을 던지는 부분들)
여기서 생기는 차이점은
Collections.unmodifiableList 가 감싸고 있는 list 는 여전히 modifiable 하다는 것!! ( UnmodifiableList 를 생성할 때 전달 해 준, 기존의 list 를 어디선가 변경하더라도 어떤 예외도 발생하지 않습니다 )
- 이로인해 , Collections.unmodifiableList(list) 가 리턴하는 view 역시, list 에 일어난 변화들을 계속해서 반영하고 있게 됩니다
- 해당 개념에 대해 혼란스럽다면, HashMap 의 view method 를 생각해보면 좋을 듯합니다
- 따라서 Collections.unmodifiableList는 실제로는 immutable collection 이 아닌 것.
테스트코드
@Test
@DisplayName("UnmodifiableList 가 래핑하는 컬렉션 인스턴스에 대한 변경은 UnmodifiableList 에 visible 하다")
public void test(){
// given
List<Person> people = new ArrayList<>();
people.add(Person.of(new Address("Street", "Samsung-Building")));
people.add(Person.of(new Address("MyStreet", "Samsung-Building")));
List<Person> unmodifiableList = Collections.unmodifiableList(people);
int beforeSize = unmodifiableList.size();
// when
people.add(Person.of(new Address("YourStreet", "Samsung-Building"))); // no exception
int afterSize = unmodifiableList.size();
// then
assertThat(beforeSize).isNotEqualTo(afterSize);
}
참조
'Java' 카테고리의 다른 글
타입 및 클래스들 사이에 존재하는 각 종 관계들과 리스코프 치환 원칙 (0) | 2023.03.24 |
---|---|
Java 에서 Stack 구현체로는 무엇을 사용해야할까?(feat_Java로 코딩테스트 문제 풀기) (0) | 2022.09.02 |
record 의 생성자를 private 으로 만드는 것이 불가능한 이슈 (0) | 2022.08.23 |
[Java] method final parameter ? (0) | 2022.07.27 |
[Java] 주어진 배열에서 특정 값을 갖는 index 찾기 (stream) (참조글 읽어보기) (0) | 2022.03.08 |