속도는 for-loop 문이 빠른데 왜 Stream API을 쓸까?

박화랑·2025년 3월 6일
3

Spring_6기

목록 보기
9/15

1. 서론

Java에서 데이터를 반복 처리할 때 for-loopStream API를 많이 사용합니다. 하지만 튜터님께 피드백 받은 결과 코드의 가독성을 위해 stream으로 전부 통일했습니다. 하지만 stream을 쓰는데 속도의 차이가 얼마나 발생하는 지 정도를 알고 작성하는 것이 중요하다 생각하여 직접 실험해보고 각 방법의 속도 차이를 알아보고 어떤 상황에 쓰면 좋을 지를 생각해보려고 합니다.

처음에는 List<Integer>를 사용하여 테스트했지만, 튜터님의 피드백을 받고 int[] 배열을 사용하여 언박싱 문제를 해결했습니다. 이를 통해 더 정확한 성능 비교를 할 수 있었습니다.


2. 코드 구현

☞ 초기 코드 (List 사용) → 개선 필요

처음에는 List<Integer>를 사용하여 Stream과 for-loop를 비교했습니다. 하지만, 오토 박싱/언박싱(auto boxing/unboxing)으로 인해 Integer 객체가 생성되고 성능 차이가 발생했습니다.

List<Integer> numbers = IntStream.rangeClosed(1, size)
                                 .boxed()
                                 .collect(Collectors.toList());

🚨 문제점: Integerint 변환 과정에서 성능 손실 발생.


💡 개선된 코드 (int[] 사용)

튜터님의 피드백을 반영하여 int[] 배열을 사용하여 오토 언박싱 문제를 해결했습니다.

int[] numbers = IntStream.rangeClosed(1, size).toArray();

이제 int[]을 사용하여 for-loop, Stream, Parallel Stream을 비교합니다.


✅ 최적화된 코드

import java.util.function.ToIntFunction;
import java.util.stream.IntStream;

public class Main {
    private static final int ITERATIONS = 20; // 반복 실행 횟수 증가

    public static void main(String[] args) {
        int[] sizes = {100, 10_000, 1_000_000};

        for (int size : sizes) {
            int[] numbers = generateNumbers(size);

            System.out.println("Data Size: " + size);

            // JVM Warm-up 실행 (첫 번째 실행은 무시)
            warmUpJVM(numbers);

            // For Loop 실행 시간 평균 계산
            long avgForLoop = calculateAverageTime(numbers, Main::sumUsingForLoop);
            System.out.println("For Loop Avg Execution Time: " + avgForLoop + " ns");

            // Stream 실행 시간 평균 계산
            long avgStream = calculateAverageTime(numbers, Main::sumUsingStream);
            System.out.println("Stream Avg Execution Time: " + avgStream + " ns");

            // Parallel Stream 실행 시간 평균 계산
            long avgParallelStream = calculateAverageTime(numbers, Main::sumUsingParallelStream);
            System.out.println("Parallel Stream Avg Execution Time: " + avgParallelStream + " ns");

            System.out.println("----------------------------------");
        }
    }

    // 특정 연산을 여러 번 실행한 후 평균 실행 시간을 반환
    private static long calculateAverageTime(int[] numbers, ToIntFunction<int[]> method) {
        long totalTime = 0;

        for (int i = 0; i < ITERATIONS; i++) {
            System.gc(); // Garbage Collector 실행으로 메모리 영향 최소화
            long startTime = System.nanoTime();
            method.applyAsInt(numbers);
            long endTime = System.nanoTime();
            totalTime += (endTime - startTime);
        }

        return totalTime / ITERATIONS;
    }

    // JVM Warm-up을 위한 더미 실행 (결과는 무시)
    private static void warmUpJVM(int[] numbers) {
        sumUsingForLoop(numbers);
        sumUsingStream(numbers);
        sumUsingParallelStream(numbers);
    }

    // 주어진 크기의 숫자 배열 생성
    public static int[] generateNumbers(int size) {
        return IntStream.rangeClosed(1, size).toArray();
    }

    // For Loop을 사용한 합산
    public static int sumUsingForLoop(int[] numbers) {
        int sum = 0;
        for (int num : numbers) {
            sum += num;
        }
        return sum;
    }

    // Stream을 사용한 합산
    public static int sumUsingStream(int[] numbers) {
        return IntStream.of(numbers)
                        .sum();
    }

    // Parallel Stream을 사용한 합산
    public static int sumUsingParallelStream(int[] numbers) {
        return IntStream.of(numbers)
                        .parallel()
                        .sum();
    }
}

3. 실행 결과

데이터 크기For Loop Avg (ns)Stream Avg (ns)Parallel Stream Avg (ns)
1003,83729,435214,368
10,00095,577134,558204,162
1,000,000685,3221,049,581303,797 (병렬 효과)

4. 결과 분석

  1. For Loop가 가장 빠름

    • 작은 데이터에서는 for-loop가 Stream보다 훨씬 빠름.
    • Stream은 내부적으로 람다식, 필터링 등의 오버헤드가 존재.
  2. Stream API는 유지보수성이 높음

    • 가독성이 좋고, 유지보수 측면에서 유리함.
    • filter(), map(), reduce() 등 다양한 기능 활용 가능.
  3. Parallel Stream은 대량 데이터에서 효과적

    • 1,000,000개 이상의 데이터에서는 병렬 처리로 성능이 크게 향상됨.
    • 하지만 작은 데이터에서는 오히려 오버헤드가 커서 비효율적.

    데이터의 크기가 커질수록 for문이 stream보다 처리량이 1.5배 빠릅니다. parallel stream의 경우 데이터의 크기가 작으면 처리 속도가 오래걸리지만 데이터가 엄청나게 커진다면 다른 두 방법보다 확연히 빠른 것을 알 수 있습니다.


5. 결론

💡 for-loop는 성능이 가장 뛰어나지만, Stream API는 가독성과 유지보수성에서 장점이 있다.
💡 대량 데이터를 처리할 경우, Parallel Stream이 가장 빠르다.

작은 데이터: for-loop 문을 사용하는 걸 추천하나 협업 시에는 stream을 쓰는 것이 가독성과 유지보수의 용이도 중요한 이슈이므로 stream을 추천합니다.
중간 규모 데이터: stream() 사용 (가독성 & 유지보수성 Good)
대량 데이터 (1,000,000+): parallelStream() 사용하면 성능 향상을 확실히 느끼실 수 있습니다.


🏁 마무리

이번 실험을 통해 매우 많은 데이터(1,000,000개 이상)가 아닌 이상 for-loop가 성능적으로 가장 뛰어나다는 점을 확인할 수 있었습니다. 작은 데이터에서는 for-loop가 Stream보다 빠르며, 불필요한 오버헤드가 없다는 것이 장점입니다. 하지만 개발자들이 Stream API를 많이 사용하는 이유는 성능뿐만 아니라 유지보수성과 가독성이 중요한 요소이기 때문입니다.

Stream API는 선언형 스타일로 데이터를 처리할 수 있어 코드를 간결하게 작성할 수 있고, 유지보수가 쉽다는 장점이 있습니다. 또한, filter(), map(), reduce() 같은 다양한 기능을 활용할 수 있어 복잡한 데이터 변환이 필요한 경우 유용합니다.

또한, 대량 데이터를 다룰 때는 parallelStream()을 사용하면 병렬 처리를 통해 성능을 향상시킬 수 있습니다. 하지만 작은 데이터에서는 병렬 처리의 오버헤드가 오히려 성능을 저하시킬 수 있으므로 주의가 필요합니다.

결국, 성능이 중요하긴 하지만, 코드의 유지보수성과 가독성 또한 고려해야 합니다. 개발자는 단순한 연산이라면 for-loop을 선택할 수 있지만, 보다 복잡한 데이터 처리 및 유지보수성을 고려한다면 Stream API를 적극 활용하는 것이 좋은 선택이 될 수 있습니다.

❕❕❕❕ 정리하자면, ❕❕❕❕❕

  • 작은 데이터에서는 for-loop가 성능적으로 유리하다.
  • Stream API는 가독성과 유지보수성을 높일 수 있다.
  • 대량 데이터에서는 parallelStream()이 성능을 향상시킬 수 있다.
  • 속도도 중요하지만, 유지보수성과 코드의 가독성도 고려해야 한다.

따라서, 단순한 반복 작업이라면 for-loop가 유리하지만, 보다 직관적이고 유지보수하기 쉬운 코드를 작성하려면 Stream API를 고려하는 것이 좋습니다!


참고

profile
개발자 희망생

0개의 댓글

관련 채용 정보