Stream이란 무엇인가
Stream은 대량의 데이터를 효과적으로 처리할 수 있도록 도와주는 데이터 처리 파이프라인이다. Stream을 활용하면 데이터를 선언적으로 처리할 수 있으며, 특히 병렬 처리와 함수형 프로그래밍 스타일을 지원하여 성능과 코드 가독성을 동시에 높일 수 있다.
Stream의 가장 큰 특징은 지연 평가(lazy evaluation)다. 이는 중간 연산이 바로 실행되지 않고, 최종 연산이 호출될 때 비로소 모든 연산이 수행된다는 것을 의미한다. 이로 인해 불필요한 연산을 줄이고 성능을 최적화할 수 있다.
Stream은 람다식과 함께 사용되며, 데이터를 필터링, 매핑, 정렬 등의 작업을 간단하고 직관적으로 처리할 수 있다.
Stream의 작동 원리
예를 들어, List<Integer>에 [1, 2, 3, 4, -1, -2, -3]이라는 데이터가 들어있다고 가정하자.
Stream은 생성된 이후 각 요소를 하나씩 처리한다. 데이터 처리 과정은 다음과 같이 이루어진다:
- 첫 번째 요소 1이 스트림에 들어간다.
- 1은 중간 연산으로 전달된다. (예: map(), filter())
- 중간 연산의 결과가 최종 연산으로 전달된다. (예: collect(), forEach())
- 두 번째 요소 2가 스트림에 들어간다.
- 2는 중간 연산으로 전달되고, 그 결과가 최종 연산으로 넘어간다.
- 세 번째 요소 3이 스트림에 들어간다.
- 3 역시 중간 연산을 거쳐 최종 연산으로 전달된다.
- 이 과정이 모든 요소에 대해 반복된다.
이렇게 Stream은 순차적으로 요소를 처리하며, 각 요소는 중간 연산을 거쳐 최종적으로 결과를 생성하게 된다. 이때 모든 연산이 하나의 파이프라인으로 연결되어 처리되기 때문에, 코드가 간결해지고 성능도 최적화된다.
또한, 필요에 따라 Stream은 병렬 처리를 사용할 수 있다. parallelStream()을 사용하면, 데이터가 여러 스레드에서 동시에 처리되어 성능을 더욱 높일 수 있다.
Stream의 특징 정리
- 데이터 처리 파이프라인: 데이터를 필터링, 매핑, 수집하는 작업을 파이프라인 방식으로 연결하여 처리.
- 지연 평가(Lazy Evaluation): 중간 연산은 최종 연산이 호출되기 전까지 실제로 실행되지 않음.
- 병렬 처리(Parallel Processing): parallelStream()을 사용하여 대량의 데이터를 병렬로 처리할 수 있음.
- 람다식과 함수형 프로그래밍: 데이터를 선언적으로 처리할 수 있으며, 메서드 참조나 람다식을 통해 코드를 간결하게 작성할 수 있음.
Stream은 대량의 데이터를 효과적으로 처리할 수 있는 강력한 도구로, Java 8 이후 널리 사용되고 있다.
// 리스트 생성
List<String> list = Arrays.asList("apple", "banana", "avocado", "apricot", "grape");
// "a"로 시작하는 문자열의 길이를 저장할 리스트
List<Integer> lengths = new ArrayList<>();
// 반복문을 이용해 필터링과 변환 작업 수행
for (String s : list) {
if (s.startsWith("a")) { // 필터링
lengths.add(s.length()); // 변환 후 리스트에 추가
}
}
stream을 사용하지 않았을때 코드
// 리스트 생성
List<String> list = Arrays.asList("apple", "banana", "avocado", "apricot", "grape");
// Stream API를 사용해 필터링, 변환, 수집 작업을 간단하게 수행
List<Integer> lengths = list.stream() // 스트림 생성
.filter(s -> s.startsWith("a")) // 필터링
.map(String::length) // 변환
.collect(Collectors.toList()); // 리스트로 수집
stream을 사용했을때 코드
이건 혁신적인 item 이다.
이제 사용법에 대해서 알아보고 직접 사용해보도록 하자.
Stream 생성
Stream을 생성하는 방법은 여러 가지가 있다. 일반적으로 Stream은 컬렉션에서 파생되거나 배열에서 생성된다.
List<String> list = Arrays.asList("apple", "banana", "orange"); Stream<String> stream = list.stream();
또는 배열로부터:
Stream<String> stream = Stream.of("apple", "banana", "orange");
중간 연산 (Intermediate Operations)
중간 연산은 다른 Stream을 반환하며, 이 연산들은 최종 연산이 호출될 때까지 지연된다.
filter(Predicate<? super T> predicate)
- 조건을 만족하는 요소만 필터링.
- 반환 타입: Stream<T>
stream.filter(s -> s.startsWith("a"));
map(Function<? super T, ? extends R> mapper)
- 요소를 다른 타입으로 변환하거나, 특정 연산을 적용.
- 반환 타입: Stream<R>
stream.map(String::toUpperCase);
flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)
- 각 요소를 스트림으로 변환하고, 그 스트림을 하나로 평면화.
- 반환 타입: Stream<R>
stream.flatMap(s -> Stream.of(s.split("")));
sorted()
- 요소들을 정렬합니다. 기본적으로 자연 순서(Comparable 인터페이스 구현 가능)에 따라 정렬.
- 반환 타입: Stream<T>
stream.sorted();
sorted(Comparator<? super T> comparator)
- 주어진 비교자에 따라 요소를 정렬.
- 반환 타입: Stream<T>
stream.sorted(Comparator.reverseOrder());
distinct()
- 중복된 요소를 제거.
- 반환 타입: Stream<T>
stream.distinct();
limit(long maxSize)
- 지정한 수만큼 요소를 제한.
- 반환 타입: Stream<T>
stream.limit(2);
skip(long n)
- 처음 n개의 요소를 건너뜁.
- 반환 타입: Stream<T>
stream.skip(1);
최종 연산 (Terminal Operations)
최종 연산은 Stream을 소비하고, 결과를 반환하거나 부작용(예: 요소 출력)을 일으킨다. 최종 연산이 호출되면 Stream은 더 이상 사용할 수 없다.
forEach(Consumer<? super T> action)
- 각 요소에 대해 주어진 작업을 수행.
- 반환 타입: void
stream.forEach(System.out::println);
collect(Collector<? super T, A, R> collector)
- Stream의 요소들을 컬렉션으로 변환하거나, 집계 작업을 수행한다. 가장 많이 사용되는 방식은 Collectors 유틸리티 클래스를 사용하는 것이다.
- 반환 타입: R (예: List, Set, Map 등)
List<String> result = stream.collect(Collectors.toList());
reduce(T identity, BinaryOperator<T> accumulator)
- Stream의 요소들을 하나의 값으로 합친다. 초기값(identity)을 지정하고, 그 이후에 요소들을 누적(accumulate)한다.
- 반환 타입: T
int sum = stream.reduce(0, Integer::sum);
findFirst()
- 첫 번째 요소를 반환. 요소가 없으면 Optional 빈 값이 반환.
- 반환 타입: Optional<T>
Optional<String> first = stream.findFirst();
findAny()
- 스트림에서 임의의 요소를 반환. 병렬 스트림에서 유용할 수 있다.
- 반환 타입: Optional<T>
Optional<String> any = stream.findAny();
count()
- 스트림의 요소 수를 반환.
- 반환 타입: long
long count = stream.count();
anyMatch(Predicate<? super T> predicate)
- 주어진 조건을 만족하는 요소가 하나라도 있으면 true를 반환.
- 반환 타입: boolean
boolean hasApple = stream.anyMatch(s -> s.equals("apple"));
allMatch(Predicate<? super T> predicate)
- 모든 요소가 주어진 조건을 만족하면 true를 반환.
- 반환 타입: boolean
boolean allMatch = stream.allMatch(s -> s.length() > 3);
noneMatch(Predicate<? super T> predicate)
- 모든 요소가 주어진 조건을 만족하지 않으면 true를 반환.
- 반환 타입: boolean
boolean noneMatch = stream.noneMatch(s -> s.isEmpty());
병렬 스트림
스트림을 병렬로 처리하면 여러 요소를 동시에 처리할 수 있어 성능을 높일 수 있다. 단, 데이터의 일관성을 위해 병렬 처리를 신중하게 사용해야 한다.
List<String> list = Arrays.asList("apple", "banana", "orange"); list.parallelStream().forEach(System.out::println);
이상 도움이 되었길 바라며 공부일지 마친다.
'Spring' 카테고리의 다른 글
Lombok 에 대해서 (0) | 2024.12.04 |
---|---|
SOLID 객체지향설계 5원칙 (0) | 2024.01.31 |