Notice
Recent Posts
Recent Comments
Link
«   2026/03   »
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
Tags more
Archives
Today
Total
관리 메뉴

프로그래밍 공부

[자바] #21. 스트림(Stream) - 2편 (병렬 스트림, 중간 연산 추가 내용) 본문

자바

[자바] #21. 스트림(Stream) - 2편 (병렬 스트림, 중간 연산 추가 내용)

하 냥 2025. 2. 21. 16:14

📚 병렬 스트림

스트림의 요소들을 여러 스레드에서 병렬로 처리하여 성능을 향상시키는 스트림.

내부적으로 'ForkJoinPool'을 사용하여 작업을 분할하고 병렬로 실행한다.

대용량 데이터 처리나 계산 집약적인 작업에서 CPU 활용도를 높여 처리 속도를 개선할 수 있다.


⚡ 병렬 스트림 생성 방법 1, 2

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

// 기존 스트림을 병렬 스트림으로 변환
numbers.stream().parallel().forEach(System.out::println);

// 컬렉션에서 바로 병렬 스트림 생성
numbers.parallelStream().forEach(System.out::println);

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

일반 스트림 (stream()) 병렬 스트림 (parallelStream())
단일 스레드에서 순차적으로 처리 멀티 스레드를 사용하여 병렬 처리
순서 보장 (forEachOrdered 제외 시) 처리 순서가 보장되지 않음 (forEachOrdered 사용 시 보장 가능)
대용량 데이터 처리 시 상대적으로 느림 대용량 데이터 처리 시 빠름 (CPU 코어 활용)
CPU 활용도가 낮음 CPU 멀티코어를 활용하여 성능 최적화

📝 병렬 스트림 예제 - 대용량 데이터 처리 시간 비교

import java.util.*;
import java.util.stream.*;
import java.time.*;

public class ParallelStreamExample {
    public static void main(String[] args) {
        List<Integer> numbers = IntStream.rangeClosed(1, 1000000)
                                         .boxed()
                                         .collect(Collectors.toList());

        // 일반 스트림 처리 시간
        Instant start = Instant.now();
        numbers.stream().reduce(0, Integer::sum);
        Instant end = Instant.now();
        System.out.println("일반 스트림 처리 시간: " + Duration.between(start, end).toMillis() + "ms");

        // 병렬 스트림 처리 시간
        start = Instant.now();
        numbers.parallelStream().reduce(0, Integer::sum);
        end = Instant.now();
        System.out.println("병렬 스트림 처리 시간: " + Duration.between(start, end).toMillis() + "ms");
    }
}
일반 스트림 처리 시간: 120ms
병렬 스트림 처리 시간: 40ms

⚡ 병렬 스트림이 대용량 데이터 처리 시 더 빠른 속도를 보여준다.


예제에서 numbers 스트림을 만드는 과정 해석:

 

1️⃣ IntStream.rangeClosed(1, 1000000)

  • IntStream은 기본형 스트림(int)을 다루는 스트림
  • rangeClosed(start, end) 메서드는 start부터 end까지 (양쪽 모두 포함)의 정수를 생성한다. (예를 들어 range(1, 5)는 1, 2, 3, 4, 5를 생성한다)

2️⃣ boxed()

  • 기본형 스트림(IntStream)을 참조형 스트림(Stream<Integer>)으로 변환
  • 즉, int → Integer로 변환하는 오토 박싱 과정을 수행한다.

💡 왜 필요할까?

  • 기본형 스트림(IntStream)은 성능상 유리하지만, 객체(Integer)를 요구하는 연산에서는 사용하기 어렵다.
  • 예를 들어, List<Integer>와 같은 컬렉션으로 수집하려면 객체 스트림(Stream<Integer>)이어야 한다.
IntStream.rangeClosed(1, 3)
         .boxed()
         .forEach(System.out::println);

 

요약:

  • int → Integer로 박싱
  • IntStream → Stream<Integer>로 변환
  • 이후 객체 스트림이 필요한 연산(collect, map, filter) 등에 사용할 수 있음

3️⃣ collect(Collectors.toList())

  • 스트림의 요소들을 List 컬렉션으로 수집
  • collect() 메서드는 스트림의 최종 연산 중 하나이며, 스트림을 특정한 형태로 수집하는 데 사용된다.

collect(Collector<T, A, R> collector) 형태로 사용되며,

Collectors.toList()는 스트림의 모든 요소를 List에 담아 반환한다.

 

다른 수집 예시:

메서드 설명 예시 출력
Collectors.toList() 요소를 List로 수집 [1, 2, 3]
Collectors.toSet() 요소를 Set으로 수집 (중복 제거) [1, 2, 3]
Collectors.joining(",") 문자열 스트림을 연결 (구분자 추가 가능) "1,2,3"
Collectors.toMap(...) 요소를 Map으로 수집 {key1=value1, ...}

 

🎯 최종 요약: 전체 코드 동작 과정

List<Integer> numbers = IntStream.rangeClosed(1, 1000000)  // 1부터 1,000,000까지 정수 생성 (기본형 int 스트림)
                                 .boxed()                 // int → Integer 변환 (Stream<Integer>로 변환)
                                 .collect(Collectors.toList()); // Stream<Integer>를 List<Integer>로 수집

🧩 중간 연산 추가 내용들

1️⃣ flatMap()

각 요소를 스트림으로 변환한 후, 중첩된 스트림을 하나의 스트림으로 평면화(flatten)

<R> Stream<R> flatMap(Function<T, Stream<R>> mapper)
// 원래는 <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)

// map 메서드는 아래와 같이 생겼다.
<R> Stream<R> map(Function<T, R> mapper)

Function<T, R>의 추상 메서드는 R apply(T t)이므로, 위 메서드 호출 시 전달해야 할 메서드는 다음과 같다:

Stream<R> apply(T t)

즉 flatMap에 전달할 람다식에서는, '스트림을 생성하고 이를 반환'해야 한다.

반면 map에 전달할 람다식에서는 스트림을 구성할 데이터만 반환하면 되는 것이다.


 

💡 예제 1: 문자열을 단어로 분해하여 평면화하기

import java.util.*;
import java.util.stream.*;

public class FlatMapExample {
    public static void main(String[] args) {
        List<String> sentences = Arrays.asList("Java is fun", "Streams are powerful");

        List<String> words = sentences.stream()
                                      .flatMap(sentence -> Arrays.stream(sentence.split(" "))) // 문자열 분해 후 평면화
                                      .collect(Collectors.toList());

        System.out.println(words);  // 출력: [Java, is, fun, Streams, are, powerful]
    }
}

 

💡 예제 2: 리스트의 리스트 평면화하기

import java.util.*;
import java.util.stream.*;

public class FlatMapListExample {
    public static void main(String[] args) {
        List<List<Integer>> listOfLists = Arrays.asList(
            Arrays.asList(1, 2, 3),
            Arrays.asList(4, 5, 6),
            Arrays.asList(7, 8, 9)
        );

        List<Integer> flatList = listOfLists.stream()
                                            .flatMap(Collection::stream) // 리스트를 평면화
                                            .collect(Collectors.toList());

        System.out.println(flatList);  // 출력: [1, 2, 3, 4, 5, 6, 7, 8, 9]
    }
}

📝 예제 2 설명:

1️⃣ listOfLists.stream():

  • List<List<Integer>>스트림으로 변환 (Stream<List<Integer>>)
  • 형태: [ [1, 2, 3], [4, 5, 6], [7, 8, 9] ]

2️⃣ flatMap(Collection::stream):

  • 각 내부 리스트(List<Integer>)를 스트림(Stream<Integer>)으로 변환
  • 결과적으로 Stream<Stream<Integer>> 형태가 되지만, flatMap()이 각 내부 스트림을 평면화하여
    👉 Stream<Integer> 형태로 반환한다.
  • 형태: [1, 2, 3, 4, 5, 6, 7, 8, 9]

3️⃣ collect(Collectors.toList()):

  • 평면화된 스트림을 List<Integer>로 수집

⚡ flatMap vs map 차이점

List<Stream<Integer>> mappedList = listOfLists.stream()
                                              .map(Collection::stream)
                                              .collect(Collectors.toList());
[java.util.stream.ReferencePipeline$Head@..., 
 java.util.stream.ReferencePipeline$Head@..., 
 java.util.stream.ReferencePipeline$Head@...]

 

🔥 왜 이렇게 출력될까?

  • map()은 각 내부 리스트를 스트림 자체로 변환하지만,
  • 평면화하지 않기 때문에 스트림의 스트림(Stream<Stream<Integer>>) 형태가 된다.
  • 즉, 내부 스트림을 단일 스트림으로 합치지 않고 그대로 유지한다.

🌈 flatMap()의 내부 동작 시각화

// 입력 데이터 (중첩 리스트)

[
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9]
]

 

// ✅ flatMap(Collection::stream) 처리 과정:
Stream [1, 2, 3, 4, 5, 6, 7, 8, 9]​
// 🤔 map(Collection::stream) 처리 과정:

Stream [
   Stream [1, 2, 3],
   Stream [4, 5, 6],
   Stream [7, 8, 9]
]

2️⃣ sorted() — 정렬(Sorting)

Stream<T> sorted()
Stream<T> sorted(Comparator<? super T> comparator)

스트림의 요소를 정렬.

기본적으로 자연 순서(Comparable)에 따라 정렬하지만, Comparator를 전달하여 사용자 정의 정렬도 가능하다.


💡 예제 1: 기본 정렬 (오름차순)

import java.util.*;
import java.util.stream.*;

public class SortedExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(5, 2, 8, 1, 3);

        List<Integer> sortedNumbers = numbers.stream()
                                             .sorted()  // 오름차순 정렬
                                             .collect(Collectors.toList());

        System.out.println(sortedNumbers);  // 출력: [1, 2, 3, 5, 8]
    }
}

 

 

💡 예제 2: 사용자 정의 정렬 (내림차순)

import java.util.*;
import java.util.stream.*;

public class CustomSortedExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Charlie", "Alice", "Bob");

        List<String> sortedNames = names.stream()
                                        .sorted(Comparator.reverseOrder())  // 내림차순 정렬
                                        .collect(Collectors.toList());

        System.out.println(sortedNames);  // 출력: [Charlie, Bob, Alice]
    }
}

여기서 Comparator.reverseOrder() 란:

기본 정렬 순서를 반대로(내림차순으로) 만드는 Comparator를 반환하는 정적 메서드.


3️️ peek() — 루핑(디버깅용)

Stream<T> peek(Consumer<? super T> action)

 

스트림의 요소를 소비하지 않고 중간 과정에서 엿보기(peek) 위한 메서드.

주로 디버깅이나 중간 단계 확인을 위해 사용된다.

forEach()와 달리 중간 연산이며, 최종 연산이 호출될 때까지 실행되지 않는다.

 

💡 예제: peek()를 사용하여 디버깅

import java.util.*;
import java.util.stream.*;

public class PeekExample {
    public static void main(String[] args) {
        List<String> result = Stream.of("one", "two", "three", "four")
                                    .filter(s -> s.length() > 3)
                                    .peek(s -> System.out.println("Filtered value: " + s)) // 중간 과정 출력
                                    .map(String::toUpperCase)
                                    .peek(s -> System.out.println("Mapped value: " + s))   // 변환 과정 출력
                                    .collect(Collectors.toList());

        System.out.println("최종 결과: " + result);
    }
}