Kotiln Sequence vs Java Stream

허준현·2023년 4월 21일
0

JAVA

목록 보기
1/5
post-thumbnail

앞에서 자바8에 대해서 Stream에 대해서 간략하게 알아보았다. 회사 면접을 보던 중 Java StreamKotiln Stream의 차이에 대해서 아는지 물어보았고 해당 사항에 대해서 답변을 하지 못해 정리해보고자 한다.

Java Stream, Kotlin Sequence , Stream, Collection

저번 시간에는 Java Stream에 대해 알아보았고 이번에 Kotlin의 Sequence, Stream, Collection 과 비교를 해볼 것이다. 먼저 Java Stream, kotlin Sequence, Stream은 Lazy Evaluation 으로 작동하고 Kotlin Collection 은 Eager Evaluation 으로 작동하게 된다.

Java Stream 와 kotlin의 Sequence의 차이점이 살짝 있는데 Sequence는 더 많은 유용한 함수를 제공하며 Sequence는 다양한 플랫폼에서 동작하지만 Java Stream은 Kotlin/JVM 위에서만 동작한다.

추가적으로 Kotiln 에서 Sequence, Stream 은 둘다 Eager Evaluation으로 작동하는 것은 동일하지만 Sequence 의 경우 별도의 builder sequence 를 사용하여 코루틴에서 간단한 순차 구문을 사용할 수 있다는 점과 좀 더 간결한 집계함수를 제공한다.

repository.findAll().map { it.id to it }.toMap() 
=> repository.findAll().associateBy { it.id } //간단한 구문 사용하기

Stream은 자바와 동일하게 ParallelStream을 제공하여 멀티 쓰레드를 포크조인풀에서 가져와 빠른 속도를 제공한다. 단점으로는 만일 포크조인풀에 쓰레드들이 block되면 제 기능을 수행하지 못한다는 것이다.

Lazy Evaluation

Lazy는 게으르다는 뜻으로 쉽게 말해서 필요하지 않은 연산을 하지 않으며 실제로 사용되기 전까지 미루는 속성이 있다. 다음 코드를 보면서 이해하도록 하자.

Stream.of(1, 2, 3, 4, 5, 6)
        .filter(e -> {
            System.out.println("filter: " + e);
            return e < 3;
        })
        .map(e -> {
            System.out.println("map: " + e);
            return e * e;
        })
        .anyMatch(e -> {
            System.out.println("anyMatch: " + e);
            return e > 2;
        });
//filter: 1
//map: 1
//anyMatch: 1
//filter: 2
//map: 2
//anyMatch: 4

총 6개의 숫자에 대해서 적용이 되야 할 것이 실제로 연산된 곳은 2개 뿐만 인것을 확인할 수 있다. 또 한 Lazy 는 종료함수가 호출되기 전까지 연산을 수행하지 않는다. 즉 종료함수가 없는 경우에는 실행되지 않는다.

Eager Evaluation

val list: List<Int> = listOf(1, 2, 3)
    .filter {
        println("filter: $it")
        it < 2
    }
    .map {
        println("map: $it")
        it * it
    }
//filter: 1
//filter: 2
//filter: 3

위의 코드에서 확인 할 수 있듯이 filter 에서 List 원소들이 걸리는 것을 확인할 수 있다.
이 처럼 Lazy 와 원소 순환에 차이 또한 있으며 collection은 각각의 단계에서 컬렉션을 만들어낸다. 이는 Iterable 객체에도 동일하게 작동한다.

numbers.filter{ ...} //collection 생성
	   .map{ ...}	// collection 생성
       .sum() 

Lazy Evaluation은 항상 좋을까?

So, the sequences let you avoid building results of intermediate steps, therefore improving the performance of the whole collection processing chain. However, the lazy nature of sequences adds some overhead which may be significant when processing smaller collections or doing simpler computations. Hence, you should consider both Sequence and Iterable and decide which one is better for your case.

코틀린 공식 문서에서 확인해보면 다음과 같다. 요약하자면 Sequences 는 중간 단계의 결과를 생략하기 때문에 성능 향상이 되지만, 오버헤드가 있기 때문에 데이터가 적거나 연산이 단순한 컬렉션을 처리할 때는 오히려 안좋을 수다는 것이다.
여기서 말하는 오버헤드는 시퀀스를 실행할 떄 새로운 (익명)객체를 생성하는 것을 말한다.

따라서 각자 자신의 프로젝트에서 사용되는 언어에 해당하는 객체나 Stream의 크기가 크거나 로직이 복잡하여 오래걸리는 경우에 이런 기능이 있었으니 한 번 테스트 해보고 수정해보는 시간을 가지고 팀 내 컴벤션에 따라서 for-loop 문을 작성하는 것도 방법이다.

추가적으로.. JavaScrpit 객체 순환

추가적으로 현재 회사 내에서 react 토이 프로젝트를 진행하면서 자바스크립트를 자주 사용하는 기회가 많았다. filter, map과 같은 메소드를 지원하기에 해당 경우에도 Lazy Evaluation을 지원하는 경우가 있는지 알아보았다.

const arr = Array(100000).fill()
const result = arr.map(num => num + 10).filter(num => num % 2).slice(0, 10)

해당 코드 같은 경우에는 100000 숫자를 배열에 채우고 1씩 더하고 나머지가 3인 것에 대해 10개씩 나누는 코드이다.
해당 경우는 모두 실행하는 Eager Evaluation 로 실행되어 200010번 실행하게 되나 10개의 숫자만 가져오는 경우에는 위에서 30개의 숫자만 뽑고 실행하게 된다면 기존의 실행 속도보다 빠르게 진행 할 수 있게 된다.

그러면 위에서 설명 한 것처럼 하나 하나 순환하는 경우로 JavaScrpit는 어떻게 작성할까?

_.filter = function* (f, iter) {
  for (let item of iter) {
    if(f(item)) yield item
  }
}

일부 코드를 가져왔는데 위의 코드에서 yield 키워드를 통해서 지연 연산을 제공한다는 것만 알아두면 좋을 것 같다. 이처럼 직접 코드를 작성하는 방법 이외에 라이브러리 Lodash 를 사용해서 유연하게 코드를 작성하는 것도 하나의 방법이다.

const arr = [0, 1, 2, 3, 4, 5]
const result = _.chain(arr)
  .map(num => num + 10)
  .filter(num => num % 2)
  .take(2)
  .value()
console.log(result) // [11, 13]

참고

Kotlin Collections, Sequence
javascript

profile
best of best

0개의 댓글