Stream API 개요
Java Stream API는 데이터 처리를 위한 강력한 도구이지만, 잘못 사용하면 오히려 성능이 저하될 수 있습니다. 이번 포스트에서는 Stream API의 효율적인 사용법과 병렬 스트림 활용 방법에 대해 알아보겠습니다.
병렬 스트림(Parallel Stream)
기본 사용법
// 순차 스트림
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.mapToInt(n -> n)
.sum();
// 병렬 스트림
int parallelSum = numbers.parallelStream()
.mapToInt(n -> n)
.sum();
// 기존 스트림을 병렬로 변환
int anotherSum = numbers.stream()
.parallel()
.mapToInt(n -> n)
.sum();
병렬 스트림이 효과적인 경우
// 1. 데이터가 많은 경우
List<Integer> largeList = new ArrayList<>(1000000);
// 리스트 초기화...
long count = largeList.parallelStream()
.filter(n -> n % 2 == 0)
.count();
// 2. 독립적인 연산이 많은 경우
double average = largeList.parallelStream()
.mapToDouble(this::complexCalculation)
.average()
.orElse(0.0);
private double complexCalculation(int number) {
// CPU 집약적인 계산
return Math.sqrt(Math.pow(number, 2));
}
성능 최적화 전략
적절한 데이터 구조 선택
// ArrayList - 병렬 처리에 좋음
List<Integer> arrayList = new ArrayList<>();
arrayList.parallelStream()...
// LinkedList - 병렬 처리에 비효율적
List<Integer> linkedList = new LinkedList<>();
linkedList.stream()... // 순차 처리 권장
// Array - 가장 효율적
int[] array = {1, 2, 3, 4, 5};
Arrays.stream(array).parallel()...
Unboxing 오버헤드 방지
// 비효율적인 방법 (boxing/unboxing 발생)
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.mapToInt(n -> n) // unboxing
.sum();
// 효율적인 방법
int[] primitiveNumbers = {1, 2, 3, 4, 5};
int sum = Arrays.stream(primitiveNumbers)
.sum();
적절한 중간 연산 사용
// 비효율적인 방법
List<String> result = strings.stream()
.filter(s -> s.length() > 3)
.sorted() // 전체 정렬 후 필터링
.limit(5)
.collect(Collectors.toList());
// 효율적인 방법
List<String> betterResult = strings.stream()
.filter(s -> s.length() > 3)
.limit(5) // 먼저 개수 제한
.sorted() // 필요한 요소만 정렬
.collect(Collectors.toList());
병렬 스트림 주의사항
상태 공유 피하기
// 잘못된 예 - 상태 공유로 인한 문제
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> result = new ArrayList<>();
numbers.parallelStream()
.map(n -> n * 2)
.forEach(result::add); // 동시성 문제 발생
// 올바른 예
List<Integer> safeResult = numbers.parallelStream()
.map(n -> n * 2)
.collect(Collectors.toList());
순서 의존성 주의
// 순서에 의존적인 작업 - 병렬 처리 부적합
String result = strings.parallelStream()
.reduce("", (a, b) -> a + b); // 순서 보장 안됨
// 올바른 방법
String betterResult = String.join("", strings); // 더 효율적
주의점
- 데이터 크기가 작은 경우 순차 처리가 더 효율적
- 공유 상태 수정은 피하기
- 올바른 데이터 구조 선택
- 순서 의존성 고려
- 성능 측정 후 판단
마치며
Stream API의 효율적인 사용과 병렬 스트림의 적절한 활용은 애플리케이션의 성능을 크게 향상시킬 수 있습니다. 하지만 무조건적인 병렬 처리가 아닌, 상황에 맞는 적절한 선택이 중요합니다.
'Language > JAVA' 카테고리의 다른 글
[JAVA] ThreadLocal에 대해서 알아보자 (0) | 2024.12.13 |
---|---|
[JAVA] StringTokenizer 에 대해서 (0) | 2024.11.14 |
[JAVA] Record와 Sealed (0) | 2024.11.13 |
[JAVA] 상속(Inheritance)과 복합(Composition) (0) | 2024.11.12 |
[JAVA] String + 연산을 왜 쓰지말아야할까 (0) | 2024.09.24 |