반응형
1️⃣ “parallelStream() 붙이면 빨라지겠지?”
많은 개발자가 스트림을 이렇게 사용함
list.parallelStream()
.map(this::expensiveOperation)
.collect(Collectors.toList());
"병렬 스트림"이니까 여러 CPU 코어를 활용해서 당연히 더 빠를거라 기대하지만,
현실은 다름!
병렬 스트림은 마법이 아님
올바른 상황에서만 이득이 있고, 그렇지 않으면 오히려 성능이 나빠지거나 오류를 일으킬 수 있음
2️⃣ 스트림 병렬화의 원리
병렬 스트림은 내부적으로 ForkJoinPool을 사용해
요소들을 여러 쓰레드로 쪼개서 처리함
IntStream.range(0, 1_000_000)
.parallel()
.forEach(i -> doWork(i));
이렇게 하면 내부적으로 여러 스레드가 doWork()를 병렬로 실행함
문제는, 이 분할 합산 비용이 꽤 크다는 점
3️⃣ 병렬 스트림이 효과적인 경우 ✅
| 조건 | 설명 |
| 데이터 크기가 큼 | 수만~수백만 개 이상일 때만 분할 비용을 상쇄 |
| 연산 비용이 높음 | CPU 계산 위주 |
| 요소 간 독립적 | 한 요소의 결과가 다른 요소에 영향 없음 |
| 데이터 구조가 Spliterator-friendly | ex. ArrayList, IntStream.range() |
| 결과의 순서가 중요하지 않음 | 병렬 스트림은 순서 유지가 어렵거나 비용이 큼 |
💡 좋은 예시
long count = IntStream.range(0, 1_000_000)
.parallel()
.mapToLong(Math::sqrt)
.count();
- 각 숫자는 독립적
- 계산은 CPU 바운드
- 순서 상관 없음
4️⃣ 병렬 스트림이 오히려 느려지는 경우 ❌
| 상황 | 이유 |
| 데이터가 작음 | 쓰레드 분할/병합 오버헤드가 더 큼 |
| I/O 작업이 포함됨 | I/O 대기 중 쓰레드 낭비 |
| 상태를 공유하거나 동기화 필요 | 경쟁 상태 발생 위험 |
| 순서 의존 로직 | 병렬 이점이 거의 사라짐 |
| 박싱/언박싱이 잦음 | 자동 변환 비용이 병렬 이득을 상쇄 |
❌ 잘못된 예시
List<String> words = List.of("A", "B", "C", "D");
words.parallelStream()
.forEach(System.out::println); // ❌ 순서 깨짐 + 부작용 가능
5️⃣ 병렬 스트림의 위험한 함정 — 부작용
스트림은 함수형 패러다임을 따름
즉, 외부 상태를 건드리지 않아야 함
병렬 스트림에서 외부 객체를 조작하면 이렇게 됨
List<Integer> list = new ArrayList<>();
IntStream.range(0, 1000)
.parallel()
.forEach(list::add); // ❌ 경쟁 상태 발생 가능
- 일부 요소가 누락되거나 중복됨
- ArrayIndexOutOfBoundsException 발생 가능
✅ 해결:
List<Integer> list = IntStream.range(0, 1000)
.parallel()
.boxed()
.collect(Collectors.toList());
→ 병렬 상태에서도 안전하게 동작 (collect는 내부 동기화 처리됨)
반응형
'Java & Spring Boot' 카테고리의 다른 글
| 🔄 순환 참조, 왜 생기고 어떻게 끊어야 할까 (0) | 2025.11.28 |
|---|---|
| 🧩 이펙티브 자바 item 56. 공개된 API 요소에는 항상 문서화 주석을 작성하라 (0) | 2025.10.24 |
| 🧩 이펙티브 자바 item 41. 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라 (0) | 2025.10.21 |
| 🧩 이펙티브 자바 item 31. 한정적 와일드카드로 API 유연성을 높이라 (0) | 2025.10.17 |
| 🧩 이펙티브 자바 item 24. 멤버 클래스는 되도록 static으로 만들라 (0) | 2025.10.16 |