• 목록 (128)
    • Android (62)
    • Back-End (2)
    • Java (3)
    • Kotlin (16)
    • CS (7)
    • 개발 서적 (12)
    • 문제 풀이 (26)

최근 글

티스토리

전체 방문자
오늘
어제
hELLO · Designed By 정상우.
MJ_94

한 우물만 파는 기술 블로그

[Kotlin] 리스트를 통한 명령형 방식과 함수형 방식 비교
Kotlin

[Kotlin] 리스트를 통한 명령형 방식과 함수형 방식 비교

2021. 8. 4. 11:19

코틀린의 리스트는 명령형 방식과 함수형 방식을 모두 제공한다.

함수형 방식으로 작성하는 경우 고차 함수를 체이닝 하면 명령형 프로그래밍에서 for와 if문을 사용해서 작성하는 로직을 간결하게 표현할 수 있다.

예를 들면 입력 리스트의 값을 제곱하여 10보다 작은 첫 번째 값을 리턴하는 함수를 명령형으로 작성해보자.

fun imperativeWay(): Int {
    val bigIntList = (1..10000000).toList()
    var firstSquare = 0

    for (value in bigIntList) {
        val doubleValue = value * value
        if (doubleValue < 10) {
            firstSquare = doubleValue
            break
        }
    }

    return firstSquare
}

 

그렇다면 이번엔 함수형으로 작성해보자.

fun functionalWay(): Int =
    (1..10000000).toList().map { n -> n * n }.first { n -> n < 10 }

 

함수형이 더 간결하고 한눈에 보기 쉽다.

그리고 코드를 실행해보면 당연히 동일한 결괏값을 가진다.

fun result(){
    println(imperativeWay())
    println(functionalWay())
}

//imperativeWay - "1"출력
//functionalWay - "1"출력

 

그럼 성능에서는 어떤 차이점이 있을지 알아보자.

성능의 차이를 확실히 보이기 위해 리스트의 크기를 매우 크게 만들고 명령형 방식의 함수와 함수형 방식의 함수를 실행해보자.

fun test() {

    var start = System.currentTimeMillis()
    
    imperativeWay()
    println("imperativeWay result: ${System.currentTimeMillis() - start} ms")

    start = System.currentTimeMillis()
    
    functionalWay()
    println("functionalWay result: ${System.currentTimeMillis() - start} ms")
}

 

수행 결과를 보면 명령형으로 처리한 함수가 더 좋은 성능을 보여준다.

imperativeWay result: 202 ms
functionalWay result: 329 ms

 

왜 이런 차이가 발생할까?

두 함수에 값이 [1, 2, 3, 4, 5]인 리스트를 넣는다고 가정해보자.

imperativeWay 함수의 경우 처음부터 10보다 작은 값이 계산되어 나왔으므로 제곱 연산 1회, 비교 연산 1회를 총 2회의 연산을 하고 바로 1을 리턴한다.

functionalWay 함수의 경우 5개의 값에 대해서 모두 map을 실행한 후, filter를 수행한다.

그리고 마지막에 first 함수를 실행하므로 총 11회의 연산이 수행되어야 한다.

이와 같은 성능 문제는 코틀린의 모든 컬렉션에서 발생하게 된다.

따라서 다음과 같은 상황일 경우 컬렉션 사용을 지양해야 한다.

  1. 퍼포먼스에 민감한 프로그램을 작성할 때
  2. 컬렉션의 크기가 고정되어 있지 않을 때
  3. 고정된 컬렉션 크기가 매우 클 때

 

해결법

코틀린의 컬렉션은 기본적으로 값이 즉시 평가된다.

게으른 평가로 실행되지 않기 때문에 이렇게 성능이 저하된다.

이러한 부분을 보완하기 위해서 아래와 같이 시퀀스(sequence)를 사용하여 개선할 수 있다.

fun test(){

    var start = System.currentTimeMillis()

    realFunctionalWay()
    println("${System.currentTimeMillis() - start} ms")
}

fun realFunctionalWay(): Int =
    (1..10000000).toList().asSequence()
           .map { n -> n * n }
           .filter { n -> n < 10 }
           .first()

또는 시퀀스와 같은 게으른 컬렉션을 만들어 사용하면 된다.

매개변수의 형태를 중점적으로 참고해서 보면 되겠다.

sealed class FunList<out T>{
    object Nil: FunList<Nothing>
    data class Cons<T>(val head: T, var tail: FunList<T>): FunList<T>()
}

FunList는 Cons가 생성되는 시점에 head와 tail을 평가해서 그 값을 가지게 된다. 즉 즉시 평가가 이루어진다.

하지만 이러한 형태를 게으른 평가로 변환할 수 있는 간단한 방법이 있다.

sealed class FunStream<out T>{
    object Nil: FunStream<Noting>()
    data class Cons<out T>(val head: () -> T, val tail: () -> FunStream<T>):
        FunStream<T>()
}

위의 FunList와의 차이점을 보면 입력 매개변수를 람다로 받았다.

이렇게 하면 Cons가 생성되는 시점에 입력 매개변수는 즉시 평가되지 않는다.

실제로 값이 평가되는 시점은 그 값을 실질적으로 사용할 때이므로 Funstream의 head, tail을 얻기 위한 함수는

아래와 같아진다.

fun main(){
    println(funStreamOf(1, 2, 3).getHead()) //...1
    println(funStreamOf(1, 2, 3).getTail()) //...2
}

fun <T> FunStream<T>.getHead(): T = when(this){
    FunStream.Nil -> throw NoSuchElementException()
    is FunStream.Cons -> head()
}

fun <T> FunStream<T>.getTail():T = when(this){
    FunStream.Nil -> throw NoSuchElementException()
    is FunStream.Cons -> tail()
}


//1. "1" 출력
//2. "Cons(head=() -> T, tail = () -> ...)" 출력

getHead() 같은 경우는 사용가능 한 값이 실제로 있기 때문에 값이 평가된다.

하지만 getTail()의 경우는 여전히 FunStream을 반환하기 때문에 값이 평가되지 않고 이름이 출력된다.

FunStram은 게으른 컬렉션이 되었다.

'Kotlin' 카테고리의 다른 글

[Kotlin] inline 함수  (0) 2021.09.06
[Kotlin] Flow  (0) 2021.08.13
[Kotlin] 커링(Currying) 함수  (0) 2021.08.03
[Kotlin] Tailrec  (0) 2021.08.02
[Kotlin] Sealed Class  (0) 2021.07.30
    'Kotlin' 카테고리의 다른 글
    • [Kotlin] inline 함수
    • [Kotlin] Flow
    • [Kotlin] 커링(Currying) 함수
    • [Kotlin] Tailrec
    MJ_94
    MJ_94
    안드로이드, 개발 관련 기술 블로그

    티스토리툴바