Kotlin - Collections와 Sequences의 차이점

JS · 07 Jul 2020

Kotlin에서 Iterator, Collection, List, Map, Set를 Collections이라고 합니다. Collections와 Sequences에 대해서 소개하기 전에, Java의 Collections와 Stream에 대해서 먼저 소개하겠습니다.

Java의 Collections는 함수형 함수를 제공해주지 않지만 Stream으로 map() 등의 함수형 함수(Functional functions)를 사용할 수 있습니다. Stream은 Lazy evaluation으로 동작합니다. Lazy evaluation의 의미는 가능한 코드를 수행을 미루고, 어쩔 수 없이 연산이 필요한 순간에 연산을 수행하는 방식을 말합니다.

Kotlin의 Collections는 Stream을 사용하지 않아도 언어에서 Functional functions를 제공해 줍니다. 다른 API의 도움 없이 map() 등을 사용할 수 있습니다. 하지만 Kotlin의 Collections는 Eager evaluation으로 동작합니다. Eager evaluation는 수행해야 할 연산이 있으면 미루지 않고 바로 처리하는 방식을 말합니다.

따라서, Kotlin의 Collection은 Java의 Stream과 결과는 동일하게 출력되지만 처리하는 과정이 다를 수 있습니다. 이 둘은 경우에 따라서 성능 차이가 발생할 수도 있습니다. Lazy evaluation는 필요하지 않으면 연산을 수행하지 않기 때문에 더 적은 연산으로 동일한 결과를 얻을 수 있기 때문입니다.

Kotlin의 Sequences는 Java의 Stream처럼 Lazy evaluation으로 동작합니다. 따라서, Collections와 Sequences는 동일한 값을 출력할 수 있지만 수행하는 과정이 다릅니다.

List의 개수가 매우 적다면 Collections가 빠를 수 있지만, List의 개수가 많다면 Sequences가 성능 측면에서 유리할 수 있습니다.

Kotlin Collections과 Java Stream의 차이점

함수형 프로그래밍 관점에서 Kotlin의 Collections는 Java의 Stream과 비슷합니다. 이 둘의 차이점을 살펴보겠습니다.

Java Stream

List를 예로 들면, Java는 기본적으로 List가 함수형 함수(Functional functions)를 제공하지 않습니다. 대신 Stream을 사용하면 map(), collect()를 사용할 수 있습니다.

다음은 Java의 Stream을 사용하는 예제입니다.

List<String> fruits = Arrays.asList("apple", "banana", "kiwi", "cherry");
fruits.stream()
      .filter(name -> {
          System.out.println("checking the length of " + name);
          return name.length() > 5;
      })
      .map(name -> {
          System.out.println("mapping to the length of " + name);
          return "" + name.length();
      })
      .collect(Collectors.toList());

수행 결과를 보면, 4개의 아이템에 대해서 filter()를 모두 수행하고 map()을 수행하지 않습니다. filter() -> map() 순서로 동작하며 filter()에서 누락된 아이템은 map()이 호출되지 않습니다.

checking the length of apple
checking the length of banana
mapping to the length of banana
checking the length of kiwi
checking the length of cherry
mapping to the length of cherry

Kotlin Stream

반면에 kotlin은 기본적으로 List가 함수형 함수를 제공합니다. Stream을 사용하지 않아도 map() 등의 함수를 사용할 수 있습니다.

다음은 코틀린을 사용하여 위와 동일한 구현을 하였습니다.

val fruits = listOf("apple", "banana", "kiwi", "cherry")

fruits.filter {
    println("checking the length of $it")
    it.length > 5
}
.map {
    println("mapping to the length of $it")
    "${it.length}"
}
.toList()

수행 결과를 보시면, 모든 아이템에 대해서 filter()를 수행하고, 그 다음에 filter()를 통과한 아이템에 대해서만 map()을 수행합니다.

checking the length of apple
checking the length of banana
checking the length of kiwi
checking the length of cherry
mapping to the length of banana
mapping to the length of cherry

따라서 위의 예제로 Java의 Stream은 Lazy evaluation으로 동작하고, kotlin의 Collections는 Eager evaluation으로 동작한다는 것을 볼 수 있었습니다.

Kotlin에서 Stream 사용하기

이 글의 주제와 벗어나지만, Kotlin Collections를 Stream으로 사용하는 예제를 소개합니다.

아래와 같이 stream()으로 Stream으로 변환할 수 있습니다.

val fruits = listOf("apple", "banana", "kiwi", "cherry")

fruits.stream().filter {
          println("checking the length of $it")
          it.length > 5
      }
      .map {
          println("mapping to the length of $it")
          "${it.length}"
      }
      .collect(Collectors.toList())

위의 Kotlin 코드를 Java로 변환해보면 아래와 같이 Stream을 사용하는 것을 볼 수 있습니다.

List fruits = CollectionsKt.listOf(
    new String[]{"apple", "banana", "kiwi", "cherry"});

fruits.stream()
      .filter((Predicate)null.INSTANCE)
      .map((Function)null.INSTANCE)
      .collect(Collectors.toList());

위 코틀린 코드를 실행해보면, Java의 Stream처럼 Lazy evaluation으로 동작한다는 것을 알 수 있습니다.

checking the length of apple
checking the length of banana
mapping to the length of banana
checking the length of kiwi
checking the length of cherry
mapping to the length of cherry

Sequences로 연산 수행(Lazy evaluation)

asSequence()으로 Collections를 Sequences로 변환할 수 있습니다. Sequences도 Collections처럼 map() 등의 functional functions를 제공합니다.

다음 코드는 위의 예제와 동일한 리스트를 리턴합니다.

val fruits = listOf("apple", "banana", "kiwi", "cherry")
fruits.asSequence()
    .filter {
        println("checking the length of $it")
        it.length > 5
    }
    .map {
        println("mapping to the length of $it")
        "${it.length}"
    }
    .toList()

결과를 보면 Java의 Stream처럼 Lazy evaluation으로 처리됩니다.

checking the length of apple
checking the length of banana
mapping to the length of banana
checking the length of kiwi
checking the length of cherry
mapping to the length of cherry

다른 예제

kotlinlang.org에 좋은 예제가 있습니다.

연산 과정을 보여주는 다이어그램도 함께 첨부되어있어서 성능 측면에서 Sequences가 좋은 부분을 이해할 수 있습니다.

Collections 예제

다음은 단어가 저장된 words 리스트에 대해서 filter()map(), take()를 적용하는 코드입니다.

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)

결과를 출력하면 다음과 같이 Eager evaluation으로 동작합니다.

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
Lengths of first 4 words longer than 3 chars:
[5, 5, 5, 4]

다이어그램으로 보면 아래와 같은 순서로 처리됩니다. iterable processing

Sequences 예제

위의 예제와 동일한 내용을 Sequences로 구현한 코드입니다.

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())

결과를 보면 다음과 같이 Lazy evaluation으로 동작합니다.

Lengths of first 4 words longer than 3 chars
filter: The
filter: quick
length: 5
filter: brown
length: 5
filter: fox
filter: jumps
length: 5
filter: over
length: 4
[5, 5, 5, 4]

다이어그램으로 보면 아래와 같은 순서로 처리됩니다. sequences processing

take(4)로 4개의 아이템만 가져오면 되기 때문에, 다른 아이템에 대해서 filter()를 수행할 필요가 없습니다. 그래서 Collections 코드보다 연산이 빨리 끝날 수 있습니다.

정리

Sequences는 Lazy evaluation으로 연산을 수행하기 때문에 Collections보다 적은 연산으로 동일한 결과를 출력할 수 있습니다. 하지만 리스트의 개수가 적은 경우 Collections가 좋을 수 있습니다. 규모가 어느정도 있다고 생각이 되면 Sequences를 고려해보는 것이 좋을 것 같습니다.

참고

댓글을 보거나 쓰려면 이 버튼을 눌러주세요.
codechachaCopyright ©2019 codechacha