개념 | 표준 Kotlin 컬렉션 | JetBrains Immutable 컬렉션 |
---|---|---|
읽기 전용 (Read-only) | List | ImmutableList |
변경 가능 (Mutable) | MutableList | 없음 (❌) |
구조적 불변 (Persistent) | 없음 (❌) | PersistentList |
List는 읽기 전용 객체이며, MutableList는 읽고 쓰기 전용 객체이다. 따라서 List는 add/addAll/remove등의 메서드 호출이 불가한 반면, MutableList는 가능하다.
MutableList의 수정작업 시, 객체 자체가 변한다. 따라서 이를 외부로 공유하는 작업은 내부 상태의 예상치 못한 변경으로 인해 위험할 수도 있다.
이를 방지하고자, List타입 공유를 생각할 수 있다. 하지만 이 또한 100% 완벽한 방법이 아니다. 아래 코드를 보면 알다시피 'list'변수의 타입은 읽기 전용인 List
타입이다. 하지만 진짜 타입은 MutableList
이다. 따라서 list 변수 타입을 하위 캐스팅 후 add()호출로 내부 상태 값을 변경할 수 있는데, 이는 List
타입이라고 해도 외부에 공유하는 작업이 완전히 안전하지 않다는 의미다.
fun main() {
val list: List<Int> = mutableListOf(1,2,3)
val list2 = list
(list as MutableList).add(4)
println(list) // 1,2,3,4
println(list2) // 1,2,3,4
}
내부적으론 'list'변수만 쓰고, 외부에 'list2'변수를 공유한다 생각해보자. list2 또한 [1,2,3,4]가 나오는 위험이 있다.
얼핏 보면 List
타입은 ImmutableList
에, MutableList
는 PersistentList
에 대응된다 생각할 수 있다. 왜냐면, 전자는 읽기 전용이며, 후자는 쓰기 전용으로 add/remove등의 메서드를 지원하기 때문이다. 하지만 이들은 차이가 있다.
일전에 List
타입은 하위 캐스팅을 통해 내부 상태 값 변경에 위험이 있다했다. 하지만 ImmutableList
는 하위 캐스팅을 통해 PersistentList
로 될 수는 있을지언정, add/remove등 수정 작업 진행 시, 내부 상태 값이 변경되지 않는다. 해당 메서드를 호출 시, 변경된 상태가 반영 된 새로운 리스트를 반환한다.
val oldList: PersistentList<Int> = persistentListOf(1, 2, 3)
val newList = oldList.add(4) // oldList는 그대로 유지
위와 같은 동작때문에, PersistentList
변경 시, '깊은 복사'를 진행한다 생각하라 수 있다. 물론, List
/MutableList
들끼리는 그렇지만, ImmutableList
/PersistentList
는 그렇지 않다. PersistentList
는 구조 공유를 사용한다. 내부는 트리 구조로 돼있고, 일부 복사 후, 나머지는 공유한다.
위 코드 예시로 설명하자면,
val oldList: PersistentList<Int> = persistentListOf(1, 2, 3)
val newList = oldList.add(4) // oldList는 그대로 유지
위 코드에서 내부적으로 [1,2,3]노드를 공유하한다. 그 후, newList에는 4만 붙임으로써 성능을 최적화시킨다. 따라서 결과적으로 기존 데이터의 깊은 복사를 진행하지 않는다는 점이 특징이고, GC비용도 줄어든다. 또한 다수의 리스트를 메모리에 올릴 수 있어 메모리적으로도 효과적이다.
요약
- List/MutableList는 내부 상태를 변경한다.
- ImmutableList/PersistentList는 내부 상태를 변경하지 않는다. 상태를 변경할 땐, 새로운 리스트를 반환한다.
- 반환하는 새로운 리스트는 ImmutableCollection 내부에서 노드 공유를 통해 GC비용을 최적화한다.