코틀린 지연계산 sequence 활용하기 | 콜렉션 연산 처리 filter, map, flatMap
람다식
람다식이란 함수선언없이 코드블록으로 실행가능한 함수 형태로 작성한 방식이다.
예를 들면,
val btn = findViewById(R.id.button)
// 옛날방식
btn.setOnClickListener(object : View.OnClickListener{
override fun onClick(v: View?) {
// click
}
})
// 람다방식
btn.setOnClickListener { v: View ->
// click
}
옛날 방식으로 버튼의 클릭 리스너를 작성하면 View.OnClickListener 익명 객체를 만들어 오버라이딩을 하고 여기에 클릭처리를 했습니다.
람다식을 사용하면 불필요한 상용코드 없이 메소드만 호출하여 클릭 이벤트 처리가 가능합니다. 버튼이 하나만 있다면 큰 차이가 없지만 우리가 만드는 애플리케이션의 뷰에 대한 이벤트 처리가 많기 때문에 람다식을 사용하는 것이 코드 길이도 줄이고 가독성도 높일 수 있습니다.
람다식을 굳이 고려하지 않아도 인텔리J 기반의 안드로이드 스튜디오가 알아서 힌트를 주기 때문에 자연스럽게 바꾸시면 됩니다.
콜렉션 연산 && 지연 계산
목록을 관리를 하면 필연적으로 콜렉션 연산(filter, map, all, any, count, find, groupBy, flatMap, flatten 등) 을 사용한다. 이를 통해 목록의 요소의 추가/제거/필터 등을 간편하게 처리하여 원하는 데이터만 가진 목록을 만들 수 있다.
목록의 데이터의 수가 작다면 큰 문제가 없지만 목록의 데이터 수가 늘어날 수록 성능 이슈를 고려해야 한다.
밑에 코드를 살펴보면,
fun main() {
/**
* 일반 처리
*/
val list = mutableListOf<Int>()
repeat(100_000_000) { list.add(Random.nextInt(100_000_000)) }
val startTime = System.currentTimeMillis()
val newList = list.filter { it % 2 == 0 }.map { it % 3 == 0 }.toList()
val endTime = System.currentTimeMillis()
val diff = endTime - startTime
println("Collection time $diff ${diff / 1000.0}")
/**
* Sequence 처리
*/
val list2 = mutableListOf<Int>()
repeat(100_000_000) { list2.add(Random.nextInt(100_000_000)) }
val startTime2 = System.currentTimeMillis()
val newList2 = list2.asSequence().filter { it % 2 == 0 }.map { it % 3 == 0 }.toList()
val endTime2 = System.currentTimeMillis()
val diff2 = endTime2 - startTime2
println("Collection Lazy time $diff2 ${diff2 / 1000.0}")
/** 결과 **/
// Collection time 4294 4.294
// Collection Lazy time 2968 2.968
}
두 연산은 기존 리스트에서 filter, map 을 하여 정제한 목록을 새로운 list 에 담는다. 그리고 각 결과에 대한 시간을 체크한다. 차이점은 아래의 연산에서는 리스트를 sequence 로 변환한 후에 filter, map 연산을 수행했다. 1번의 1억건에 대한 결과는 4.294초가 걸렸고 sequence를 추가한 2번의 1억건 결과는 2.968초가 걸렸다.
비슷한 연산이지만 시간 차이가 나는 것은 sequence 로 인한 지연계산 이 발생했기 때문이다.
일반적으로 collection 연산은 매번 수행을 할 때마다 새 collection 만들어서 반환하는데 자원을 사용한다.
public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
return filterTo(ArrayList<T>(), predicate)
}
public inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> {
return mapTo(ArrayList<R>(collectionSizeOrDefault(10)), transform)
}
반면에 sequence 로 넘어온 것은 해당 타입을 그대로 사용한다.
public fun <T> Sequence<T>.filter(predicate: (T) -> Boolean): Sequence<T> {
return FilteringSequence(this, true, predicate)
}
public fun <T, R> Sequence<T>.map(transform: (T) -> R): Sequence<R> {
return TransformingSequence(this, transform)
}
결정적인 차이는 수행을 하는 연산 방식이 다르다. 콜렉션 연산은 filter() 모두 수행 -> 콜렉션 만들고 -> map() 모두 수행 -> 콜렉션 만들고 -> 리턴이다 시퀀스를 사용한 연산은 각 항목에 대해 순차적으로 filter() -> map 을 반복하고 최종 연산은 호출하면 연기되엇던 모든 계산이 수행된다.
아래의 코드를 보면,
listOf(1, 2, 3, 4, 5).asSequence()
.map { print("map[$it] "); it * it }
.filter{ print("filter[$it] "); it % 2 ==0}
.toList()
println()
listOf(1, 2, 3, 4, 5)
.map { print("map[$it] "); it * it }
.filter{ print("filter[$it] "); it % 2 ==0}
.toList()
/** 결과 **/
// map[1] filter[1] map[2] filter[4] map[3] filter[9] map[4] filter[16] map[5] filter[25]
// map[1] map[2] map[3] map[4] map[5] filter[1] filter[4] filter[9] filter[16] filter[25]
collection 처리는 map 의 모든 연산 결과를 출력하지만 sequence 처리는 map -> filter 수행후 최종연산시 연기되었던 중간연산을 호출한다. 원소를 재배치하고 콜렉션을 만드는 비용이 줄어들기 때문에 지연계산을 활용한 시퀀스처리가 성능적으로 우수하다고 볼 수 있다.
결론은 콜렉션 연산을 처리할 때 시퀀스를 고려하자!!