ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 49장 - 하나 이상의 처리 단계를 가진 경우에는 시퀀스를 사용하라
    Kotlin/Effective Kotlin 요약 2023. 4. 13. 00:01

    Iterable과 Sequence의 차이

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

    둘의 차이는 이름밖에 없는 것처럼 보입니다.

    하지만 완전히 다른 목적으로 설계되어 있습니다.

     

    서로 다른 filter 확장함수 구현

    public inline fun<T> Iterable<T>.filter(
    	predicate: (T) -> Boolean
    ): List<T>{
    	return filterTo(ArrayList<T>(), predicate)
    }
    
    
    public inline fun<T> Sequence<T>.filter(
    	predicate: (T) -> Boolean
    ): List<T>{
    	return FilteringSequence(this, true, predicate)
    }
    
    fun main() {
        val seq = sequenceOf(1,2,3)
        val filtered = seq.filter { print("f$it"); it % 2 == 1 }
        println(filtered) //FilteringSequence@...
    
        val list = listOf(1,2,3)
        val listFiltered = list.filter { print("f$it"); it % 2 == 1 }
        println(listFiltered) //f1f2f3[1, 3]
    }

    Sequence는 lazy하게 처리됩니다. (연산으로 새로운 Sequence가 return 됩니다)

    Iterable는 eager하게 처리됩니다. (연산으로 새로운 List가 만들어집니다)

     

     

    Lazy? Eager?

    lazy하게 처리된다? eager 하게 처리된다는 어떤 의미일까요.

    Iterable은 호출할 때 연산이 이루어지고(eager), Sequence는 최종 연산이 이루어지기 전까지 각 단계에서 연산이 일어나지 않습니다(lazy)

     

    이는 위의 예시로도 알아볼 수 있습니다.

     

    Sequence의 장점

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

     

    순서의 중요성

    sequenceOf(1, 2, 3)
    	.filter { print("F$it, "); it % 2 == 1 }
    	.map { print("M$it, "); it * 2 }
    	.forEach { print("E$it, ") }
    
    //F1, M1, E2, F2, F3, M3, E6, 
    
    println()
    
    listOf(1, 2, 3)
    	.filter { print("F$it, "); it % 2 == 1 }
    	.map { print("M$it, "); it * 2 }
    	.forEach { print("E$it, ") }
    //F1, F2, F3, M1, M3, E2, E6,

    해당 예제까지보면 lazy와 eager의 차이를 명확하게 이해할 수 있습니다.

     

    sequence의 경우에 마지막에 forEach를 만났기 때문에 순서가 다음과 같이 발생합니다.

    • F1 -> M1 -> E2
    • F2
    • F3 -> M3 -> E6

     

    List의 경우에는 Eager 연산이기 때문에 순차적으로 F -> M -> E 순서로 발생하는 것을 볼 수 있습니다.

    그리고 filter, map각각의 결과는 List가 될 것입니다.

     

     

    Sequence의 최소 연산

    위의 특성 때문에 sequence는 자연스럽게 최소 연산을 할 수 있습니다.

    만약 최종 연산에서 2개의 결과만 원한다면 2개의 요소에 대한 처리만 할 수 있습니다.

     

    하지만 Iterable의 경우 만약 2개의 결과만 원하더라도 모든 연산을 처리하게 됩니다.

    이는 eager로 순차적으로 발생하기 때문입니다.

     

    무한 Sequence

    비슷한 의미로 무한적인 sequence를 만들고 필요한 부분까지만 값을 추출하는 것도 가능합니다.

    하지만 Iterable의 경우 무한으로 만들면 연산 후 무한적인 List를 반환해야 하기 때문에 무한적인 연산이 일어나게 됩니다.

    sequence의 경우에도 몇 개 활용할지 지정하지 않고 toList를 반환한다면 무한하게 반복되어 종료되지 않습니다.

     

     

    각 단계에서 컬렉션을 만들어 내지 않음

    크거나 무거운 컬렉션은 큰 비용이 들어갑니다.

    극단적인 예로 기가바이크 단위의 파일을 읽어 들이고 컬렉션 처리를 한다면 엄청난 메모리 낭비를 불러일으킬 수 있습니다.

    따라서 파일을 처리할 때는 시퀀스를 활용할 수 있습니다.

     

    Sequence가 Collection보다 빠르지 않은 경우 - sorted

    Collection 전체를 기반으로 처리해야 하는 연산은 sequence를 사용해도 빨라지지 않습니다.

    예를 들어 stdlib의 sorted가 존재합니다.

    sorted는 sequence를 List로 변환한 뒤, 자바 stdlib의 sort를 사용해 처리합니다.

    이런 변환 과정 때문에 오히려 sequence가 collection 처리보다 느려집니다.

     

    이런 과정 때문에 무한 sequence에서 sorted를 적용하면 무한 반복에 빠지는 경우가 있습니다.

    따라서 sequence에서 sorted를 빼야 한다는 의견도 존재합니다.

    하지만 정렬 처리는 일반적으로 사용되므로 sequence에도 들어갔습니다.

    따라서 무한 시퀀스에 sorted를 사용할 수 없다는 결함은 따로 기억해야 합니다.

     

    자바 8의 Stream

    Java8의 Stream 기능은 코틀린의 시퀀스와 비슷한 형태로 동작합니다.

    stream도 lazy 하게 동작합니다.

     

    코틀린의 sequence와 자바의 stream의 차이

    • sequence가 더 많은 처리 함수를 가지고 있습니다. 예를 들어 collect(Collectors.toList())가 아닌 toList처럼 활용할 수 있습니다.
    • stream은 병렬 모드로 실행할 수 있습니다.
    • sequence는 코틀린/JVM, 코틀린/JS, 코틀린/네이티브 등의 일반적인 모듈에서 모두 사용할 수 있습니다. 하지만 stream은 코틀린/JVM에서만 동작하며, JVM이 8 버전 이상일 때만 동작합니다.

     

    Sequence의 디버깅

    Kotlin Sequence Debugger라는 플러그인을 활용해서 디버깅을 수행할 수 있습니다.

     

     

     

     

     

     

    댓글

Designed by Tistory.