Kotlin의 List와 MutableList의 차이점을 알아보고 어떤 경우에 사용하면 좋은지 알아보자!
불변 - 만든 후에 요소가 바뀔 수 없음, 대신 sorted()
, reversed()
와 같은 새 List를 만들어 반환하는 함수 사용 가능
가변 - 만든 후에 요소가 바뀔 수 있음 → add()
, remove()
, … 요소를 변경하는 함수 사용 가능
var list1 = listOf(1, 2, 3, 4, 5, 6)
val list2 = mutableListOf(1, 2, 3, 4, 5, 6)
두 형태 모두 요소를 바꿀 수 있는 형태이다.
var list1 = listOf(1,2,3,4,5,6)
list1 = list1 + listOf(7) // 1, 2, 3, 4, 5, 6, 7
val list2 = mutableListOf(1, 2, 3, 4, 5, 6)
list2.add(7) // 1, 2, 3, 4, 5, 6, 7
list1은 +
연산을 사용하여 새 List를 만들어 대입, list2는 MutableList의 함수인 add
를 활용한 모습이다.
list1은 요소가 변하지 않을 것이라는 데이터의 불변성을 보장할 수 있지만 list2는 그렇지 않다. 하지만 요소를 변경하는 함수가 지원되므로 편리하게, 직관적으로 요소를 변경하는 코드를 작성할 수 있다.
그래도 List를 사용할 때 데이터의 불변성이 보장되는게 좋은게 아닌가? 라고 생각할 수 있지만…
Effective Kotlin Item 56: Consider using mutable collections
위 문서의 첫 문장에 나와있듯, MutableList는 강한 성능을 보여준다고 한다. 정말일까?
import kotlin.time.ExperimentalTime
import kotlin.time.TimeSource
@OptIn(ExperimentalTime::class)
fun main() {
var mark = TimeSource.Monotonic.markNow()
var list1 = listOf(1, 2, 3, 4, 5, 6)
list1 = list1+listOf(7) // --- (1)
println(list1)
println(mark.elapsedNow())
mark = TimeSource.Monotonic.markNow()
val list2 = mutableListOf(1, 2, 3, 4, 5, 6)
list2.add(7) // --- (2)
println(list2)
println(mark.elapsedNow())
}
먼저 시간을 재보았다.
(1)보다 (2)가 확연히 더 빠르다는 것을 확인할 수 있었다. 디컴파일도 해보자
public static final void main() {
long mark = Monotonic.INSTANCE.markNow-z9LOYto();
List list1 = CollectionsKt.listOf(new Integer[]{1, 2, 3, 4, 5, 6});
list1 = CollectionsKt.plus((Collection)list1, (Iterable)CollectionsKt.listOf(7));
System.out.println(list1);
Duration var3 = Duration.box-impl(ValueTimeMark.elapsedNow-UwyO8pc(mark));
System.out.println(var3);
mark = Monotonic.INSTANCE.markNow-z9LOYto();
List list2 = CollectionsKt.mutableListOf(new Integer[]{1, 2, 3, 4, 5, 6});
list2.add(7);
System.out.println(list2);
Duration var4 = Duration.box-impl(ValueTimeMark.elapsedNow-UwyO8pc(mark));
System.out.println(var4);
}
CollecitonsKt.plus()
를 사용하는구나! 더 파고 들어가보자
public operator fun <T> Collection<T>.plus(elements: Iterable<T>): List<T> {
if (elements is Collection) {
val result = ArrayList<T>(this.size + elements.size)
result.addAll(this)
result.addAll(elements)
return result
} else {
val result = ArrayList<T>(this)
result.addAll(elements)
return result
}
}
새 List를 만들어서 반환하는 모습을 확인할 수 있었다. (1)의 방법이 더 느리다는 것을 알 수 있었다.
var list1 = listOf(1,2,3,4,5,6)
list1 = list1 + listOf(7) // 1, 2, 3, 4, 5, 6, 7
val list2 = mutableListOf(1, 2, 3, 4, 5, 6)
list2.add(7) // 1, 2, 3, 4, 5, 6, 7
첫 번째의 경우에는 느리지만, 변할 수 없는 List를 반환받아 사용하므로 이러한 경우를 클래스에 적용했을 때 데이터가 외부에서 변환되지 않는다는 보장이 있다.
두 번째의 경우 첫 번째 처럼 데이터 변환에 대한 보장은 없지만 빠르게 add할 수있다.
따라서, 나는 List를 자주 변경해야한다면 두 번째 방법을, 그렇지 않다면 첫 번째 방법이 좋다고 생각한다.
아래 스택오버플로우 글에서는 성능 문제가 아니더라도 Collection이 중간에 수정될 여지가 있음을 직관적으로 나타낼 수 있는 2번을 선호한다는 의견도 존재했다.
Kotlin: val mutableList vs var immutableList. When to use which?
요약하자면, Collection이 바뀔 수 있다면 mutable을 사용하고, 오직 보기 전용이라면 immutable를 사용한다. mutable vs immutable과 별개로 val과 var의 목적은 값이나 주소가 바뀔 수 있고 없고의 관점이기 때문에 var을 사용하게 되면 ‘이 변수가 나중에 바뀔 수 있구나!’를 직관적으로 확인할 수 있는 것이다.