Understanding the Stream API in Java 8
Java 8에서 Stream API는 대량 데이터를 처리하는 방식을 혁신했다. Stream은 데이터를 추상화하여 필터링, 변환, 축소 등의 작업을 선언형으로 수행할 수 있도록 한다. 이는 컬렉션(Collection) API와 함께 자주 사용되며, 더 나은 가독성과 효율적인 코드를 작성하는 데 큰 도움이 된다.
What is a Stream?
Stream은 데이터의 흐름을 나타내며, 원본 데이터를 변경하지 않고 다양한 중간 연산과 최종 연산을 통해 데이터를 처리할 수 있다.
Stream API는 두 가지 연산으로 나뉜다.
- 중간 연산 (Intermediate Operations): 필터링, 매핑 등으로 데이터를 변환하거나 추출한다. 이 연산은 지연 평가(lazy evaluation) 방식으로, 최종 연산이 호출될 때까지 실제로 실행되지 않는다.
- 최종 연산 (Terminal Operations): 스트림을 처리해 결과를 반환하는 연산이다. 최종 연산이 호출되면 스트림은 닫히고 더 이상 사용할 수 없다.
Stream API의 특징
- 선언형 코드: 반복문을 사용하지 않고, 선언적으로 데이터 흐름을 표현할 수 있다.
- 지연 평가: 중간 연산은 최종 연산이 호출되기 전까지 실제로 실행되지 않아 성능을 향상시킨다.
- 병렬 처리: 멀티코어 환경에서 병렬로 데이터를 처리할 수 있어 성능을 극대화할 수 있다.
Stream API Examples
Example 1: Filtering and Collecting
아래 코드는 List<String>
에서 문자열 길이가 3인 문자열을 필터링한 후, 결과를 새로운 리스트로 수집하는 예시다.
@Test
void filtering_with_stream(){
List<String> names = Arrays.asList("John", "Jane", "Tom", "Jerry");
List<String> filteredNames = names.stream()
.filter(name -> name.length() == 3)
.toList();
assertEquals("Tom", filteredNames.get(0));
}
여기서 Collectors.toList()
는 Collector 인터페이스의 구현체로, 스트림의 요소들을 리스트로 수집하는 데 사용된다. Collectors는 Stream API의 여러 최종 연산에서 사용되는 다양한 수집 연산을 제공한다. 이를 통해 데이터를 특정 컬렉션 형태로 변환할 수 있다.
Example 2: Mapping and Sorting with Collectors
Stream의 map() 연산은 데이터를 변환하는 강력한 도구이다. 아래 예시는 문자열 리스트를 대문자로 변환하고, 알파벳 순으로 정렬한 후 이를 다시 리스트로 수집하는 예시다.
@Test
void mapping_and_sorting(){
List<String> names = Arrays.asList("John", "Jane", "Tom", "Jerry");
List<String> sortedNames = names.stream()
.map(String::toUpperCase) // 중간 연산: 대문자로 변환
.sorted() // 중간 연산: 알파벳 순으로 정렬
.toList();
assertEquals("JANE", sortedNames.get(0));
assertEquals("JERRY", sortedNames.get(1));
assertEquals("JOHN", sortedNames.get(2));
assertEquals("TOM", sortedNames.get(3));
}
이 코드에서 .collect(Collectors.toList())
는 최종 연산으로, 대문자로 변환되고 정렬된 결과를 리스트로 수집한다. Collectors 클래스는 여러 종류의 수집 연산을 제공하며, 예를 들어 toList()
, toSet()
, toMap()
등 다양한 데이터 구조로 결과를 수집할 수 있다. 이를 통해 스트림을 매우 유연하게 처리할 수 있다.
Sorting with Comparator
위 예시에서는 기본 알파벳 순서로 정렬했지만, Comparator를 사용하면 정렬 기준을 사용자 정의할 수 있다. Comparator는 Java에서 객체를 비교하고 정렬하는 데 사용되는 인터페이스이다.
@Test
void mapping_and_reverse_sorting_with_comparator(){
List<String> names = Arrays.asList("John", "Jane", "Tom", "Jerry");
List<String> sortedNames = names.stream()
.map(String::toUpperCase)
.sorted(Comparator.reverseOrder())
.toList();
assertEquals("TOM", sortedNames.get(0));
assertEquals("JOHN", sortedNames.get(1));
assertEquals("JERRY", sortedNames.get(2));
assertEquals("JANE", sortedNames.get(3));
}
Comparator의 다양한 사용 방법
• 기본 정렬: .sorted()
는 요소의 자연 정렬(알파벳 순 등)을 따른다.
• 내림차순 정렬: .sorted(Comparator.reverseOrder())
는 기본 정렬을 뒤집어 내림차순으로 정렬한다.
• 사용자 정의 정렬: Comparator.comparing()
을 사용하면 객체의 특정 필드를 기준으로 정렬할 수 있다.
예를 들어, 아래는 문자열의 길이를 기준으로 정렬하는 예시이다.
@Test
void mapping_and_sorting_by_field(){
List<String> names = Arrays.asList("James", "Jane", "Tom", "Verstapen");
List<String> sortedNames = names.stream()
.map(String::toUpperCase)
.sorted(Comparator.comparingInt(String::length))
.toList();
assertEquals("TOM", sortedNames.get(0));
assertEquals("JANE", sortedNames.get(1));
assertEquals("JAMES", sortedNames.get(2));
assertEquals("VERSTAPEN", sortedNames.get(3));
}
여기서 Comparator.comparingInt()
는 문자열의 길이를 기준 오름차순으로 정렬한다.
Stream의 병렬 처리 (Parallel Streams)
Java 8의 Stream API는 병렬 처리를 매우 쉽게 구현할 수 있는 기능을 제공한다. 병렬 처리는 데이터를 여러 스레드에서 동시에 처리할 수 있게 하여 대용량 데이터 처리 성능을 크게 향상시킨다. parallelStream()
을 사용하면 스트림이 자동으로 병렬 처리되어 여러 CPU 코어를 활용할 수 있다. 다음 예시는 병렬 스트림을 통해 데이터를 효율적으로 처리하는 예시다.
@Test
void stream_with_parallel_streams(){
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
int sum = numbers.parallelStream() // 병렬 스트림 생성
.filter(n -> n % 2 == 0) // 중간 연산: 짝수 필터링
.mapToInt(n -> n) // 중간 연산: int로 변환
.sum(); // 최종 연산: 합계 계산
assertEquals(30, sum);
}
위 예시에서는 parallelStream()
을 사용하여 스트림을 병렬로 처리한다. 병렬 스트림은 내부적으로 데이터를 여러 스레드에서 나누어 처리하기 때문에 대량의 데이터를 빠르게 처리할 수 있다.
병렬 스트림의 장점과 주의점
병렬 스트림을 사용하면 대량의 데이터를 처리할 때 성능을 크게 향상시킬 수 있다. 하지만 모든 경우에 병렬 처리가 최적의 성능을 제공하는 것은 아니다. 다음과 같은 상황에서는 주의가 필요하다:
- 작은 데이터셋: 데이터셋이 작을 경우 병렬 처리 오버헤드가 발생할 수 있다. 오히려 성능이 느려질 수 있다.
- 데이터 순서: 병렬 스트림을 사용할 경우 데이터의 순서가 보장되지 않을 수 있다. 정렬이 중요한 경우에는 .sorted()와 같은 메서드를 추가로 사용해야 한다.
- 상태 공유: 병렬 처리는 여러 스레드에서 동시에 실행되기 때문에 공유된 상태를 변경하는 작업을 피해야 한다. 그렇지 않으면 동기화 문제가 발생할 수 있다.
Conclusion
Java 8의 Stream API는 데이터를 선언적으로 처리할 수 있는 강력한 도구이다. 필터링, 매핑, 정렬, 수집 등의 작업을 간결하게 처리할 수 있으며, 특히 병렬 스트림을 통해 성능을 최적화할 수 있다. Collectors를 사용해 다양한 방식으로 데이터를 수집하고 가공할 수 있다는 점도 매우 유용하다. Comparator를 통해 정렬 기준을 사용자 정의하여 더욱 유연하게 데이터를 다룰 수 있다. Stream API를 적절하게 활용하면 더 가독성 높고 효율적인 코드를 작성할 수 있다.
'개발' 카테고리의 다른 글
Java8 Lambda tutorial (4) | 2024.10.23 |
---|---|
ThreadPoolTaskExecutor와 CompletableFutre를 사용하여 비동기처리하기 (0) | 2024.09.30 |
FeignClient에서 네이밍 전략 변환하기 (2) | 2024.01.11 |
분산환경은 로깅을 어떻게 할까 (1) | 2023.12.05 |
분산환경은 서비스 트레이싱을 어떻게 할까 (1) | 2023.11.30 |