Kotlin의 Sequence에 대해서 알아보려고 한다. 사실, [Kotlin in action] 책에서 설명이 나오지만, 당시에는 대략적으로만 이해하고 넘어갔던 것 같다. 하지만, 최근에 Collection과 Sequence의 차이를 물어보는 질문에 대해서 제대로 답하지 못했던 것 같아서 그 차이를 알기 위해 정리하려 한다.

Sequence vs Collection

[Sequence]

  • kotlin standard library에는 collection 뿐 아니라 또다른 container type인 sequence를 가지고 있다.
  • sequence의 여러 연산 처리는 전체 연산이 처리된 결과가 요구되었을 때, 실제 연산이 일어나며 느리게(나중에) 처리된다.(lazy evaluation)
  • 동작의 순서 또한 다르다. sequence는 각각 하나의 element에 대해 모든 연산을 수행한다.

[Iterable]

  • 여러 연산을 포함하는 Iterable을 수행할 때는 각 단계(연산)의 처리는 바로 끝나고 즉시 그 결과를 반환한다.
  • Iterable은 전체 Collection에 대해 각 연산의 수행을 완료하고 다음 단계로 넘어간다.

[Sample Code]

  • 간단한 예제 코드와 함께 이해해보자.
  • 먼저, Collection의 경우이다.
1
2
3
4
5
val list = listOf(1,2,-3) // [1,2,-3] 생성
val maxOddSquare = list
.map { it*it } // [1,4,9] 생성
.filter { it%2 == 1} // [1,9] 생성
.max()
  • map() 연산이 수행될 때, filter() 연산이 수행될 때 총 3개의 intermediate collection(중간 컬렉션 결과값)이 생성된다.
  • 원하는 최종 결과를 위해 불필요한 중간 결과가 생성된다는 비효율성이 존재한다.
  • 1개의 연산만을 진행할 경우, 바로 최종 결과물에 도달하므로 문제가 없지만 chain calls 연산을 진행하는 경우 유의미한 퍼포먼스 오버헤드가 생길 수 있다.
  • Collection을 다룰 때는 보통 1개의 함수로 처리하기 보다는 다양한 확장함수를 이용한 chain calls 패턴이 대부분이기 때문에 이 이슈가 중요할 수 있으며 문제를 피하는 방법이 sequence이다.
  • 아래는 sequence의 경우이다.
1
2
3
4
5
6
val list = listOf(1,2,-3) // [1,2,-3] 생성
val maxOddSquare = list
.asSequence()
.map { it*it }
.filter { it%2 == 1}
.max()
  • asSequence()를 통해 sequence로 변환해준다.
  • sequence를 사용하는 경우, 중간 과정에서 intermediate collection(중간 컬렉션 결과값)이 반환되지 않으며, first collection에 대한 reference와 어떠한 연산이 수행되는지를 저장해둔 sequecne object가 반환된다.
  • 그리고 결과가 필요한 시점에만 연산을 수행(lazy evaluation)하여 최종 결과만을 반환한다.
  • 이러한 방식을 통해 chain calls에 대한 퍼포먼스 오버헤드를 막을 수 있다.

따라서 sequence는 중간 단계의 결과에 대한 처리를 피할 수 있게 해주며, Collection 전체 처리에 대한 수행 성능이 향상된다. 하지만 크기가 작은 Collection이나 단순한 연산 동작에 대해서는 오히려 불필요한 오버헤드가 생길 수도 있다. 그러므로 어느 경우에 sequence와 Iterable이 나을지 적절하게 선택해야 한다.

Sequence

[From elements]

  • sequenceOf() 함수를 사용하여 원소를 포함하는 sequence를 생성할 수 있다.
1
val numbersSequence = sequenceOf("four", "three", "two", "one")

[From Iterable]

  • asSequence()를 사용하면 List, Set 같은 Iterable Object로부터 Sequence를 생성할 수 있다.
1
val numbers = listOf("one", "two", "three", "four").asSequence()

[From function]

  • generateSequence()를 사용하여 element를 생성하는 함수(람다)를 사용해 sequence를 생성할 수 있다.
  • 첫번째 element를 특정 값으로 지정하거나 함수 호출의 결과를 지정하는 것도 가능하다.
  • 함수가 null을 반환하면 sequence 생성을 멈추게 된다.
1
2
3
4
// 무한히 생성되는 오류를 가진 코드이다.
val oddNumbers = generateSequence(1) { it + 2 }
println(oddNumbers.take(5).toList()) // [1,3,5,7,9] 생성
println(oddNumbers.count()) // error : the sequence is infinite
  • generateSequence() 함수로 유한한 sequence를 만들기 위해서는 마지막 element 뒤에 null을 반환하는 함수를 제공해야 한다.
1
2
3
val oddNumbers = generateSequence(1) { if (it < 10) it + 2 else null }
println(oddNumbers.take(5).toList()) // [1,3,5,7,9] 생성
println(oddNumbers.count()) // 6

sequence processing example

[Iterable]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
val words = "The quick brown fox jumps over the lazy dog".split(" ")
val list =
words.filter { println("filter : $it"); it.length > 3 }
.map { println("length : ${it.length}"); it.length }
.take(4)
println(list)

// Result
filter : The
filter : quick
filter : brown
filter : fox
filter : jumps
filter : over
filter : the
filter : lazy
filter : dog
length : 5
length : 5
length : 5
length : 4
length : 4
[5, 5, 5, 4]
  • filter()에서 모든 element에 대해 동작이 수행되고 나서 map()에서 filter()의 결과로 리턴된 list(elements)에 대해 연산을 수행한다.

[Sequence]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
val words = "The quick brown fox jumps over the lazy dog".split(" ")
val sequence = words.asSequence()
.filter { println("filter : $it"); it.length > 3 }
.map { println("length : ${it.length}"); it.length }
.take(4)

println(sequence.toList())

// Result
filter : The
filter : quick
length : 5
filter : brown
length : 5
filter : fox
filter : jumps
length : 5
filter : over
length : 4
[5, 5, 5, 4]
  • print문이 먼저 호출된 것을 통해 filter()와 map()이 실제 필요로 하는 때에 호출되는 것을 볼 수 있다.
  • 또한, map()filter()가 element를 반환하자마자 바로 실행되는 것을 볼 수 있다.
  • 그리고 take()에 의해 최종 결과의 갯수가 4개를 만족하면 나머지 수행은 멈추고 모든 동작이 마무리 된다. 이때, 뒤의 단어에 대한 연산은 수행되지 않음을 확인할 수 있다.
  • 위의 예제에서 같은 크기의 list에 대해 Iterable은 23단계를 수행하지만 sequence는 18단계만을 수행하는 것을 확인할 수 있다.

References