본문 바로가기

Kotlin

Kotlin In Effective - 8장. 컬렉션 효율적으로 사용하기

프로그래밍에서 컬렉션은 가장 중요한 개념 중 하나일 것이다.  코틀린도 컬렉션 처리 과정을 위한 여러가지 도구를 제공해준다.

 

val visibleNews = mutableListOf<News>() 

for(n in news){
      if(n.visible) { 
         visibleNews.add(n)
      }
}

Collections.sort(visibleNews,{ n1, n2 -> n2.publishedAt - n1.publishedAt })
val newsItemAdapters = mutableListOf<NewsItemAdapter>() 

for(ninvisibleNews){ 
     newsItemAdapters.add(NewsItemAdapter(n))     
}

위의 코드를 코틀린에서는 아래와 같이 짤 수 있다.

val newItemsAdapters = news
	.filter { it.visibile }
    .sortedByDescending { it.publishedAt }
    .map(::NewsItemAdapter)

코드가 간결해지고 그리고 가독성까지 더 좋아졌다! 

 

컬렉션 처리과정을 최적화 하는것은 매우 중요하다. 특히, 데이터 분석가나 백앤드 개발자라면 이를 무시하고 넘어가서는 안된다. 이러한 최적화는 어려운 것이 아니라 몇가지만 기억하면 된다. 이 방법들을 살펴보자.

 

Item 49 : 처리 단계가 두개 이상인 대규모 컬렉션에서는 sequence를 더 선호하라

인터페이스만 보면 두개는 별 차이가 없어 보인다. 

interface Iterable<out T> {
	operator fun iterator() : Iterator<T>
}

interface Sequence<out T> {
	operator fun iterator() : Iterator<T>
}

하지만 중요한 차이점이 있다

  • Iterable processing : 매 단계마다 List와 같은 컬렉션을 반환한다
  • Sequence processing : lazy, 새로운 동작과 함께 decorate된 새로운 Sequence를 반환한다
public inline fun <T> Iterable<T>.filter(
  predicate: (T) -> Boolean
): List<T> {
	return filterTo(ArrayList<T>(), predicate)
}

public fun <T> Sequence<T>.filter(
	predicate: (T) -> Boolean
): Sequence<T> {
	return FilteringSequence(this, true, predicate)
}

그래서 이러한 sequence의 lazy한 특성이 어떠한 좀이 좋은것 일까?

 

Order is important

  • sequence processing : 원소 원소 하나씩 모든 프로세스를 태운다 ( element-by-element or lazy order )
  • iterable processing : 프로세스 하나하나를 모든 원소에 태운다 ( step-by-step or eager order )

Sequences do the minial number of operations

종종 우리는 결과를 생산하기 위해 모든 단계에서 전체 컬렉션을 처리할 필요가 없을 때도 있다. 아래 그림을 보면 왜 더 효율적인지 이해가 된다.

Sequences do not create collections at every processing step

대략 700MB인 파일을 라인별로 읽어서 filter 한 후 출력을 한다면?

  • iterator : 13초
  • sequence : 4초, 메모리도 더 적게 먹음

5000개의 element중 구매한 상품의 가격 평균을 구하는 로직?

  • list processing : 81ns
  • sequence processing : 55 ns
  • list processing and acumulate ( average() ) : 83 ns
  • sequence processing and acumulate : 6 ns

Sequence를 써야 할 합당한 이유 ( 큰 컬렉션 작업 )가 있고 적절하게 사용한다면  성능의 이점을 얻을 것이다

 

Item 50 : operation의 횟수를 최대한 줄여라

class Student(val name : String?)

//Works
fun List<Student>.getNames() : List<String> =this .map { it.name }
    .filter { it != null }
    .map { it!! }

//Better
fun List<Student>.getNames():List<String>=this.map { it.name }
                                              .filterNotNull()

//Best
fun List<Student>.getNames():List<String>=this.mapNotNull { it.name }

아래는 operation의 횟수를 줄이기 위한 몇몇의 가이드이다.

위의 가이드를 통해 컬렉션이 iterate 되는 동안 중간 컬렉션 생성 비용을 줄여줄 수 있다.

 

Item 52 : mutuable collection 사용을 고려하라

mutable collection의 장점 중 하나는 훨씬 빠르다는 것이다. immutable collection에 하나의 원소를 더할 때, 우린 새로운 collection을 생성한 후에 하나의 원소를 더해야 한다.

operator fun <T> Iterable<T>.plus(element: T) : List<T> {
  if (this is Collection) return this.plus(element) 
  
  val result = ArrayList<T>()
  result.addAll(this)
  result.add(element)
  return result
}

그렇다고 무조건 mutuable을 사용하는게 좋을까? immutable을 사용하면 그들이 어떻게 변하는지를 우리가 제어할 수 있다는 장점이 있다. 그렇기 때문에, local scope 정도에서 원소 추가 작업이 많은 경우정도에 mutable 사용을 권장한다.