일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 31 |
- Hilt
- 코드포스
- textfield
- 쿠링
- activity
- architecture
- MiTweet
- ProGuard
- Rxjava
- TEST
- 백준
- relay
- pandas
- boj
- Compose
- AWS
- 프로그래머스
- Coroutine
- android
- MyVoca
- Coroutines
- androidStudio
- Kotlin
- GitHub
- Python
- 암호학
- livedata
- Codeforces
- Gradle
- 코루틴
- Today
- Total
이동식 저장소
Kotlin Immutable Collections 본문
``List<T>``에는 리스트를 수정할 수 있는 메서드가 없지만, 어떤 리스트의 타입이 ``List<T>``라고 해서 리스트의 내용이 변경되지 않는다고 말할 수는 없다. ``MutableList<T>``를 ``List<T>``로 반환했을 지도 모르기 때문이다. Compose 컴파일러가 ``List<T>``를 unstable로 판단하는 이유이다.
하지만 Immutable Collections 라이브러리에는 어떠한 경우에도 내용이 변경되지 않는 collections 타입이 정의되어 있다. 내용을 변경해야 하는 경우에는 아예 새로운 객체를 만들어서 변경을 적용한 후 반환한다. Compose도 이것만큼은 stable로 인정한다.
구조
``Immutable~`` 인터페이스는 완벽한 읽기 전용이다. 아예 함수조차 ``get``과 ``subList`` 뿐이다. ``Persistent~`` 인터페이스는 리스트의 수정을 지원하지만, 리스트의 사본에 변경 사항을 적용하여 반환하기 때문에 원래 리스트는 바뀌지 않는다.
딱 보면 답 나오지? ``Immutable``는 UI 등 앞단에서, ``Persistent``는 데이터를 관리하는 뒷단에서 사용하면 되겠다. UI에서는 데이터를 읽기만 하니까(정확히는 읽기만 해야 하니까). 반면 뒷단에서는 현재 데이터에 변경 사항을 적용하여 새로운 데이터를 제공해야 하는데, 이때 ``Persistent`` 인터페이스에 정의된 수정 함수를 사용하면 된다.
코드
이 글에서는 ``ImmutableList``와 ``PersistentList``만을 살펴본다.
``ImmutableCollection``은 단순한 marking interface이다. 진짜 한 줄밖에 없다.
``ImmutableList``에는 ``subList()`` 단 하나의 함수만 정의돼 있다. 코드를 안 봐도 알 것 같은..
그런데 ``subList()``의 반환 타입인 ``SubList<T>``는 볼 만하다. 그냥 ``ImmutableList``를 잘라서 반환하는 거 아닌가 싶지만 그 말대로라면 애초에 ``SubList`` 타입이 존재할 이유가 없다. ``SubList``에서 주목할 점은, 특정 리스트의 일부만을 표현함에도 불구하고 전체 리스트를 들고 있다는 점이다.
public interface ImmutableList<out E> : List<E>, ImmutableCollection<E> {
// 리스트 전체를 파라미터로 넘긴다.
fun subList(fromIndex: Int, toIndex: Int): ImmutableList<E> = SubList(this, fromIndex, toIndex)
private class SubList<E>(private val source: ImmutableList<E>, private val fromIndex: Int, private val toIndex: Int) : ImmutableList<E>, AbstractList<E>() {
private var _size: Int = 0
init {
ListImplementation.checkRangeIndexes(fromIndex, toIndex, source.size)
this._size = toIndex - fromIndex
}
override fun get(index: Int): E {
ListImplementation.checkElementIndex(index, _size)
return source[fromIndex + index]
}
override val size: Int get() = _size
override fun subList(fromIndex: Int, toIndex: Int): ImmutableList<E> {
ListImplementation.checkRangeIndexes(fromIndex, toIndex, this._size)
return SubList(source, this.fromIndex + fromIndex, this.fromIndex + toIndex)
}
}
}
보다시피 원본 리스트를 계속 참조한다. (의미상) 한번 자른 ``SubList``를 잘라도 여전히 똑같은 원본을 참조한다. 리스트를 매번 자르는 게 더 비효율적이라고 판단한 듯하다. 흠.. 나름 일리 있는 것 같기도 하고?
이제 ``PersistentCollection``을 보자. ``PersistentCollection``에는 몇 가지 수정 함수와, 수정 함수를 지원하기 위한 ``Builder`` 인터페이스가 정의돼 있다.
public interface PersistentCollection<out E> : ImmutableCollection<E> {
fun add(element: @UnsafeVariance E): PersistentCollection<E>
fun addAll(elements: Collection<@UnsafeVariance E>): PersistentCollection<E>
fun remove(element: @UnsafeVariance E): PersistentCollection<E>
fun removeAll(elements: Collection<@UnsafeVariance E>): PersistentCollection<E>
fun removeAll(predicate: (E) -> Boolean): PersistentCollection<E>
fun retainAll(elements: Collection<@UnsafeVariance E>): PersistentCollection<E>
fun clear(): PersistentCollection<E>
/**
* 수정 연산용 builder
*/
interface Builder<E>: MutableCollection<E> {
fun build(): PersistentCollection<E>
}
fun builder(): Builder<@UnsafeVariance E>
}
전부 이름만 봐도 알 수 있는 함수이다. 특이한 점은 모든 수정 함수가 ``PersistentCollection``을 반환한다는 점. 말했다시피 원본 객체에 수정 연산을 적용한 사본을 반환하기 때문이다.
단순한 삽입/삭제보다 더 복잡한 연산을 하고 싶다면, ``PersistentCollection.mutate()`` 확장 함수를 사용하자. ``mutate()`` 함수 안에서는 현재 리스트의 사본을 ``MutableList``로 접근할 수 있어 다양한 수정 연산을 적용할 수 있다. 당연히 원본은 바뀌지 않는다.
// list의 타입은 MutableList이다.
persistentList.mutate { list -> list.replaceAll { number -> number * number } }
``PersistentList``는 ``PersistentCollection``과 똑같은 형태의 인터페이스라 생략한다. 밑단의 세부적인 구현은 당연히 생략.
요약
- 데이터를 읽기만 한다면 ``Immutable...``을 사용하자.
- 데이터를 수정한다면 ``Persistent...``를 사용하자.
Compose 개발자들은 stability 때문에라도 자주 사용할 듯하다.
'Primary > Kotlin' 카테고리의 다른 글
[Kotlin] Computed Property (0) | 2022.10.06 |
---|---|
[Kotlin] Coroutine 테스트 디버깅 후기 (0) | 2022.07.30 |
[Kotlin] sealed class vs. enum class (0) | 2022.07.07 |
[Kotlin] value class로 값을 감싸보자 (0) | 2022.06.17 |
[Kotlin] 클래스 안에 확장 함수? (0) | 2022.06.16 |