티스토리 뷰

언어/Java

[Java]스트림(Stream)에 대하여

꼬마우뇽이(원종운) 2022. 1. 3. 19:35

1편 - 스트림(Stream)에 대하여 - 현재 글

2편 - 스트림(Stream)의 사용 방법에 대하여


스트림이란 무엇인가?

공식 문서에 의하여 순차(sequence) 및 병렬(parallel) 집계(aggregate) 연산을 지원하는 데이터의 유한 또는 무한의 시퀀스를 의미합니다. 시퀀스라는 단어를 한글로 번역하기보다는 그대로 받아들이는 것이 더 좋은 것 같아 시퀀스라고 표현하겠습니다.

 

영어 Stream을 검색해보면 물이 졸졸 흐르는 물줄기 이런 말이 있습니다. 이를 우리의 시선에 맞게 해석하면 우리가 다루는 데이터들의 흐름이라고 생각하시면 됩니다. 그 흐름을 우리가 원하는 연산에 따라 제어를 하는 거죠.

 

스트림이 왜 나오게 되었는가?

표준화되지 않은 데이터 처리 방법

일반적으로 데이터 시퀀스를 다루는 많은 자료구조들이 존재합니다. 하지만 사용하는 방법이 표준화되어있지 않습니다. 배열의 정렬을 보더라도 기본형 배열의 경우는 정렬을 하려면 Arrays.sort 메서드를 사용하여야 하고, List와 같은 컬렉션들을 정렬하기 위해서는 Collections.sort()를 사용하여야 합니다. 이렇게 표준화되지 않은 방법들을 높은 추상화 레벨을 두어 표준화했다고 볼 수 있을 것 같습니다.

 

함수형 프로그래밍 패러다임

함수형 프로그래밍 패러다임은 선언형 프로그래밍의 특성을 함수를 통하여 구현하는 패러다임입니다. 기존의 명령형 프로그래밍 패러다임은 데이터를 가지고 어떻게(How) 연산을 수행할지에 대해서 명령을 한다면, 함수형 프로그래밍 패러다임의 경우는 어떻게가 아닌 무엇(What)을 수행할 건 지 선언만 하고 어떻게 처리할지는 신경 쓰지 않습니다. 즉 더욱 높은 단계의 추상화를 도입한 것입니다.

 

스트림의 특징

1. 원본 데이터를 변경하지 않는다.

스트림은 데이터의 무한 또는 유한 시퀀스이며 데이터들의 흐름입니다. 스트림은 연산 과정 중 데이터 시퀀스를 변경하지 않습니다.

 

public class App {

    public static void main(String[] args) {
        int[] arr = {10, 4, 2, 1, 12};
        int[] sortedArr = Arrays.stream(arr).sorted().toArray();
        System.out.println("기존 arr : " + Arrays.toString(arr));
        // 결과 : 기존 arr : [10, 4, 2, 1, 12]
        System.out.println("정렬된 arr : " + Arrays.toString(sortedArr));
        // 결과 : 정렬된 arr : [1, 2, 4, 10, 12]
    }
}

 

위 코드는 배열(arr)을 스트림을 변환하여 오름차순으로 정렬한 후 새로운 배열로 반환받은 결과(sortedArr)를 출력한 코드입니다. 보시다시피 새로운 배열(sortedArr)이 올바르게 정렬되어 반환되었지만 원본 데이터 시퀀스인 배열(arr)에는 아무런 변경이 가해지지 않은 걸 알 수 있습니다.

 

이와 같이 스트림은 원본 데이터 시퀀스를 읽기만 할 뿐, 변경을 가하지 않습니다.

 

2. 일회용이다.

생성된 스트림은 Iterator처럼 일회용입니다. 스트림을 종단 연산을 통하여 최종 결과를 반환받을 경우 스트림이 닫히게 되며, 재사용(종단 연산 호출) 할 수 없습니다.

public class App {

    public static void main(String[] args) {
        int[] arr = {10, 4, 2, 1, 12};
        IntStream arrStream = Arrays.stream(arr).sorted(); // 중간연산

        System.out.println("기존 arr : " + Arrays.toString(arr));
        System.out.println("정렬된 arr : " + Arrays.toString(arrStream.toArray())); // 종단 연산
        System.out.println("정렬된 arr의 합 : " + arrStream.sum()); // 종단 연산
    }
}

  

위 코드는 배열을 스트림을 변환하여 정렬을 하도록 선언한 것입니다. 결과를 받기 위한 종단 연산을 수행하지 않았습니다. 하지만 toArray 메서드를 통하여 결과를 배열로 반환받는 종단 연산을 수행할 경우 스트림은 닫히게 되며, 종단 연산을 추가로 수행하게 될 경우 다음과 같이 스트림이 닫혔다는 예외가 발생합니다.

 

이와 같이 스트림은 재사용할 수 없으며, 일회용입니다.

 

3. 중간 연산과 종단 연산

스트림의 연산은 중간 연산과 종단 연산으로 구분이 됩니다. 이러한 중간 연산과 종단 연산으로 구성한 연산을 통하여 데이터 시퀀스를 처리하는 연산 단계를 스트림 파이프라인이라고 부릅니다. 스트림 파이프라인은 최종적으로 종단 연산으로 끝나야 하며 그 사이에 0개 이상의 중간 연산이 존재할 수 있습니다.

 

중간 연산을 거치기 전 스트림으로 변환된 데이터 시퀀스를 소스 스트림이라고도 합니다.

 

중간 연산

중간 연산은 모두 한 스트림을 집계 연산을 거친 후 다른 스트림으로 변환하는 역할을 합니다. 변환된 스트림의 원소 타입은 집계 연산에 따라 변환 전 스트림의 원소 타입과 다를 수 있습니다. 또한 중간 연산은 스트림을 반환하여주기 때문에 또 다른 중간 연산으로 연결(체이닝)이 가능합니다!

 

public class App {

    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4};
        int result = Arrays.stream(arr) // 소스 스트림
            .map(v -> v * 2) // 중간 연산
            .filter(v -> v > 2) // 중간 연산
            .sum(); // 종단 연산
        
        System.out.println("결과 : " + result);
    }
}

 

스트림에서 지원하는 중간 연산의 종류는 조금 이따 알아보도록 하겠습니다. 위 코드에서는 소스 스트림에서 시작하여 2개의 중간 연산(map, filter)을 연결한 후 최종 연산을 통하여 결과를 도출한 것입니다.

 

종단 연산

종단 연산은 중간 연산을 통하여 원하는 집계 연산이 수행된 결과들을 원하는 결과로 가공하는 연산을 말합니다. 합을 구하거나 평균을 구하거나 아니면 특정 데이터 형태로 반환을 받거나 등의 연산 등이 있습니다. 종단 연산을 할 경우 스트림이 닫히게 되며 재사용할 수 없습니다.

 

위 코드에서 스트림의 sum이라는 메서드가 종단 연산에 해당합니다.

 

4. 지연(Lazy) 평가

스트림 파이프라인은 소스 스트림에서 시작하여 하나 이상의 중간 연산과 종단 연산으로 구성된다고 하였습니다. 그리고 최종적으로 연산의 결과를 종단 연산을 통하여 얻는다고도 하였습니다. 여기서 중요한 점은 연산의 결과를 종단 연산을 통해서 얻는다는 점입니다.

 

즉, 종단 연산을 호출하지 않는 이상 중간 연산을 연결하여 스트림을 연결하였더라도 우리가 원하는 연산이 수행되지 않는다는 것입니다.

 

종단 연산이 없는 스트림 파이프는 아무런 동작을 하지 않습니다.

 

5. 순차 수행 및 병렬 수행

스트림은 데이터들의 흐름이라고 했습니다. 일반적으로 스트림에서는 이러한 흐름이 한 방향으로만 흐르며 따라 순차적으로 처리되는 방식으로 구성되어있습니다.

 

하나의 흐름으로 처리한다.

하지만 처리해야 하는 데이터가 많을 경우 데이터 시퀀스들을 분할(fork)하여 처리한 후 취합(join)하는 방식의 병렬 수행을 지원합니다. fork, join이라는 말을 들으니 이 글에서 다뤘던 쓰레드 풀과 ForkJoinPool이 떠오르지 않으시나요!? 맞습니다.

하나의 흐름을 여러 흐름으로 나누어 처리한 후 다시 합친다.

기본적으로 병렬 수행을 할 경우 ForkJoinPool을 이용하여 수행됩니다.

 

병렬 수행을 하면 대부분의 경우 빠른 처리가 가능할 거라 생각하여 무조건 병렬 수행을 할 수 있는데, 멀티 쓰레드 비용, 데이터를 분할하고 순서가 보존되어야 하는 연산의 경우 취합 과정의 비용 등을 고려하여 선택하여야 합니다.

 

앞서 살펴봤듯이 무조건 멀티 쓰레드를 이용하여 처리한다고 성능 향상이 되는 것이 아니라는 걸 우리는 알고 있습니다. 또한 대부분의 경우 병렬 수행을 한다고 하여도 큰 이점을 누리긴 어렵습니다.

 

마무리

다음 글에서는 대표적인 중간 연산과 종단 연산을 알아보고, 스트림 사용 시 주의해야 하는 점과 사용 팁과 단점에 대해서 알아보도록 하겠습니다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
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
글 보관함