Stream API로 바꿨더니 오히려 느려진 이유
문제 발생
가독성을 위해 for문을 Stream으로 리팩터링했는데, 벤치마크를 돌려보니 오히려 실행 시간이 늘어난 경우가 있습니다.
// for문
int sum = 0;
for (int n : numbers) {
sum += n;
}
// Stream
int sum = numbers.stream().mapToInt(Integer::intValue).sum();원인 분석
Stream은 편의성과 병렬 처리(parallelStream())를 위한 추상화이지, 단순 순차 처리에서 항상 for문보다 빠르다는 보장은 없습니다. 원인은 크게 세 가지입니다.
- 람다/메서드 참조 호출 오버헤드: 각 요소마다 함수형 인터페이스 호출이 일어나는데, JIT가 인라인 최적화를 하지 못하면 일반 반복문보다 느립니다.
- 박싱/언박싱:
Stream<Integer>처럼 제네릭 Stream을 쓰면 매 요소가int↔Integer로 박싱/언박싱됩니다.IntStream/LongStream/DoubleStream같은 primitive 특화 Stream을 안 쓰면 이 비용이 누적됩니다. - 중간 연산의 오버헤드:
filter().map().collect()처럼 체이닝이 길어지면 각 단계마다 객체 생성과 함수 호출이 늘어납니다. 작은 컬렉션(수십~수백 개)에서는 이 오버헤드가 실제 작업량보다 커질 수 있습니다.
반대로 parallelStream()은 코어 수가 많고 데이터가 충분히 크며 각 요소 처리가 독립적일 때는 for문보다 확실히 빠를 수 있습니다. 작은 데이터셋에 parallelStream()을 쓰면 스레드 풀 분배 오버헤드가 실제 이득보다 커서 오히려 손해입니다.
해결 방안
- 성능이 실제로 중요한 hot path(자주 반복 호출되는 코드)라면 Stream보다 for문을 우선 고려하고, 반드시 필요하면 JMH로 실측합니다.
- Stream을 쓴다면
IntStream.of()/mapToInt()처럼 primitive 특화 버전을 써서 박싱 비용을 없앱니다. parallelStream()은 "코어가 여러 개니까 무조건 빠르겠지"라는 가정으로 쓰지 않습니다 — 데이터 크기와 작업의 독립성을 먼저 따집니다.- 대부분의 비즈니스 로직에서는 가독성과 유지보수성이 마이크로초 단위 성능 차이보다 중요합니다 — 실제로 병목인 지점을 프로파일러로 먼저 찾은 뒤에 최적화합니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.