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를 고려하는 것이 좋습니다!