[Java] Stream 총정리 (개념, 구조, 사용법)

김찬미·2024년 11월 29일

Java

목록 보기
1/20

이 게시물의 이미지는 ELANCER를 참조하고 있습니다.

🤔 Stream이란?

StreamJava 8에서 도입된 함수형 프로그래밍 스타일의 API로, 데이터 컬렉션을 처리하고 변환하는 강력한 도구이다. Stream을 이용하면 일련의 데이터를 함수형 연산을 통해 표준화된 방법으로 쉽게 가공, 처리할 수 있다.

Java Stream API는 이러한 작업을 간편하게 수행할 수 있도록 다양한 기능을 제공한다. 이제 본격적으로 Stream의 개념, 특징, 활용법 등을 알아보자.


1) Stream을 사용하는 이유

가독성과 유지보수성 향상

Java Stream은 어떻게 할지를 하나하나 기술하는 ‘명령형’ 방식이 아닌, 무엇을 하고싶다는 것을 ‘선언적’으로 코딩하는 ‘선언형 프로그래밍’이다. 또한 연속적으로 필터링, 매핑, 정렬을 ‘체이닝’하여 표현할 수 있다.

기존의 반복문을 사용하면 코드가 길어질 때, Stream을 사용하면 간결하고 명확한 코드로 데이터를 처리할 수 있어서 코드의 가독성과 유지보수성이 향상된다.

또한 코드의 주석을 달지 않더라도 코드의 의도를 파악하기 쉬워지고, 변경이 필요한 부분을 쉽게 수정할 수 있다. 이러한 점은 협업, 유지보수와 같은 작업에 도움이 된다.

병렬 처리 지원

🤔 병렬 처리란?
병렬 처리는 작업을 여러 스레드로 나누어 동시에 수행하는 방식으로, 멀티코어 프로세서의 성능을 극대화할 수 있다. Java의 Stream API는 간단히 병렬 처리를 적용할 수 있는 기능을 제공한다.

Stream에서 지원하는 병렬처리는 데이터의 흐름을 나누어서 멀티 스레드로 병렬로 처리하고 처리 후에 합치는 과정을 통해 대량의 데이터를 빠르고 쉽게 처리할 수 있다는 장점이 있다.

Java Stream API는 간단하게 parallel() 또는 parallelStream()이라는 연산을 추가하는 것만으로 병렬처리가 가능하다. 이 메서드를 활용하면 사용자가 스레드를 직접 관리하지 않아도 되므로, 병렬 처리를 손쉽게 적용할 수 있다.

일반 스트림과 병렬 스트림의 차이

일반 스트림병렬 스트림
데이터가 순차적으로 처리됨데이터가 여러 청크로 나뉘어 병렬 처리
순서 보장이 중요할 때 적합성능 최적화가 중요할 때 적합
단일 스레드에서 작업 수행여러 스레드에서 작업 수행

2) Stream의 처리 구조와 처리 특징

데이터를 처리하기 위해서는, 일단 데이터를 생성하고, 생성된 데이터를 가공하여 필요한 형태로 변환한 다음, 최종적으로 결과를 소비해야 한다.

Java Stream은 이 과정을 따라 ‘생성 -> 가공 -> 소비’의 구조로 구성되어 있다.

Stream 생성

생성은 데이터의 컬렉션(집합)을 Stream으로 변환하는 과정으로, Stream API를 사용하기 위해서는 반드시 최초 1번 수행되어야 한다.

생성 단계에서는 모든 데이터가 한꺼번에 로드되는 것이 아닌, 필요할 때만 메모리에 로드된다. 이는 대량의 데이터 셋에서 메모리 사용량을 최적화하고, 불필요한 데이터를 로드하지 않아도 되어 효율적이다.

가공(중간 연산)

가공은 필터(filter), 변형(map), 정렬(sort) 등의 중간 처리 과정을 의미한다. 이는 데이터를 여러 가지로 가공을 하기 위한 목적이므로 중간처리 과정을 통해 데이터에 다양한 가공을 수행할 수 있다.

중간 연산의 입력값은 Stream이며, 결과물도 Stream이다. 이러한 특징 덕에 중간 연산을 연결하여 연속적으로 여러 번 수행할 수 있다.

최종 연산

최종 연산은 최종적인 목적물을 얻는 처리 과정을 의미한다. 이를 수행하면 데이터 컬렉션(집합)이나 하나의 값(ex, 합계)와 같은 결과물을 얻을 수 있다.

최종 연산 또한 단 1번만 수행할 수 있다. 최종 연산이 수행되면, Stream은 닫혀서 더 이상 어떠한 연산도 처리할 수 없다. 만약 데이터에 추가적인 가공이 필요할 경우, 새롭게 Stream을 생성해서 작업해야 한다.

- 지연 평가 (Lasy Evaluation)

중간 연산은 Stream을 다른 Stream으로 변환하거나, 요소들을 변환 또는 필터링하는 작업을 수행한다. 이러한 중간 연산들은 연산을 호출할 때 즉시 수행되지 않고 최종 연산이 호출될 때까지 지연되는데, 이를 Lazy Evaluation(지연 평가)라고 한다.

데이터의 연속적인 흐름에 대해 중간 연산은 실행되지 않고 있다가 최종 연산을 만나게 되면, 그때 중간 연산이 실제로 실행된다. Stream의 데이터가 실제로 처리되는 순서는 다음과 같은 특징이 있다.

import java.util.stream.IntStream;

public class LazyEvaluationExample2 {
    public static void main(String[] args) {
        int sum = IntStream.range(1, 100)
                           .filter(x -> x % 2 == 0) // 짝수 필터링
                           .map(x -> x * 2)         // 각 숫자 2배
                           .limit(5)                // 첫 5개만 처리
                           .sum();                  // 합산

        System.out.println("결과: " + sum); // 출력: 60 (2*2 + 4*2 + 6*2 + 8*2 + 10*2)
    }
}

// limit(5) 이후의 데이터는 처리되지 않음 & Stream은 데이터 처리량을 최소화함

- Stream 의 데이터가 처리되는 순서

데이터 처리는 일련의 데이터가 나타난 흐름의 순서대로 처리된다. 앞선 데이터가 먼저 처리되고 뒤의 데이터가 나중에 처리되는 구조라고 이해할 수 있다.

예를 들어 하수관에 오염물을 정화하는 과정에서 하수관에 1차 필터(침전), 2차 필터(정화), 최종 필터(소독)의 과정을 거친다고 할 때, 물이 흘러가는 순서대로 먼저 도달한 물이 각각 개별적으로 먼저 처리되고 나중에 도달하는 물은 나중에 처리되는 방식을 떠올리면 유사하다.


3) Stream 사용법

그럼 이제 본격적으로 Stream을 직접 사용하는 방법을 알아보자. 여기서는 앞서 설명한 대로 Java Stream API의 생성, 중간 연산, 최종 연산으로 구분하여 확인하려고 한다.

3.1) Stream 생성

Stream은 데이터의 집합인 컬렉션, 즉, 배열, ArrayList, Set, Map 등으로부터 생성할 수 있다.

  • 컬렉션이 제공하는 stream() 메소드를 이용하면 쉽게 Stream을 만들어 낼 수 있다.
public class StreamExample {

    public static void main(String[] args) {
    
        // 1. 컬렉션에서 생성
        List<String> list = List.of("A", "B", "C");
        Stream<String> listStream = list.stream();

        Set<Integer> set = Set.of(1, 2, 3);
        Stream<Integer> setStream = set.stream();

        // 2. 배열에서 생성
        String[] array = {"X", "Y", "Z"};
        Stream<String> arrayStream = Arrays.stream(array);

        // 3. 값으로 직접 생성
        Stream<Integer> valueStream = Stream.of(10, 20, 30);

        // 4. Map에서 생성
        Map<String, Integer> map = Map.of("Apple", 3, "Banana", 5);
        Stream<Map.Entry<String, Integer>> entryStream = map.entrySet().stream();
        Stream<String> keyStream = map.keySet().stream();
        Stream<Integer> valueStreamFromMap = map.values().stream();

        // 5. 파일에서 생성
        Stream<String> fileStream = Files.lines(Paths.get("example.txt")); // 텍스트 파일에서 읽기

        // 6. 숫자 범위로 생성
        IntStream rangeStream = IntStream.range(1, 5); // 1 ~ 4
        IntStream rangeClosedStream = IntStream.rangeClosed(1, 5); // 1 ~ 5
    }
}

3.2) 중간 연산

중간 연산은 데이터 변환을 수행하며 새로운 Stream을 반환한다.

메서드설명예제
filter조건에 맞는 요소만 걸러냄filter(x -> x > 10)
map각 요소를 변환map(x -> x * 2)
sorted요소 정렬 (기본 정렬 또는 Comparator 제공 가능)sorted() 또는 sorted(Comparator)
distinct중복 제거distinct()
limit스트림의 크기를 제한limit(5)
skip앞의 n개 요소를 건너뜀skip(3)
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9);
List<Integer> evenNumbers = numbers.stream()
                                   .filter(x -> x % 2 == 0)  // 짝수만 필터링
                                   .map(x -> x * 2)          // 각 숫자를 2배로 변환
                                   .sorted()                 // 정렬
                                   .toList();                // 최종 결과를 리스트로 변환
System.out.println(evenNumbers); // [4, 8, 12, 16]

3.3) 최종 연산

최종 연산은 Stream을 소모하고 결과를 반환한다.

메서드설명반환값
forEach각 요소에 대해 지정된 동작 수행void
collect요소를 특정 컬렉션으로 변환List, Set
reduce모든 요소를 하나로 합침Optional 또는 값
count요소의 개수를 반환long
anyMatch조건을 만족하는 요소가 하나라도 있는지 검사boolean
allMatch모든 요소가 조건을 만족하는지 검사boolean
noneMatch모든 요소가 조건을 만족하지 않는지 검사boolean
findFirst첫 번째 요소 반환Optional
List<Integer> numbers = List.of(1, 2, 3, 4, 5);

int sum = numbers.stream()
                 .reduce(0, Integer::sum);  // 요소들의 합 계산
System.out.println(sum); // 15

List<Integer> doubledNumbers = numbers.stream()
                                       .map(x -> x * 2)
                                       .toList();           // 컬렉션으로 변환
System.out.println(doubledNumbers); // [2, 4, 6, 8, 10]

3.4) 병렬 처리

대량의 데이터를 처리할 때 parallelStream을 사용하면 멀티스레드를 통해 성능을 향상시킬 수 있다.

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9);

int sum = numbers.parallelStream()
                 .filter(x -> x % 2 == 0)
                 .reduce(0, Integer::sum);  // 병렬 처리로 합 계산
System.out.println(sum); // 20

마치며

요즘에는 다수의 로그 등에서 발생하는 데이터를 효과적으로 가공하여 대량의 데이터를 처리해야하는 경우가 많다. 그럴 때는 Java Steam을 활용한 병렬처리가 필수적이다. 따라서 개발자라면 필수적으로 Java Stream API를 다룰 줄 알아야 한다. 처음엔 복잡해 보이지만 점차 써보며 익숙해지자!

profile
백엔드 지망 학부생

0개의 댓글