[Java] Stream은 왜 존재할까? (feat.코테에서 Stream 쓰지 마세요)

김태훈·2023년 11월 26일
0
post-thumbnail

배열에서 값을 꺼내기 위해 사용했던 for문..
하지만 이를 대체하기 위한 stream이 등장했었다.
그렇다면 지금 stream이 왜 쓰이고, for문보다 나은 점이 대체 무엇일까?
책 '자바의 정석'을 참고하며 나의 생각들을 정리하였다.

1. for / iterator 의 문제점

  1. 코드가 너무 긺
  2. 재사용성 하락
  3. 데이터마다 다른방식을 사용 ( List 정렬시 Collections.sort()를, Array 정렬시 Arrays.sort() 를...)

2. 문제를 해결하기 위한 Stream의 등장

  1. 코드가 너무 긺
    -> Stream 사용하면서 기하급수적으로 짧아짐
  2. 재사용성 하락, 3.데이터마다 다른방식을 사용
    -> 데이터마다 다르게 사용되던 방식을 통일시키기 위해 스트림을 사용해 데이터 소스를 추상화함

예시코드

참고 공식 문서 : https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html

 int sum = widgets.stream()
 	.filter(w -> w.getColor() == RED)
    .mapToInt(w -> w.getWeight())
    .sum();

위 코드는 어떤 코드인지 한눈에 읽힌다.
1. widgets이라는 stream object를 Collection.stream()으로 만들고,
2. 각 원소마다, RED color만 추출한 후에,
3. RED color의 weight를 int로 변환하고
4. 모든 weight의 합을 구하여 sum에 저장한다.
이렇게 코드가 무엇을 하려는 지 읽기도 쉽고, 자칫 굉장히 길게 만들어질 수 있는 코드를 단축시킨다.

3. Stream의 특징

(1) 데이터 소스를 변경하지 않는다.

데이터 소스로부터 '읽기'만 할 뿐, 변경하지 않는다. 다만, 필요하다면 다음과 같이 저장할 순 있겠다.

List<String> sortedList = strStream.sorted().collect(Collectors.toList());

(2) 일회용이다. (다시 사용 불가능!)

생성된 Stream으로 뭔 짓을 하면, 다시 사용할 수 없다.

Stream<String> strStream = strList.stream();

strStream.sorted().forEach(System.out::println);
int numOfStr = strStream.count(); //이것이 불가능하다.

(3) 작업을 내부 반복 연산으로 처리한다. feat. 병렬

스트림이 가져다 주는 간결성은 내부에서 반복적으로 처리해주는 작업 때문이다.
특히 Stream에는 forEach 가 굉장히 많이 사용된다.
이는 스트림 내에 구현되어 있고, 파라미터로 대입된 람다식을 stream의 모든 원소에 적용시킨다.
공식문서에 이런 설명이 있다.

The behavior of this operation is explicitly nondeterministic. For parallel stream pipelines, this operation does not guarantee to respect the encounter order of the stream, as doing so would sacrifice the benefit of parallelism.

Stream은 병렬적으로 이루어진다는 내용이다. 즉, 각 원소마다 parallel하게 pipeline(연쇄 작업) 들이 이루어지고, 이에 따라 작업 순서를 보장하지 않는다.
연쇄작업을 거치는 동안, 연산 결과를 계속해서 stream으로 반환하고, 이를 이용해서 원하는 값을 도출할 수 있다.

(4) 지연된 연산

반복적인 연산을 거치면서, 중간연산을 호출해도, 즉각적으로 연산이 이루어지지 않는다. 중간 연산은 최종연산이 수행되어야 비로소 수행된다.

(5) Stream<T> 와 IntStream (LongStream...)

요소의 타입이 T이면 보통 Stream<T> 를 사용하지만, 오토박싱과 언박싱으로 인한 비효율을 줄이기 위해 prmitive type을 다루는 Stream인 IntStream, LongStream, DoubleStream이 추가되었다.

4. 하지만 이슈는 있다.

속도가 느리다.

Collection의 원소에 접근해야하는 횟수가 많아지게 되면, Array나 Collection이 좋다고 한다.
이는 코딩테스트에서도 확인할 수 있었다.
https://school.programmers.co.kr/learn/courses/30/lessons/136798
이 문제를 푸는데, 나는 다음과 같은 코드를 제출했었다.

import java.util.stream.*;
class Solution {
    public int solution(int number, int limit, int power) {
        int answer = 0;
        int[] numbers = IntStream.rangeClosed(1,number).toArray();
        for(int n:numbers){
            int cnt = getCntDivisor(n) ;
            if (cnt> limit){
                answer += power;
            }
            else{
                answer += cnt;
            }
                
        }
        return answer;
    }
    
    public static int getCntDivisor(int n){
        int cnt = 0;
        for (int i = 1; i <= (int)Math.sqrt(n); i++){
            if (n % i == 0) {
                if (n/i ==i) cnt ++;
                else cnt += 2;
            }
        }
        return cnt;
    }
}


대충 이런 시간이 걸렸다.

하지만 Stream이 아닌 Array를 사용한다면..?

전반적으로 빠른 속도로 끝내는 것을 볼 수 있다.
그러니 코딩테스트에 굳이 Stream을 사용해 시간적 효율을 줄이지 말자.
물론, 각 원소에서 해야할 작업이 많다면 Stream을 고려해볼만 하지만, 코딩테스트의 경우 그럴 경우는 거의 없다.

참고할만한 블로그 : https://brorica.tistory.com/entry/java-stream

profile
기록하고, 공유합시다

0개의 댓글