Java에서 데이터를 반복 처리할 때 for-loop와 Stream API를 많이 사용합니다. 하지만 튜터님께 피드백 받은 결과 코드의 가독성을 위해 stream으로 전부 통일했습니다. 하지만 stream을 쓰는데 속도의 차이가 얼마나 발생하는 지 정도를 알고 작성하는 것이 중요하다 생각하여 직접 실험해보고 각 방법의 속도 차이를 알아보고 어떤 상황에 쓰면 좋을 지를 생각해보려고 합니다.
처음에는 List<Integer>
를 사용하여 테스트했지만, 튜터님의 피드백을 받고 int[]
배열을 사용하여 언박싱 문제를 해결했습니다. 이를 통해 더 정확한 성능 비교를 할 수 있었습니다.
처음에는 List<Integer>
를 사용하여 Stream과 for-loop를 비교했습니다. 하지만, 오토 박싱/언박싱(auto boxing/unboxing)으로 인해 Integer
객체가 생성되고 성능 차이가 발생했습니다.
List<Integer> numbers = IntStream.rangeClosed(1, size)
.boxed()
.collect(Collectors.toList());
🚨 문제점:
Integer
→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();
}
}
데이터 크기 | For Loop Avg (ns) | Stream Avg (ns) | Parallel Stream Avg (ns) |
---|---|---|---|
100 | 3,837 | 29,435 | 214,368 |
10,000 | 95,577 | 134,558 | 204,162 |
1,000,000 | 685,322 | 1,049,581 | 303,797 (병렬 효과) |
For Loop가 가장 빠름
Stream API는 유지보수성이 높음
filter()
, map()
, reduce()
등 다양한 기능 활용 가능.Parallel Stream은 대량 데이터에서 효과적
1,000,000
개 이상의 데이터에서는 병렬 처리로 성능이 크게 향상됨.데이터의 크기가 커질수록 for문이 stream보다 처리량이 1.5배 빠릅니다. parallel stream의 경우 데이터의 크기가 작으면 처리 속도가 오래걸리지만 데이터가 엄청나게 커진다면 다른 두 방법보다 확연히 빠른 것을 알 수 있습니다.
💡 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
를 고려하는 것이 좋습니다!