개요
저는 간단한 알고리즘 문제 해결 시에는 Stream API를 적용해 본 경험이 있지만,
프로젝트를 하다 보면 내부 구현이 복잡해져서 Stream API를 적용하지 못하고 for-loop를 사용해서 해결하곤 합니다.
그런데 실무에서는 Stream을 사용하여 가독성 있는 코드를 작성하는 경우가 있다고 하여, 프로젝트에 적용해 보려고 했습니다.
하지만 for-loop보다 Stream API를 사용했을 때 성능이 떨어질 수 있다는 포스트를 읽게 되어, 실제로 Stream API 사용이 성능에 영향을 미칠 수 있는지 확인해 보고자 했습니다.
Stream API
Stream API는 JDK 8부터 지원되는 기능으로 데이터 컬렉션을 함수형 방식으로 조합하여 원하는 결과를 필터링하거나 가공할 수 있도록 합니다.
간단하게 for-loop와 Stream API 방식으로 짝수의 합을 출력하는 프로그램을 작성해 봤습니다.
[ for-loop ]
import java.util.Arrays;
import java.util.List;
public class ForLoopExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
int sumOfEvens = 0;
for (int number : numbers) {
if (number % 2 == 0) {
sumOfEvens += number;
}
}
System.out.println("for-loop 결과 : " + sumOfEvens);
}
}
[ Stream API ]
import java.util.Arrays;
import java.util.List;
public class StreamExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
int sumOfEvens = numbers.stream()
.filter(n -> n % 2 == 0)
.mapToInt(Integer::intValue)
.sum();
System.out.println("Stream 결과 : " + sumOfEvens);
}
}
벤치마킹으로 성능을 비교해 보자
Java Stream API는 왜 for-loop보다 느릴까?
Angelika Langer가 JAX London 2015에서 발표했던 The Performance Model of Streams in Java 8
위의 포스트와 발표를 바탕으로 작성하였습니다!
for-loop과 Stream API를 사용했을 때 성능을 비교하기 위해서 다음 세 가지 상황을 가정했습니다.
- 원시 타입(Primitive Type)에서 for-loop와 Stream
- 래퍼타입(Wrapper Type)에서 for-loop와 Stream
- 비용이 높은 연산을 처리할 때 for-loop와 Stream
강연에서 진행한 벤치마킹을 기준으로 설명합니다.
벤치마킹에 사용한 기기의 사양은 Intel E8500 (2 x 3.17 GHZ, 4GB RAM), JDK 1.8입니다.
원시 타입(Primitive Type) 비교
50만 개의 원시 타입 int를 저장하는 배열을 만들고, 배열에서 가장 큰 원소를 찾는 함수를 for-loop와 Stream으로 작성했습니다.

벤치마킹 결과를 확인해 보겠습니다.

결과는 for-loop가 0.36ms, Stream이 5.35ms로 for-loop의 성능이 약 15배 정도 빠르게 측정되었습니다.
이러한 성능 차이가 발생하는 이유는 JIT Compiler가 for-loop에 대한 내부 최적화가 잘 되어 있기 때문이라고 설명합니다.
하지만, 항상 스트림을 사용하는 것이 loop보다 느리다는 것을 의미하지는 않습니다.
다음 예시인 래퍼 타입으로 실행한 결과를 확인해 보겠습니다.
래퍼 타입(Wrapped Type) 비교
이번에는 50만 개의 래퍼타입을 저장하는 ArrayList를 구성했습니다.

래퍼 타입을 사용한 경우 벤치마킹 결과를 확인해 보겠습니다.

래퍼 타입의 벤치마킹 결과 for-loop의 성능이 약 1.27배 정도 빨랐습니다.
원시 타입을 사용한 경우보다 격차가 크게 줄었다는 것을 확인할 수 있습니다.
그 이유는 ArrayList를 순회하는 비용이 for-loop와 Stream의 성능 격차를 압도할 만큼 크기 때문입니다.
이 순회 비용이 높은 이유는 JVM에서 메모리를 참조하는 방식에 있습니다.
원시 타입은 JVM에서 stack에 저장되어 직접 참조로 값을 바로 불러오지만, 래퍼 타입은 heap 영역에 저장됩니다.
힙에 있는 객체를 참조하기 위해서는 간접적인 주소 접근이 필요하여 추가적인 메모리 액세스가 필요하게 됩니다.
즉, 래퍼 타입을 사용하면 순회하는 과정의 비용이 높아, for-loop의 컴파일러 최적화 이점이 사라집니다.
비용이 높은 연산을 하게 된다면?
이전에는 루프 내부에서 단순하게 값을 비교하고 최댓값을 찾아 반환하도록 작성하였습니다.
그렇다면, for-loop와 Stream 내부에 복잡한 로직을 담은 함수를 구현하게 된다면 어떻게 될지 비교해 보겠습니다.

비용이 높은 계산을 위해서 Apache 라이브러리에서 제공하는 테일러급수 계산 함수를 사용했습니다.

📌 테스트 조건
- 데이터 크기: 10,000개
- 연산: slowSin() 함수 적용
📌 테스트
- 원시 타입 (int [], for-loop)
- 원시 타입 (int [], Stream)
- 래퍼 타입 (ArrayList <Integer>, for-loop)
- 래퍼 타입 (ArrayList <Integer>, Stream)
벤치마킹 결과를 확인해 보겠습니다.

벤치마킹 결과는 모두 비슷한 수치로 측정되었습니다.
for-loop 가 Stream보다 빠르지도 않고, 원시 타입을 사용하는 경우와 래퍼 타입을 사용하는 경우에도 차이가 없습니다.
즉, 벤치마킹 결과가 비용 높은 연산에 좌우된다는 것을 확인할 수 있습니다.
마치며
만약 순회 중에 발생하는 연산에 비용이 높은 연산을 적용하게 된다면, 크게 성능 차이를 가져갈 수 없습니다.
또한 비용이 높은 연산을 사용하지 않더라도, 래퍼 타입을 사용하는 경우에도 영향이 크지 않을 것입니다.
하지만 이번 결과가 모든 상황을 대표한다고 보기는 어렵습니다.
벤치마킹 결과는 실행 환경에 따라 달라질 수 있습니다.

실제로 발표 영상에 따르면 for-loop, 순차 스트림, 병렬 스트림을 비교해 보았을 때 실행 환경에 따른 차이를 보였습니다.
실제로 유의미한 성능을 비교하기 위해서는 실행 환경에서 벤치마킹 해야 한다고 합니다.
실제로 제 컴퓨터에서 코드를 작성하고 벤치마킹을 진행해 봤습니다.
50만 개의 데이터를 ArrayList에 담고, 최댓값을 구하는 메서드를 for-loop와 Stream으로 실행해 보았습니다.

실제 벤치마킹에서 for-loop가 Stream보다 약 2배 정도 빠르게 측정되었습니다.
이는 예상했던 결과와 달랐고, 실행 환경에 따라 벤치마킹 결과가 다를 수도 있다는 것을 알게 되었습니다.
프로젝트에서 성능이 중요한 부분에서는 for-loop를 사용하는 것이 적절하다고 생각합니다.
하지만 실무에서는 팀원과의 원활한 협업과 코드 가독성도 중요하기 때문에 필요에 따라 Stream을 적절히 섞어 사용하는 것이 좋다고 생각합니다.
참고
Java Stream API는 왜 for-loop보다 느릴까?
https://mong9data.tistory.com/131
'Teck Stack > Java' 카테고리의 다른 글
| [Java] Error와 Exception (0) | 2025.10.08 |
|---|---|
| [Java] JVM의 구조 (0) | 2025.09.27 |
| [JPA] Bulk Insert를 사용해서 쿼리 성능 개선하기 (0) | 2025.06.20 |
| BDDMockito.willReturn() 주의할 점 (0) | 2025.04.02 |