Search

Sequences

Kotlin 표준 라이브러리에는 컬렉션과 함께 또 다른 유형인 시퀀스(Sequence<T>)가 있다.
컬렉션과 달리 시퀀스는 요소를 포함하지 않고 반복할 때 요소를 생성한다.
시퀀스는 Iterable과 동일한 함수를 제공하지만 다단계 컬렉션 처리에 대해 다른 접근 방식을 구현한다.

시퀀스의 지연 실행

Iterable의 처리가 여러 단계로 이루어질 때 각 단계는 즉시 실행된다.
각 단계가 완료되고 그 결과를 반환하면 다음 단계가 이 컬렉션에서 실행된다.
반면, 시퀀스의 다단계 처리는 가능한 한 지연 실행된다.
즉, 전체 처리 체인의 결과가 요청될 때만 실제 계산이 수행된다.

시퀀스의 실행 순서

시퀀스는 각 요소에 대해 모든 처리 단계를 하나씩 수행한다.
반면, Iterable은 전체 컬렉션에 대해 각 단계를 완료한 후 다음 단계로 진행한다.

Iterable과 시퀀스의 선택 기준

시퀀스를 사용하면 중간 단계의 결과를 생성하지 않아 전체 컬렉션 처리 체인의 성능을 향상시킬 수 있다.
그러나 시퀀스의 지연 실행 특성은 작은 컬렉션을 처리하거나 단순한 계산을 할 때 성능에 영향을 줄 수 있다.
따라서 시퀀스와 Iterable 중 어떤 것이 더 적합한지 고려해야 한다.

시퀀스 생성

요소로부터 생성

val numbersSequence = sequenceOf("four", "three", "two", "one")
Kotlin
복사
sequenceOf() 함수를 호출하여 시퀀스를 생성할 수 있다.

Iterable로부터 생성

val numbers = listOf("one", "two", "three", "four") val numbersSequence = numbers.asSequence()
Kotlin
복사
이미 Iterable 객체(List 또는 Set)가 있는 경우 asSequence()를 호출하여 시퀀스를 생성할 수 있다.

함수로부터 생성

val oddNumbers = generateSequence(1) { it + 2 } println(oddNumbers.take(5).toList()) // [1, 3, 5, 7, 9]
Kotlin
복사
generateSequence() 함수를 사용하여 요소를 계산하는 함수로 시퀀스를 생성할 수 있다.
옵션으로 첫 번째 요소를 명시적인 값으로 제공하거나 함수 호출의 결과로 제공할 수 있다.
제공된 함수가 null을 반환하면 시퀀스 생성이 중지된다.

청크로부터 생성

val oddNumbers = sequence { yield(1) yieldAll(listOf(3, 5)) yieldAll(generateSequence(7) { it + 2 }) } println(oddNumbers.take(5).toList()) // [1, 3, 5, 7, 9]
Kotlin
복사
sequence() 함수를 사용하면 시퀀스 요소를 하나씩 또는 임의 크기의 청크로 생성할 수 있다.
이 함수는 yield() 와 yieldAll() 함수 호출을 포함하는 람다 표현식을 받는다.

시퀀스 연산

시퀀스 연산은 상태 요구 사항에 따라 다음과 같이 분류할 수 있다.
상태 없는 연산
각 요소를 독립적으로 처리하며 현재 처리 중인 요소에만 집중하는 연산이다.
연산을 수행할 때 추가적인 상태나 메모리를 거의 사용하지 않으며 각 요소를 개별적으로 처리한다.
시퀀스나 컬렉션의 크기와 무관하게 동일한 양의 메모리와 자원을 사용한다.
예를 들어 map(), filter(), take(), drop() 가 있다.
상태 있는 연산
처리 중인 요소뿐만 아니라 시퀀스 또는 컬렉션의 여러 요소에 대한 정보를 필요로 하는 연산이다.
연산을 수행하는 동안 다른 요소들의 상태를 추적하거나 연산이 완료될 때까지 일정한 메모리를 유지해야 한다.
시퀀스의 크기에 비례하는 메모리나 성능 자원을 소모한다.
예를 들어 sorted(), distinct(), groupBy() 가 있다.
시퀀스 연산이 다른 시퀀스를 반환하고 이 시퀀스가 지연 생성된다면 이 연산을 중간 연산이라고 한다.
그렇지 않으면 중단 연산이다.

Iterable 과 Sequence 의 차이점

단어 목록이 있다고 가정하고 3글자보다 긴 단어를 필터링하고 처음 4개 단어의 길이를 출력한다.

Iterable

val words = "The quick brown fox jumps over the lazy dog".split(" ") val lengthsList = words.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:") println(lengthsList)
Kotlin
복사
filter() 및 map() 함수가 코드에 나타난 순서대로 실행되는 것을 볼 수 있다.
먼저 모든 요소에 대해 filter 한 후 필터링되고 남은 요소에 대해 length 로 매핑한 후 4개의 결과만 남긴다.

Sequence

val words = "The quick brown fox jumps over the lazy dog".split(" ") 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:") println(lengthsSequence.toList())
Kotlin
복사
filter() 및 map() 함수가 결과 목록을 빌드할 때만 호출됨을 보여준다.
따라서 먼저 “Lengths of …” 가 출력되고 시퀀스 처리가 시작된다.
filter 후 다음 요소를 처리하기 전에 map 이 실행된다.
take(4)이기 때문에 반환할 수 있는 결과가 4개에 도달하면 처리가 중지된다.

결론

동일한 작업을 할 때 Iterable이 23단계가 걸리는 것과 달리 Sequence는 18단계가 걸린다.