Hanbit the Developer

Effective Kotlin | 8장. 효율적인 컬렉션 처리 본문

Mobile/Kotlin

Effective Kotlin | 8장. 효율적인 컬렉션 처리

hanbikan 2023. 9. 11. 22:03

아이템 49: 하나 이상의 처리 단계를 가진 경우에는 시퀸스를 사용하라

*Sequences | Kotlin Documentation를 읽어보시는 것을 적극 권장합니다.

Sequences는 Iterable과 매우 흡사하지만, 연산을 lazy하게 해서 더 효율적으로 처리하도록 설계되어 있으며, 다음과 같은 장점이 있다:

  • 자연스러운 순서를 유지한다.
  • 최소한만 연산한다.
  • 무한 시퀸스 형태로 사용할 수 있다.
  • 각각의 단계에서 컬렉션을 만들어 내지 않는다.

자연스러운 순서를 유지한다.

.filter { print("F$it, "); it % 2 == 1 }
.map { print("M$it, "); it * 2 }
.forEach { print("E$it, ") }

위 코드를 listOf(1, 2, 3)와 sequenceOf(1, 2, 3)에 적용하면, F1, F2, F3, M1, M3, E2, E6, 와 F1, M1, E2, F2, F3, M3, E6, 가 출력된다. 컬렉션 처리 함수를 사용하지 않고, 반복문을 통해 구현한다면 후자(Sequences)쪽으로 출력이 되기 때문에 순서가 자연스럽다.

*전자는 step-by-step order 또는 eager order, 후자는 element-by-element order 또는 lazy order라고 부른다.

최소한만 연산한다.

lazy하게 연산을 처리하기 때문에 연산 횟수가 최소화된다. 예시는 코틀린 공식 문서에서 확인해볼 수 있다.(”이 예시에서, 시퀸스가 연산을 18회 한 데 비해, list는 연산을 23회 하였다.”)

무한 시퀸스 형태로 사용할 수 있다.

generateSequence 또는 sequence를 통해 무한 시퀸스 형태를 사용할 수 있다.

generateSequence(1) { it + 1 }
	.take(5) // 1, 2, 3, 4, 5
val fibonacci = sequence {
	yield(1)
	var current = 1
	var prev = 1
	while (true) {
		yield(current)
		val temp = prev
		prev = current
		current += temp
	}
}
fibonacci.take(5) // 1, 1, 2, 3, 5

다만 오사용하면 무한 반복에 빠질 수 있으므로, 종결 연산으로 take와 first 정도만 사용하는 것이 좋다.

각각의 단계에서 컬렉션을 만들어 내지 않는다.

컬렉션은 중간 연산에서 단계마다 새로운 컬렉션을 만들어낸다. 이 때문에 시간, 공간적인 손해를 보게 된다.

시퀸스가 빠르지 않은 경우

sorted 함수의 경우 처리 방식 때문에 시퀸스보다 컬렉션이 더 빠르다. 하지만 다른 처리는 모두 시퀸스가 빠르기 때문에, 여러 처리가 결합된 경우에는 시퀸스를 사용하는 것이 더 빠르다.

Java 8: stream()

자바 8에 stream이라고 하는, 시퀸스와 유사한 것이 있다.

list.stream()
	.filter { it.bought }
	.map { it.price }

하지만 다음과 같은 차이점으로 인해 여전히 시퀸스를 사용하는 것이 좋다:

  • 시퀸스가 더 많은 함수를 갖고 있다.
  • 시퀸스가 사용하기 더 쉽다.(자바 스트림이 나온 이후 코틀린 시퀸스가 몇 가지 문제를 해결했기 때문)
  • 시퀸스는 Kotlin/JVM, Kotlin/JS, Kotlin/Native 등 일반적인 모듈에서 모두 사용 가능하지만, 자바 스트림은 JVM 8 이상 Kotlin/JVM에서만 동작한다.

단, 자바 스트림은 병렬 모드로 실행할 수 있어, 성능적 이득을 볼 수 있는 곳에서만 사용하는 것이 좋다. 하지만 몇 가지 결함이 있으므로 주의해서 사용해야 한다.

코틀린 시퀸스 디버깅

Kotlin Sequence Debugger 플러그인을 통해 단계적으로 요소의 흐름을 추적할 수 있다.

아이템 50: 컬렉션 처리 단계 수를 제한하라

컬렉션의 여러 함수를 알아둠으로써 처리 단계수를 줄여야 한다. 아래 세 코드는 모두 같은 동작을 수행한다.

.map { it.name }
.filter { it != null }
.map { it!! }
.map { it.name }
.filterNotNull()
.mapNotNull { it.name }

이외에 컬렉션 처리 함수를 한번에 끝내는 예시는 다음과 같다:

Before After
.filter { it ≠ null }  
.map { it!! } .filterNotNull()
.map { http://it.name }  
.filterNotNull() .mapNotNull { http://it.name }
.map { http://it.name }  
.joinToString() .joinToString { http://it.name }
.filter { it % 2 == 1}  
.filter { it > 5 } .filter { it % 2 == 1 && it > 5 }
.filter { it is Type }  
.map { it as Type } .filterIsInstance<Type>()
.sortedBy { <Key 2> }  
.sortedBy { <Key 1> } .sortedWith(comparedBy({ <Key 1> }, { <Key 2> }))
listOf(…)  
.filterNotNull() listOfNotNull(…)
.withIndex()  
.filter { (index, elem) → <Predicate> }  
.map { it.value } .filterIndexed { index, elem → <Predicate> }

*Collections | Kotlin Documentation을 전부 읽어보시는 것을 추천드립니다.

아이템 51: 성능이 중요한 부분에는 기본 자료형 배열을 사용하라

가볍고(메모리) 빠르기 때문에, 성능이 중요한 부분에는 기본 자료형 배열을 쓸 수 있다면 사용하는 것이 권장된다.

컬렉션은 제네릭을 사용하기 때문에 기본 자료형을 사용할 수 없다. 하지만 IntArray, LongArray 등을 활용하면 가능하다.

Kotlin Java
List<Int> List<Integer>
Array<Int> Integer[]
IntArray int[]

*코틀린 공식 문서: https://kotlinlang.org/docs/arrays.html#primitive-type-arrays

아이템 52: mutable 컬렉션 사용을 고려하라

immutable 컬렉션에서 아이템을 추가할 때, 새로운 컬렉션을 만들어서 처리하게 되는데, 비용이 매우 큰 작업이다. 따라서 추가 처리가 빠른 mutable 컬렉션을 사용하는 것이 좋다.