•
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단계가 걸린다.