본문으로 건너뛰기
개발 머꼬
개발 노트Java
hohyeon.dev16

Stream API로 바꿨더니 오히려 느려진 이유

  • #Engineering Note
  • #Java
  • #Performance
  • #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문보다 빠르다는 보장은 없습니다. 원인은 크게 세 가지입니다.

  1. 람다/메서드 참조 호출 오버헤드: 각 요소마다 함수형 인터페이스 호출이 일어나는데, JIT가 인라인 최적화를 하지 못하면 일반 반복문보다 느립니다.
  2. 박싱/언박싱: Stream<Integer>처럼 제네릭 Stream을 쓰면 매 요소가 intInteger로 박싱/언박싱됩니다. IntStream/LongStream/DoubleStream 같은 primitive 특화 Stream을 안 쓰면 이 비용이 누적됩니다.
  3. 중간 연산의 오버헤드: filter().map().collect()처럼 체이닝이 길어지면 각 단계마다 객체 생성과 함수 호출이 늘어납니다. 작은 컬렉션(수십~수백 개)에서는 이 오버헤드가 실제 작업량보다 커질 수 있습니다.

반대로 parallelStream()은 코어 수가 많고 데이터가 충분히 크며 각 요소 처리가 독립적일 때는 for문보다 확실히 빠를 수 있습니다. 작은 데이터셋에 parallelStream()을 쓰면 스레드 풀 분배 오버헤드가 실제 이득보다 커서 오히려 손해입니다.

해결 방안

  1. 성능이 실제로 중요한 hot path(자주 반복 호출되는 코드)라면 Stream보다 for문을 우선 고려하고, 반드시 필요하면 JMH로 실측합니다.
  2. Stream을 쓴다면 IntStream.of()/mapToInt()처럼 primitive 특화 버전을 써서 박싱 비용을 없앱니다.
  3. parallelStream()은 "코어가 여러 개니까 무조건 빠르겠지"라는 가정으로 쓰지 않습니다 — 데이터 크기와 작업의 독립성을 먼저 따집니다.
  4. 대부분의 비즈니스 로직에서는 가독성과 유지보수성이 마이크로초 단위 성능 차이보다 중요합니다 — 실제로 병목인 지점을 프로파일러로 먼저 찾은 뒤에 최적화합니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.