코틀린 표준 라이브러리에는 Sequence(시퀀스)라는 타입이 있다. 시퀀스는 컬렉션과 다르게 요소를 포함하고 있지 않고 반복하는 동안 요소를 생성한다. 그래서 자바의 Iterable
과 같은 역할을 한다고 볼 수 있지만 멀티 스텝 처리를 하는 경우에는 Iterable
과 다른 방식으로 동작한다.
Iterable
이 멀티 스텝을 처리하는 경우는 각 단계(Intermediate collection - 중간 결과 컬렉션)을 바로 계산(eagerly)한다. 반면에 시퀀스는 가능하면 계산을 나중으로 미루기(lazily) 때문에 그 결과값을 호출할 때 전체 프로세스 연산을 시작한다. 시퀀스 연산의 결과가 다시 시퀀스인 경우는 계산이 지연되고, Intermediate(중간 결과)라고 부른다. 결과가 시퀀스가 아닌 경우는 terminal(최종 결과)라고 한다. 시퀀스의 요소는 terminal operation(최종 연산)를 호출하는 경우에 얻을 수 있고 최종 연산의 예로는 toList()
, sum()
등이 있다. 그리고 연산 실행 순서에서도 차이가 난다. Sequence는 각 요소에 대해서 전체 프로세스를 실행하고, Iterable
은 각 프로세스를 전체 컬렉션에 대해 실행하고 그 다음 단계를 차례로 수행한다.
중간 결과 컬렉션을 생성하지 않음으로써 컬렉션 처리의 성능을 향상할 수 있지만, 지연 연산에 오버헤드가 따르기 때문에 컬렉션 크기가 작거나 간단한 연산에 대해서는 큰 효과를 보기 어려울 수 있다. 그래서 Sequence
나 Iterable
중 무엇이 내 상황에서 더 좋을지 고려해봐야 한다.
Sequence 생성 방법
요소들에서 생성하는 방법
리스트를 생성하는 것과 동일하게 요소들을 sequenceOf()
의 인자로 넣어주면 생성할 수 있다.
val korSequence = sequenceOf("가", "나", "다", "라")
리스트에서 생성하는 방법
이미 Iterable
(List나 Set 등)이 있다면 asSequence()
를 호출해서 생성할 수 있다.
val kors = listOf("가", "나", "다", "라")
val korSequence = kors.asSequence()
함수(람다)를 통해 생성하는 방법
요소를 생성하는 함수(람다)를 이용해서 시퀀스를 생성하는 방법도 있다. 이 때는 generateSequence()
함수의 인자로 초기값, 생성함수를 넣어준다. 시퀀스는 생성 함수가 null을 리턴하는 경우 요소 생성이 멈춘다. 그래서 아래 예제처럼 무한으로 요소가 생성되는 시퀀스를 만드는 것도 가능하다.
val oddNumbers = generateSequence(1) { it + 2 } // 1은 초기값, it은 이전 요소를 나타냄
println(oddNumbers.take(5).toList()) // [1, 3, 5, 7, 9]
// println(oddNumbers.count()) // 무한 시퀀스라서 에러 발생
청크에서 생성하는 방법
마지막으로 yield()
나 yieldAll()
함수 호출을 포함하는 람다식을 인자로 받아서 요소를 하나씩 만들거나 또는 임의의 개수만큼 만드는 sequence()
함수를 사용하는 방법이 있다. 이 함수는 시퀀스 소비자가 다음 요소를 호출할 때까지 sequence()
함수의 호출을 미룬다. yield()
는 인자로 요소 하나를 받고, yieldAll()
은 Iterable
이나 Sequence
를 인자로 받을 수 있다. yieldAll()
의 인자가 Sequence
인 경우 그 뒤의 값들은 호출이 안될 것이기 때문에 람다식 가장 마지막에 와야 한다.
val oddNumbers = sequence {
yield(1)
yieldAll(listOf(3, 5))
yieldAll(generateSequence(7) { it + 2 })
}
println(oddNumbers.take(5).toList())
Sequence와 Iterable 예제
단어 리스트가 주어졌을 때 단어가 3자 초과인 것들만 필터링하고 첫 네개 요소들의 글자수만 프린트 하는 예제로 동작 차이를 살펴 본다.
Iterable
예제 코드를 실행해보면 filter()
, map
가 코드상 순서와 동일하게 호출됨(eagerly)을 확인할 수 있다.
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)
Sequence
시퀀스 코드의 결과는 최종 연산(toList()
)가 호출되기 전까지 실행이 지연되기 때문에 "Lengths of.."라는 문장이 호출되고 난 뒤에 filter()
와 map()
연산 결과가 출력된다. 시퀀스는 필터링 후 맵 연산을 수행하고 시퀀스 결과 개수가 4개가 됐을 때 처리가 중단된다.
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())
처리 순서를 도식화하면 다음과 같다. 같은 결과에 대해서 리스트는 총 23번 스텝을 동작하고 시퀀스는 총 18번 스텝을 동작한다.
참조
'Tech' 카테고리의 다른 글
ChatGPT 무료 세미나 & 리소스 (2) | 2023.05.08 |
---|---|
ChatGPT로 인한 새로운 패러다임 세미나 참석 후기 (0) | 2023.05.03 |
Google Bard 사용기 & ChatGPT 결과와 비교 (0) | 2023.04.26 |
Spring Webflux에서 Mono.zip() 사용시 주의사항 (ReactorRejectedExecutionException) (0) | 2023.04.25 |
Microsoft + OpenAI Conference 온라인 참석 후기 (0) | 2023.04.04 |