본 프로젝트는 Java Stream API 수업의 일환으로, Stream 사용 시 자주 언급되는 성능/설계 이슈를 실제 코드와 측정 결과로 검증하는 것을 목표로 합니다.
Stream API는 선언적이고 가독성 높은 코드 작성을 가능하게 하지만, 내부 동작을 고려하지 않은 사용은 불필요한 오버헤드로 인해 오히려 성능이 저하될 수 있습니다.
이번 프로젝트에서는 다음 두 가지 이슈를 중심으로 비교 실험을 진행했습니다.
-
Parallel Stream의 임계점(Threshold)
- 데이터가 작거나 연산이 가벼우면 병렬화 오버헤드가 커져 순차 스트림이 더 빠를 수 있음
- 데이터가 크고 CPU 집약적 연산일수록 병렬 처리 이점이 커질 수 있음
-
중간 연산(Intermediate Operations) 과다 체이닝 오버헤드
filter()/map()을 불필요하게 여러 번 체이닝하면 파이프라인 단계가 늘어나 오버헤드가 발생할 수 있음- Stream Fusion 관점에서 연산을 통합해 실행 시간을 비교함
![]() |
![]() |
![]() |
|---|---|---|
| 서지혜 | 김종연 | 강민영 |
- Language: Java (JDK 17 권장)
- IDE: Eclipse (4.36.0)
- Version Control: Git / GitHub
- Tool: GitKraken (선택)
저희는 Stream API에서 자주 언급되는 7가지 이슈 중, 아래 두 가지 주장을 직접 구현한 비추천 코드 vs 추천 코드 비교를 통해 검증하고자 합니다.
- 중간 연산(filter, map)을 과도하게 체이닝하면 성능 오버헤드가 발생한다
- 병렬 처리 시 동일한 데이터를 동시에 수정하면 레이스 컨디션이 발생할 수 있다
이를 단순한 코드 나열이 아니라 측정/재현 가능한 형태로 구현하여 왜 비추천인지와 어떤 방식이 더 안전/효율적인지를 결과로 확인하는 것을 목표로 합니다.
- 문제점: 데이터셋이 매우 작음(예: 5개)에도 parallelStream()을 사용하면, 작업 분할/병합 비용(스레드 스케줄링, ForkJoin 오버헤드 등)이 실제 연산보다 커져 비효율이 발생할 수 있다.
- 결과 해석: 불필요한 병렬화는 오히려 순차 처리보다 성능을 저하시킬 수 있음을 확인.
단순히 병렬이 빠르다/느리다가 아니라 성능 이득이 발생하는 임계점(Threshold) 을 입증하기 위해 아래 두 변수를 통제하여 실험했다.
-
변수 1) 데이터셋 크기 (N)
- 작은 데이터(예: 5개) vs 큰 데이터(1,000,000개)
- 병렬 처리의 이득이 유의미해지는 데이터 규모를 확인
-
변수 2) 연산 복잡도 (Q)
- 가벼운 연산: (예: n×n 같은 단순 계산)
→ 데이터가 많아도 연산이 너무 가벼우면 병렬화 오버헤드가 상대적으로 커질 수 있음 - 무거운 연산(CPU 집약): 소수 판별(isPrime)
→ 병렬 스트림의 진정한 성능 우위를 확인하기 위해 선택
- 가벼운 연산: (예: n×n 같은 단순 계산)
- 병렬 스트림 적용 여부는 단순히 “데이터가 많다”가 아니라, N × Q(데이터 수 × 연산 비용) 모델로 판단해야 함을 수치로 입증
- 무분별한 병렬 처리가 아닌 컨텍스트(데이터 크기, CPU 부하) 기반의 최적 스트림 사용 가이드라인 제시
- 문제점: 동일한 데이터를 처리하면서 filter()와 map() 같은 중간 연산을 불필요하게 여러 번 체이닝(Chaining)함
- 결과 해석: 각 단계마다 내부 반복/람다 호출/객체 생성 등 파이프라인 처리 과정에서 불필요한 성능 오버헤드가 누적될 수 있음
단순히 코드를 줄이는 수준이 아니라, 스트림의 Stream Fusion(연산 융합) 관점에서 파이프라인 단계를 최적화했다.
-
Filter 통합:
filter(A) → filter(B) 를
filter(A && B) 형태로 합쳐 중간 연산 단계 축소 -
Map 통합:
대문자 변환 + 문구 추가를 하나의 람다에서 처리하여 연산 횟수 축소 -
객관적 입증:
동일한 대량 데이터(1,000,000개)를 대상으로
Original(과한 중간 연산) vs Optimized(융합된 연산) 실행 시간을 ns 단위로 측정하여 수치로 비교
- 파이프라인 단계를 줄여 실행 시간이 단축되는 방향을 확인
- 코드 가독성을 높이는 동시에, 대규모 데이터 처리에서 JVM이 스트림 파이프라인을 더 효율적으로 최적화할 수 있도록 구조 개선
- 같은 입력 데이터(크기 동일)에서
- (1) 중간 연산을 여러 번 나누는 방식(Original) vs 조건을 합쳐 중간 연산 개수를 줄이는 방식(Optimized)
- 위 두 방식을 동일 조건으로 반복 실행하여 실행 시간 비교
- (2) parallelStream()이 항상 성능 우위를 점하는지 확인하기 위해 **연산 비용(Q)**이 다른 두 가지 케이스를 대조군으로 설정함 (단순 연산 vs 복잡 연산)
- 단순 연산: 이미지 예시와 같은 단순 곱셈(n × n) 연산을 수행하여, 병렬화 오버헤드가 성능을 저하시키는 현상을 확인
- 복잡 연산: CPU 집약적인 소수 판별(isPrime) 연산을 수행하여, 병렬 처리가 실질적인 성능 이득을 제공하는 상황을 입증
문제 상황
- Stream 성능을 System.nanoTime()으로 측정했는데 실행할 때마다 시간이 들쭉날쭉하거나 첫 실행이 유난히 느리게 나오는 현상이 발생함
- Java는 실행 중에 JIT(Just-In-Time) 컴파일이 적용되고 자주 실행되는 코드가 최적화되므로 첫 실행 결과를 그대로 비교하면 성능 비교가 왜곡될 수 있음
원인
- 첫 실행에는 JIT 최적화가 아직 충분히 적용되지 않았고 클래스 로딩/람다 초기화/캐시 미스 등의 초기 비용이 포함될 수 있음
해결 방법
- 측정 전에 동일 로직을 여러 번 실행(워밍업)하여 JVM이 최적화할 시간을 준 뒤, 그 다음 실행부터 시간을 측정하도록 변경함
- (권장) 워밍업과 측정 모두 동일 조건(같은 데이터/같은 횟수) 으로 맞춰 공정하게 비교함
| 데이터 범위 | 순차(ms) | 병렬(ms) | 시간 단축(ms) | 개선율 |
|---|---|---|---|---|
| 1 ~ 1,000,000 | 111.0 | 60.0 | 51.0 | 45.95% |
| 데이터 | Optimized(ms) | Original(ms) | 시간 단축(ms) | 개선율 |
|---|---|---|---|---|
| 1,000,000~5,000,000 | 312.1 | 341.9 | 29.8 | 8.72% |
streamTest/
├─ README.md
└─ src/
└─ step02/
├─ ParallelEfficiencyTest.java
└─ StreamOptimizationTest.java


