티스토리 뷰

개발/JAVA

[JAVA] Stream

애쿠 2023. 1. 26. 01:12

1. Stream이란?

JAVA 8에서 함수형 인터페이스, 람다를 활용할 수 있는 기술 중 하나다. 배열과 컬렉션의 변환, 필터링, 정렬 등을 간소화된 코드로 작성할 수 있게 해준다.

 

일련의 과정을 하나의 스트림 파이프라인으로 구현해, 코드의 가독성 또한 높일 수 있다는 장점이 있다. 다만, 자바 인 액션의 말을 빌리면 stream의 비용은 비싸다고 한다.

 

또한 parallelStream으로 쓰레드를 이용한 병렬처리도 제공한다.

 

2. Stream 생성

stream 자체 객체도 있지만, 일반적으로는 List, Set과 같은 Collection에서 주로 사용하고 Array를 대상으로 생성할 수 있다. 객체명.sream() 으로 생성 가능하다.

2-1 List(Collection)

ArrayList<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");

Stream<String> stream = list.stream();

2-2 Array

String[] arr = {"a","b","c"};
Stream<String> streamArr = Arrays.stream(arr);

2-3 Straem

Stream<String> stream1 = Stream.of("1","2","3");

위와 같이 생성이 가능하다.

 

3. 스트림 파이프라인

스트림은 함수형 인터페이스의 람다식을 파라미터로 받는 스트림 연산을 연결해, 하나의 파이프라인을 구축해 최종적으로 스트림 자신을 반환한다. 이를 스트림 파이프라이닝, 체이닝 등의 용어로 부른다.

 

스트림 파이프라인은 일반적으로 세가지의 과정을 거친다.

 

  1. 데이터 소스 생성
  2. 파이프라인을 구성할 중간 연산들의 연결
  3. 결과를 출력하는 최종 연산

데이터 소스 생성은 2번 항목에서 소개한 내용이다.

 

java.util.stream 자바 공식 문서에서는 중간 연산을 intermediate operation이라 부르며 stream을 return 한다. 최종 연산을 terminal operation이라 부르며 단일 값을 return 한다.

 

Interface Stream에 모든 중간 연산과 최종 연산이 있고, 이 글에선 자주 사용하는 몇 가지만 추려서 정리해보고자 한다.

먼저 중간 연산을 소개한다.

3.1 중간 연산(intermediate operation)

중간 연산에는 stateful/non-stateful operation이 있다.

 

stateful operation은 distinct(중복 제거)와 같이 연산이 어디까지 진행되었는가를 기억해야하는 stream 연산을 의미한다.

skip, sorted, peek이 stateful에 해당한다. (non-stateful operation은 stateful 외의 중간 연산을 말한다.)

 

이와 별개로 가장 많이 쓰이는 세 가지(map, filter, sorted)만 예제를 들어서 설명한다.

3.1.1 map

map은 파라미터로는 함수형 인터페이스인 Function을 받아 주어진 함수를 적용한 결과로 구성된 스트림을 반환한다.

public class StreamTest {

    public static void main (String[] args) {

        List<Integer> list = Arrays.asList(3, 6, 9, 12, 15);

        list.stream().map(number -> number * 3).forEach(System.out::println);
    }
} 

결과는 초기 List에 3을 곱해준 리스트를 출력한다.

9
18
27
36
45  

3.1.2 filter

filter는 파라미터로 Predicate를 받아 주어진 조건과 일치하는 스트림을 반환한다.

public class StreamTest {

    public static void main (String[] args) {
        List<Integer> intList = List.of(15,20,48,63,49,27,56,32,9);
        test(intList);
    }

    private static void test(List<Integer> intList){
        System.out.print("\nEven numbers are : ");

        intList.stream().filter(
                        element -> (element%2==0)
                )
                .forEach(
                        element -> System.out.print(element+ " ")
                );

    }
}

결과는 초기 리스트에서 짝수 값을 반환한다.
Even numbers are : 20 48 56 32

3.1.3 sort

sort은 파라미터로 Comparator를 받아 스트림을 정렬하여 반환한다. 파라미터 없이도 사용 가능하며 파라미터가 없을 경우 오름차순으로 정렬한다.

public class StreamTest {
    public static void main (String[] args) {
        List<Integer> list = Arrays.asList(-9, -18, 0, 25, 4);

        list.stream().sorted().forEach(System.out::println);
    }
}

결과는 오름차순 정렬 된 리스트를 반환

-18
-9
0
4
25

이 외에도 수많은 중간 연산들이 있다. limit, distinct, peek, skip 등이 있으니 더 찾아보면 좋을 것 같다.

3.2 최종 연산(terminal operation)

최종 연산은 stream을 단일 값(Collection, boolean, int 등)으로 반환한다. 중간 연산을 거치지 않아도 바로 사용 가능하며, 최종 연산은 따로 예제를 다루진 않겠다.

allMatch() -> 모든 값이 일치하면 true
anyMatch() -> 하나라도 일치하는게 있으면 true
noneMatch() -> 일치하는게 하나도 없으면 true
collect() -> Collectors.toList() 등의 함수를 이용해 값을 누적
count() -> 스트림의 갯수 return
forEach() -> 외부 반복
min() -> 최소값 return
max() -> 최대값 return
reduce() 

reduce는 조금 생소한데, javascript에도 거의 같은 동작을 하는 함수가 있다. 예제는 이곳에서 가져왔다.

누산기(accumlator) 개념이 들어 가는데, 가장 앞에 들어가는 파라미터가 값을 누적한다고 생각하면 된다.

Optional<String> reducedValue = listPersons.stream()
                    .map(p -> p.getFirstName())
                    .reduce((name1, name2) -> name1 + ", " + name2);
 
if (reducedValue.isPresent()) {
    String names = reducedValue.get();
    System.out.println(names);
}

Output: Alice, Bob, Carol, David, Eric, Frank, Gibb, Henry, Isabell, Jane

int[] numbers = {123, 456, 789, 246, 135, 802, 791};
 
int sum = Arrays.stream(numbers).reduce(0, (x, y) -> (x + y));
 
System.out.println("sum = " + sum);

Output: sum = 3342

int[] numbers = {123, 456, 789, 246, 135, 802, 791};
 
int sum = Arrays.stream(numbers).reduce(0, (x, y) -> (x + y), Integer::sum);
 
System.out.println("sum = " + sum);

Output: sum = 3342

 

4. Parallel Stream

스트림 인터페이스를 이용하면 아주 간단하게 스트림 내의 요소들을 병렬로 처리할 수 있다.

 

(스트림).parallelStream()을 붙이면 병렬 스트림이 생성되며, 각각의 스트림에서 처리할 수 있도록 요소를 여러 서브 파트로 나눈 후 쓰레드로 할당한다.

 

쓰레드를 이용하는 것이 무조건 좋지만은 않은게, 쓰레드를 나누고 데이터를 재배치하는작업은 자바에서 굉장히 비싼 작업이다.

 

단순 연산이나 적은 데이터에는 단일 stream 보다 성능이 떨어지므로, 대부분의 경우에는 단일 stream만을 사용해도 충분하다. 데이터가 충분히 크거나 연산 작업이 매우 큰 작업에 적합하니 사용에 있어서 충분한 고려가 필요하다.

 

5. 성능 이슈

앞서 이야기했지만 스트림은 비용이 비싸다고 했다. 일반 반복문(for-loop)에 비해서 성능이 썩 좋지 않다는 이슈가 있다. stream이 for-loop보다, 느리다는 글인데 아래 링크를 보면 자세히 정리되어 있다.


언제 stream을 쓰지말아야하는가?
Java Stream API는 왜 for-loop보다 느릴까
(parallel stream에 대해서도 매우 자세히 설명되어 있으니 참고)

 

요약하면, 자바 컴파일러는 40년 동안 for-loop를 최적화 시켜왔기 때문에, 비교적 이후에 개발된 stream 연산은 for-loop에 비해 덜 최적화 되어있다고 한다.

 

경우에 따라서는 큰 차이가 나지 않으나 연산 속도가 중요한 개발일 경우 for-loop의 사용을 권장한다고 한다.(연산 속도가 중요한 개발에서 Java를 쓴다는 것 자체가 의문스럽긴하겠지만 말이다.)

 

다만, 두 글 모두 공통적으로 이야기하는 것은 현재 개발자의 상황에 맞게 사용하면 된다고 한다. 또한 stream의 코드 가독성은 for-loop에 비해 우위에 있기 때문에 더욱 그러한 것 같다. Java Stream API는 왜 for-loop보다 느릴까 이 글의 마지막 말이 굉장히 와닿는다.

속도도 중요한 이슈이지만, 가독성과 유지보수의 용이도 중요한 이슈이다. 상황에 따라 개발자가 어떠한 방식을 취할 것인가를 종합적으로 고려해 판단할 필요가 있어보인다.

'개발 > JAVA' 카테고리의 다른 글

[JAVA] Optional 클래스  (1) 2023.01.26
[JAVA] Collectors  (1) 2023.01.26
[JAVA] 함수형 인터페이스  (1) 2023.01.26
[JAVA] TemporalAdjusters  (0) 2023.01.26
[Java] ObjectMapper  (0) 2023.01.22
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/10   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
글 보관함