[Effective Kotlin] Chapter 8 | 아이템 49: 하나 이상의 처리 단계를 가진 경우에는 Sequence를 사용하라

박희중·2023년 5월 30일
2

Kotlin

목록 보기
3/4
post-thumbnail

Chapter 8: Efficient collection processing

  • 컬렉션은 프로그래밍에서 가장 중요한 개념 중 하나이다.

  • 컬렉션이 없는 어플리케이션을 상상하는 건 어렵고 프로그래밍에서 어디에나 존재한다.

  • 코틀린을 포함한 대부분의 (함수형 프로그래밍 언어같은) modern 언어들은 컬렉션 처리를 위한 강력한 도구들을 가지고 있다.

  • Before
  • After

코드를 보면 짧아질 뿐만 아니라, readable 측면에서도 향상된다. 둘의 성능은 비슷하다.
다양한 다른 방식으로 같은 processing을 할 수 있다.


하지만 아래 방식은 성능 차이가 발생한다. (Sequence 이므로)

  • Before
  • After

이럿듯 컬렉션 처리 최적화는 매우 중요하고 컬렉션 처리 최적화는 몇몇 rule을 기억하면 된다.
다.

Sequence를 사용함으로써 성능 차이가 발생한 이유에 대해 알아보자.



Item 51: Prefer Sequences for big collections with more than one processing step

[하나 이상의 처리 단계를 가진 큰 컬렉션은 sequence를 선호해라]


'Iterable'과 'Sequence"는 완전히 다른 usages와 contracts를 가지고 있고 대부분 처리 함수는 다르게 동작한다.



컬렉션과 시퀀스

  • Kotlin Collection
    -> Eager Evaluation (연산을 미루지 않고 바로 처리하는 방식)

  • Kotlin Sequence
    -> Lazy Evaluation (게으른 연산, 연산을 최대한 미루고 연산이 필요한 순간에 수행하는 방식)

Collection 처리 연산은 사용될 때 바로 실행되지만

Sequenceterminal operation 전에는 실행되지 않고 terminal operation 실행 시점에 모든 연산들이 수행된다.
( terminal operation: sequence와는 다른 것을 마지막에 반환하는 연산, ex) toList(), count() )



Kotlin Collection 방식 (Eager Evaluation)

컬렉션 방식은 출력문(주석 내용)을 보면 동작 방식을 알 수 있다.
첫번째 연산인 filter를 모든 컬렉션에 실행시키고 다음 연산인 map으로 이동하여 동작하는 모습을 볼 수 있다.


Kotlin Sequence 방식 (Lazy Evaluation)

시퀀스 방식 또한 출력문을 보면 동작 방식을 알 수 있다.
컬렉션 방식과는 달리 시퀀스는 첫번째 요소인 "The"를 모든 연산(filter, map, take) 까지 실행 후 다음 요소("quick")가 모든 연산을 거치고 또 다음 요소("brown")를 실행시키는 방식이다.

그림 출처: Kotlin Docs



Order is important

[순서는 중요하다]

위에서 봤듯이 iterable과 sequence 처리 중 무엇을 실행하는 지에 따라 연산 순서가 다르다.

  • Sequence processing
    첫번째 요소를 모든 연산까지 실행 후 다음 요소를 실행시킴
    -> element-by-element 또는 lazy order라 부른다.

  • Iterable processing
    첫번째 연산을 모든 컬렉션에 실행시키고 다음 연산으로 이동함
    -> step-by-step 또는 eager order라 부른다.


일반적인 loops와 조건문도 sequence processing과 비슷하게 element-by-element를 가진다.
-> 따라서 sequence processing에서 쓰이는 element-by-element 순서가 더 자연스럽다.

또한 sequence processing은 basic loops와 condition과 최적화될 수 있으므로 low-level 컴파일러 최적화에 대한 문이 열려있다.



Sequences do the minimal number of operations

[Sequence는 최소한의 연산을 수행한다]

sequence는 중간연산(intermediate operation)이라는 개념을 갖고 있고 최소 연산을 할 수 있다.

앞서 위의 Sequence 예시에서 봤듯이 .take(4) (terminal operation)에서 4개를 가져왔다면 그 다음 element로 넘어가지 않고 거기서 연산을 끝낸다.

그래서 필요한 만큼 (최소한으로) 연산을 수행할 수 있다.

이러한 이유로 연산을 모든 요소에 반복할 필요가 없는 경우 sequece를 사용하는 것이 처리 성능에 있어서 더 좋다.

모든 요소를 처리할 필요가 없는 연산 예시로는 first, find, take, any, all, none, indexOf 가 있다.



Sequence can be infinite

[Sequence는 무한할 수 있다]

Sequence는 종결 연산(terminal operation)이 일어나기 전에는 컬렉션에 어떠한 처리도 하지 않는다.
따라서 무한 sequence를 만들어, 필요한 부분까지만 값을 추출하는 방법도 가능하다.

  • 무한 sequence 예시

take와 같은 종결 연산을 사용하지 않으면 무한하게 반복된다.
따라서 무한 sequence를 사용하는 경우 take, first, find, indexOf 같은 종결 연산을 사용해 sequence를 제한해야한다.



Sequences do not create collections at every processing step

[Sequence는 각각의 처리 단계에서 컬렉션을 만들어내지 않는다]

표준 컬렉션 처리 함수들은 모든 단계에서 새로운 컬렉션을 만들어낸다.
각각의 단계에서 만들어진 값을 활용하는 것은 컬렉션의 장점이지만 그만큼 cost가 든다.
컬렉션은 매번 단계마다 데이터를 새로 만든다.

특히 무거운 컬렉션을 처리할 때 굉장히 큰 비용이 들어가지 때문에, 기본적으로 파일을 처리할 때는 sequence를 사용하는 것이 좋다.

  • 예시

또한 하나 이상의 처리 단계를 포함하는 경우 sequence 사용시 컬렉션 처리 대비 20~40% 정도의 성능 향상이 보인다.



When aren't sequences faster?

컬렉션 전체를 기반으로 처리해야하는 연산은 sequence를 사용해도 빨라지지 않는다.

  • kotlin stdlib의 sorted
    sorted는 Sequence를 List로 변환한 다음 stdlib의 sort를 이용해 처리한다.
    이러한 변환때문에 Sequence가 Collection 처리보다 느려진다. (큰 차이는 아니다)

    무한 sequence처럼 sequence의 다음 요소를 lazy하게 구하는 sequence에 sorted를 적용하면 무한 반복에 빠지므로 주의해야한다.
    sorted는 Sequence보다 Collection이 더 빠른 희귀한 예 중 하나이다.
    다른 연산 처리는 모드 Sequence가 빠르기 때문에 여러 연산 처리가 결합된 경우라면 sorted가 포함되어 있더라도 Sequence를 사용하는 것이 더 빠르다. (Sequence를 사용함으로써 발생하는 sort의 성능 저하는 미비한 정도이므로)



What about Java streams?

자바 8부터는 컬렉션 처리를 위해 스트림 기능이 추가되었고
이는 kotlin의 Sequence와 유사하게 lazy로 동작한다.

다만 java의 stream과 kotlin의 sequence는 세 가지 큰 차이점을 가진다.

  1. kotlin의 sequence가 더 많은 처리 함수를 갖고 있으며 사용하기 더 쉽다.
    (함수형 프로그래밍 언어의 이점)

  2. 자바 stream processing은 병렬 함수를 사용하면서 병렬 모드로 실행할 수 있다.
    이는 멀티 코어 환경에서 큰 성능 향상을 가져온다. (몇 가지 결함이 있으니 주의)

  3. kotlin의 sequence는 kotlin/JVM, kotlin/JS, kotlin/Native 등의 일반적인 모듈에서 모두 사용할 수 있지만
    java의 stream은 kotlin/JVM, 그리고 JVM 8 이상에서만 동작한다.

따라서 병렬 모드로 성능적인 큰 이득을 얻을 수 있는 것이 아니라면, 코틀린 sequence를 사용하는 것이 좋다.



Kotlin Sequence debugging

kotlin sequence나 java stream은 단계 요소 흐름을 추적할 수 있는 디버깅 기능을 지원한다.
아래 플러그인을 이용해 활용할 수 있다.

  • Kotlin Sequence Debugger
  • Java Stream Debugger



Summary

Sequence는 lazy하게 처리되며 아래와 같은 장점이 있다.

  • 자연스러운 처리 순서 (element-by-element)
  • 최소한으로 연산
  • 무한 sequence로 사용 가능
  • 각각의 단계에서 컬렉션을 만들어내지 않는다.




책 내용뿐만 아니라 개인적인 의견 및 코드도 포함되어있으므로 참고부탁드립니다.


참고: 책 Effective Kotlin - Marcin Moskala
Kotlin Docs

profile
백엔드 엔지니어 박희중입니다.

0개의 댓글