Lined Notebook

Iterator와 Sequence

by HeshAlgo

1. Iterator?

- Collection에 저장된 요소들을 순차적 처리 하기 위해 사용합니다. (Eager 처리)
- 해당 Iterator 인터페이스는 아래와 같은 구조를 가지고 있습니다.

public interface Iterator<out T> {
    /**
     * Returns the next element in the iteration.
     */
    public operator fun next(): T

    /**
     * Returns `true` if the iteration has more elements.
     */
    public operator fun hasNext(): Boolean
}

 
Iterator는 저장된 요소들을 순차적으로 접근하기 때문에 특정 인덱스에 바로 접근하지 못한다는 특징이 있습니다.
그렇기 때문에 3번째 요소에 접근하고 싶다면 1, 2번째 요소를 접근한 후에 접근할 수 있습니다. 
아래의 코드는 Iterator를 사용하는 기본적인 예시입니다.

fun main() {
    val numbers = listOf("one", "two", "three", "four").iterator()
    println(numbers.next())     // one
    println(numbers.next())     // two
    println(numbers.next())     // three
    println(numbers.next())     // four
    println(numbers.next())     // NoSuchElementException
}

위와 같이 Iterator는 더 이상 반환할 요소가 없을 경우 NoSuchElementException을 호출하게 되어있습니다.
그렇기 때문에 hasNext() 메소드를 통해 Collection 요소에 반환할 데이터가 있는지 체크 후 안전하게 next() 메소드를 호출 할 필요가 있습니다. 

fun main() {
    val numbers = listOf("one", "two", "three", "four").iterator()
    while (numbers.hasNext()) {
        println(numbers.next())
    }
}

 
 

2. Iterator의 단점

코틀린을 사용하다 보면 kotlin.Collections 내부에 있는 map, filter, take 등을 많이 사용하게 되는데 이들은 전부 Iterable 인터페이스로 구현이 되어있습니다. 그렇기 때문에 해당 함수들을 처리할때마다 각각의 단계에서 새로운 컬렉션을 만들어 낸다.
이를 활용해 저장하는것이 컬렉션의 장점이긴 하지만 메모리를 차지하는것이 단점이 될 수 있기 때문에 무거운 컬렉션을 처리할 때는 큰 비용이 발생합니다. 

fun main() {
    numbers.filter { ... }   // 새로운 컬렉션 생성1
    	   .map { ... }      // 새로운 컬렉션 생성2
           .sum()            // 전체적으로 2개의 컬렉션이 생성
}

 
 

3. Sequence란?

- Java에서 Stream과 유사한 기능을 지원한다.
- 이전 Iterator는 순차적으로 컬렉션을 처리하는 반면 Sequence는 지연처리(Lazy)를 통해 컬렉션을 처리한다.
그렇기 때문에 필요한 시점에만 연산을 수행한다는 특징을 가지고 있습니다.
해당 지연처리 부분에 대한 부분이 이해가 안될수 있는데 한가지 예시를 보도록 하겠습니다.
아래의 예시는 문자열 리스트를 asSequence()를 통해 시퀀스화 시킨 예제입니다. (해당 예제를 꼭 실행해보시는것을 추천드립니다.)

fun main() {
    val words = "The quick brown fox jumps over the lazy dog".split(" ")
    //convert the List to a Sequence
    val wordsSequence = words.asSequence()

    val lengthsSequence = wordsSequence.filter { println("filter: $it"); it.length > 3 }
        .map { println("length: ${it.length}"); it.length }
        .take(4)

    println("Lengths of first 4 words longer than 3 chars")
    // terminal operation: obtaining the result as a List
    println(lengthsSequence.toList())
}

 
해당 코드를 실행해보면 filter, map 내부에 있는 println 함수가 먼저 찍힐것 같지만 그렇지 않고
"Lengths of first 4 words longer than 3 chars" 문자열이 먼저 찍히는 모습을 보실수 있으실 것입니다. 이러한 동작이 Sequence의 lazy 동작입니다. 즉 마지막에 toList()를 호출하는것처럼 결과가 필요한 시점에만 동작이 수행됩니다.
그리고 해당 Sequnce는 Iterator처럼 중간중간에 새로운 리스트를 생성하지 않고 바로 바로 연산처리를 한다는 특징을 가지고 있습니다.

 

4. Iterator와 Sequence 무엇을 사용하면 좋을까?

위의 글에서 작성했다시피 Sequence를 사용하면 중간중간 불필요한 연산이나 새로운 컬렉션을 만들지 않으니 무조건 Sequence를 쓰면 되겠네! 싶겠지만 그건 아닙니다. 결론을 말씀드리자면 정말 많은 대용량을 처리해야되는 경우 Sequece를 사용하는 것이 효율적이겠지만 그렇지 않은 단순 연산이나 소량의 데이터를 처리해야되는경우는 단순 Iterator로 처리하는게 좋은것 같습니다. 
무조건 Sequence를 쓴다해도 단순한 연산에서 사용할 경우 불필요한 오버헤드가 발생하기 때문에 Sequence가 더 느리게 처리되기 때문입니다. 
아래의 예제는 천만개의 숫자 리스트를 iterator와 sequence로 처리했을때의 성능 속도의 차이이다.

    @Test
    fun `Iterator와 Sequence 성능 차이`() {
        val numbers = (1..10_000_000).toList()
        val iteratorNumbers = measureTimeMillis { iteratorFunction(numbers) }
        val sequenceNumbers = measureTimeMillis { sequenceFunction(numbers) }

        println("iteratorNumbers : $iteratorNumbers")    // 15908
        println("sequenceNumbers : $sequenceNumbers")    // 1905
    }

    private fun iteratorFunction(lists: List<Int>): List<Int> {
        return lists.filter { it > 0 }.filter { it > 1 }.filter { it > 2 }.map { it + it }.filter { it > 3 }.toList()
    }

    private fun sequenceFunction(lists: List<Int>): List<Int> {
        return lists.asSequence().filter { it > 0 }.filter { it > 1 }.filter { it > 2 }.map { it + it }.filter { it > 3 }.toList()
    }

위와 같이 엄청난 차이의 속도 차이가 나고 해당 범위를 한번 50만개 정도로 더 적게 실행해보는것도 추천한다.
 
실무에서 위 예시와 같이 여러가지 filter 및 map등을 적용하는 케이스는 드물기 때문에 대부분 Sequence를 취하는 케이스는 드물었던것 같다. 하지만 해당 인터페이스들의 동작방식을 알아야만이 좀 더 효율적인 코드를 작성할수 있지 않을까 싶다.
 
 
 
# 참고문헌
- https://kotlinlang.org/docs/iterators.html
- https://kotlinlang.org/docs/sequences.html#construct

'Language > 코틀린(Kotlin)' 카테고리의 다른 글

Scope Function  (0) 2021.10.26
[Kotlin] 프로그램 흐름 제어  (0) 2021.03.31
[Kotlin] 코틀린 null 처리 및 자료형 변환  (0) 2021.03.30

블로그의 정보

꾸준히 공부하는 개발 노트

HeshAlgo

활동하기