14.5 병렬 스트림과 기본형 스트림
1. 병렬 스트림 (Parallel Stream)
parallelStream()은 스트림의 연산을 여러 CPU 코어를 활용해 병렬로 처리합니다. ForkJoinPool을 내부적으로 사용합니다.
import java.util.*;
import java.util.stream.*;
List<Integer> numbers = IntStream.rangeClosed(1, 1_000_000)
.boxed().collect(Collectors.toList());
// 순차 스트림
long start1 = System.currentTimeMillis();
long sum1 = numbers.stream()
.mapToLong(Integer::longValue).sum();
System.out.println("순차: " + (System.currentTimeMillis() - start1) + "ms, 합=" + sum1);
// 병렬 스트림
long start2 = System.currentTimeMillis();
long sum2 = numbers.parallelStream()
.mapToLong(Integer::longValue).sum();
System.out.println("병렬: " + (System.currentTimeMillis() - start2) + "ms, 합=" + sum2);
언제 병렬 스트림을 써야 하나?
| 적합한 경우 | 부적합한 경우 |
|---|---|
| 데이터가 매우 많음 (수만 건 이상) | 데이터가 적음 |
| 연산이 CPU 집약적 | 연산이 단순하고 빠름 |
| 독립적인 연산 (순서 무관) | 순서가 중요한 경우 |
| ArrayList 등 분할 용이한 자료구조 | LinkedList 등 분할 어려운 자료구조 |
// 병렬에 적합: 복잡한 연산, 대용량 데이터
List<Integer> bigList = IntStream.rangeClosed(1, 100_000)
.boxed().collect(Collectors.toList());
long result = bigList.parallelStream()
.filter(n -> n % 2 == 0)
.mapToLong(n -> (long) n * n)
.sum();
// 병렬에 부적합: 순서 중요, I/O 위주
List<String> files = List.of("a.txt", "b.txt", "c.txt");
// 파일 I/O는 병렬로 해도 효과 없거나 순서 깨질 수 있음
주의사항: 상태 공유 금지
// ❌ 위험! 공유 상태 수정
List<Integer> shared = new ArrayList<>();
IntStream.range(0, 1000).parallel().forEach(shared::add); // ConcurrentModificationException!
// ✅ collect로 안전하게
List<Integer> safe = IntStream.range(0, 1000)
.parallel()
.boxed()
.collect(Collectors.toList()); // 내부적으로 스레드 안전하게 처리
2. 기본형 스트림 (Primitive Streams)
일반 Stream<Integer>는 객체 박싱(boxing) 때문에 오버헤드가 있습니다. IntStream, LongStream, DoubleStream 은 기본형을 직접 다루어 성능이 훨씬 좋습니다.
IntStream
// 생성 방법
IntStream s1 = IntStream.of(1, 2, 3, 4, 5);
IntStream s2 = IntStream.range(1, 6); // 1,2,3,4,5 (끝 미포함)
IntStream s3 = IntStream.rangeClosed(1, 5); // 1,2,3,4,5 (끝 포함)
IntStream s4 = "Hello".chars(); // 각 문자의 int 코드
// 통계 연산 (박싱 없이 기본형으로 직접 계산)
int sum = IntStream.rangeClosed(1, 100).sum(); // 5050
double avg = IntStream.rangeClosed(1, 100).average().orElse(0); // 50.5
int max = IntStream.of(3, 1, 4, 1, 5, 9).max().orElse(0); // 9
long count = IntStream.rangeClosed(1, 100).count(); // 100
// 통계 한 번에
IntSummaryStatistics stats = IntStream.rangeClosed(1, 100).summaryStatistics();
System.out.println(stats.getSum()); // 5050
System.out.println(stats.getAverage()); // 50.5
System.out.println(stats.getMax()); // 100
// 배열로
int[] arr = IntStream.rangeClosed(1, 5).toArray(); // [1, 2, 3, 4, 5]
// 박싱 (Stream<Integer>로 변환)
Stream<Integer> boxed = IntStream.rangeClosed(1, 5).boxed();
List<Integer> list = boxed.collect(Collectors.toList());
LongStream, DoubleStream
// LongStream: 큰 수 처리
long factorial = LongStream.rangeClosed(1, 20)
.reduce(1L, (a, b) -> a * b);
System.out.println(factorial); // 2432902008176640000
// DoubleStream: 실수 연산
double[] prices = {100.0, 200.0, 300.0, 150.0};
DoubleSummaryStatistics priceStats = DoubleStream.of(prices).summaryStatistics();
System.out.printf("평균 가격: %.2f%n", priceStats.getAverage()); // 평균 가격: 187.50
mapToInt, mapToLong, mapToDouble - 기본형 스트림으로 변환
record Product(String name, int price, int quantity) {}
List<Product> products = List.of(
new Product("사과", 1000, 50),
new Product("바나나", 800, 30),
new Product("딸기", 2000, 20)
);
// 총 재고 가치 계산 (int 박싱 없이 효율적)
int totalValue = products.stream()
.mapToInt(p -> p.price() * p.quantity())
.sum();
System.out.println("총 재고 가치: " + totalValue + "원"); // 126000원
// 최고가 상품
OptionalInt maxPrice = products.stream()
.mapToInt(Product::price)
.max();
System.out.println("최고가: " + maxPrice.orElse(0) + "원"); // 2000원
// 평균 가격
OptionalDouble avgPrice = products.stream()
.mapToDouble(Product::price)
.average();
System.out.printf("평균가: %.0f원%n", avgPrice.orElse(0)); // 평균가: 1267원
3. Stream 생성 고급 방법
// Stream.of
Stream<String> s1 = Stream.of("A", "B", "C");
// Stream.generate - 무한 스트림 (반드시 limit 사용!)
Stream<Double> randoms = Stream.generate(Math::random).limit(5);
Stream<String> helloStream = Stream.generate(() -> "hello").limit(3);
// Stream.iterate - 초기값 + 함수로 생성
Stream<Integer> evens = Stream.iterate(0, n -> n + 2).limit(10);
// 0, 2, 4, 6, 8, 10, 12, 14, 16, 18
// Stream.iterate with 조건 (Java 9+)
Stream<Integer> under100 = Stream.iterate(1, n -> n < 100, n -> n * 2);
// 1, 2, 4, 8, 16, 32, 64
// Stream.concat - 두 스트림 합치기
Stream<String> combined = Stream.concat(
Stream.of("A", "B"),
Stream.of("C", "D")
);
combined.forEach(System.out::print); // ABCD
// Stream.builder
Stream.Builder<String> builder = Stream.builder();
builder.add("A");
builder.add("B");
if (true) builder.add("C");
Stream<String> built = builder.build();
고수 팁
병렬 스트림 실무 가이드:
-
먼저 측정하라: 병렬 스트림이 항상 빠른 것이 아닙니다. 소규모 데이터나 간단한 연산에서는 오히려 느립니다.
System.nanoTime()으로 측정 후 적용하세요. -
ForkJoinPool 크기 설정: 기본적으로
Runtime.getRuntime().availableProcessors() - 1개의 스레드를 사용합니다. 스프링 웹 서버에서 무분별한parallelStream()사용은 공유 스레드 풀을 소진시킬 수 있습니다. -
기본형 스트림 우선:
Stream<Integer>대신IntStream,Long연산에LongStream을 쓰면 박싱 비용이 없어 성능이 좋습니다. 특히 수치 집계 연산에서 차이가 큽니다.
// 느림: 박싱 오버헤드
long sum1 = Stream.iterate(1, n -> n + 1).limit(1_000_000)
.mapToLong(Integer::longValue).sum();
// 빠름: 처음부터 기본형 스트림 사용
long sum2 = LongStream.rangeClosed(1, 1_000_000).sum();