728x90
6. 스트림을 이용한 데이터 처리
- 자바 8에서 도입된
스트림 API
는 데이터 처리에 대해 선언적이고 지연 평가된 접근법을 제공한다.
6.1 반복을 통한 데이터 처리
외부 반복
- FOR-LOOP
- 명확한 단점은 반복 기반 루프에 필요한 보일러 플레이트 코드가 많다.
- 루프의 바디에는 continue와 break의 형태로 반복 과정에 대한 의사 결정 및 여러 문장까지 포함할 수 있다.
- 연속적인 순회 과정이 필요하며, 병렬 데이터 처리가 필요한 경우 전체 루프 재작성해야 하며, 그 과정에서
ConcurrentModificationException
과 같은 복잡한 문제를 해결해야한다.
public class ExternalIteration {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = 0;
for (int number : numbers) {
sum += number;
}
System.out.println("합: " + sum); // Output: Sum: 15
}
}
내부 반복
- 개발자가 순회 과정을 직접 제어하는 것을 포기하고, 데이터 소스 자체가
어떻게 수행되는지
를 담당하도록 한다. - 순회를 직접 제어하기 위해 Iterator 대신, 데이터 처리 로직은 스스로 반복을 수행하는 파이프라인을 사전에 구성한다.
public class InternalIteration {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.mapToInt(Integer::intValue)
.sum();
System.out.println("합: " + sum); // Output: Sum: 15
}
}
6.2 함수형 데이터 파이프라인으로써의 스트림
- 내부 반복자의 장점이 있으며, 함수형 관점에서 유리하다.
- 스트림은
느긋한 순차 데이터 파이프라인
으로 간단하게 설명할 수 있다.
선언적 접근법
- 단일 호출 체인을 통해 간결하고 명료한 다단계 데이터 파이프라인을 구축한다.
조합성
- 데이터 처리 로직을 위한 고차 함수로 이루어져 있으며, 필요에 따라 조합하여 사용할 수 있다.
지연 처리
- 모든 요소를 일일이 순회하는 대신, 마지막 연산이 파이프라인에 연결된 이후에 각 요소가 하나씩 순차적으로 파이프 라인을 통해 처리됩니다.
public class LazyExample {
public static void main(String[] args) {
List<String> words = Arrays.asList("apple", "banana", "cherry", "date");
words.stream()
.filter(word -> {
System.out.println("필터: " + word);
return word.length() > 5;
})
.map(word -> {
System.out.println("맵: " + word);
return word.toUpperCase();
})
.forEach(word -> System.out.println("반복: " + word));
}
}
성능 최적화
- 데이터 소스와 사용된 연산의 종류에 따라 순회 방식을 자동으로 최적화합니다.
병렬 데이터 처리
- 내장된 병렬처리 기능이 있어, 호출 체인에서 단일 호출을 간단히 변경해서 활용할 수 있다.
흐름
- 스트림 생성
- 기존의 데이터 소스에서 스트림을 생성한다.
- 연속적인 요소를 제공할 수 있다면 스트림으로 변환할 수 있다.
- 작업 수행
- 중간 연산이라 불리는 것들은
java.util.stream.Stream<T>
의 메서드로 제공되는 고차 함수들로, 파이프 라인을 통과하는 요소들을 대상으로 하여 필터링, 매핑, 정렬 등 여러 작업을 수행한다. - 각 연산은 새로운 스트림을 반환하며, 원하는 만큼의 다양한 중간 연산과 연결될 수 있다.
- 중간 연산이라 불리는 것들은
- 결과 얻기
- 데이터 처리 파이프라인을 완료하기 위해 스트림 대신 결과를 반환하는 마지막 종료 연산이 필요하다.
스트림의 특성
느긋한 계산법
- 스트림에서 중간 연산을 수행할 때 즉각적으로 싱행되지 않는다. 해당 호출은 파이프라인을 단순히
확장
하고 새롭게 지연 평가된 스트림을 반환한다. - 모든 연산을 모아놓고, 종료 연산을 호출하기 전까지 아무런 작업도 시작하지 않는다.
- 스트림은 데이터 소스로써, 누군가 더 많은 요소를 요청하지 않는 한 요소를
과도하게 제공
하거나 버퍼링할 필요가 없다. - 스트림 요소의 흐름은
깊이 우선
방식을 따르며, 이를 통해 필요한 CPU 주기, 메모리 사용량, 스택의 깊이를 줄일 수 있다.
(대부분) 상태 및 간섭 없음
- 스트림은 변경 불편한 원칙을 따른다.
- 스트림은 간섭하지 않고 통과하는 파이프라인으로, 해당 요소들이 가능한 한 자유롭게, 간섭 없이 통과해야 한다.
최적화
- 스트림은 성능 햐상을 위해 여러 전략을 활용한다
- (무상태의) 연산 융합
- 불필요한 연산 제거
- 단축 파이프라인 경로
보일러 플레이트 최소화
filter
,map
,findFirst
와 같이 간결하면서도 직관적인 연산으로, 데이터 처리 로직의 복잡성을 명료하고 표현력 있게 풀어낸다.
재사용 불가능
- 스트림 파이프라인은 단 한 번만 사용할 수 있다. 스트림 파이프라인은 데이터 소스에 연결되어 있으며, 종료 연선이 호출된 후 정확히 한 번만 전달된다.
원시 스트림
- 스트림과 그 변형(
IntStream, LongStream, DoubleStream
)들은 모두BaseStream
이라는 기본 인터페이스를 기반으로 한다.
쉬운 병렬화
- 스트림은 처음부터 병렬 실행을 효율적으로 지원하도록 설계되었고, 자바 7에서 도입된
Fork/Join
프레임워크를 기반으로 한다. - 모든 스트림 파이프라인이 병렬 처리에 적합하지 않다. 원본은 충분한 데이터를 포함해야 하며, 연산이 여러 스레드의 오버헤드를 수용할 만큼의 비용을 감당할 수 있어야 한다.
예외 처리의 한계
- 스트림은 데이터 처리에 함수형 접근 방식을 도입하여 코드를 더 간결하게 만들어준다.
스트림의 핵심, Spliterator
- 스트림도 자체 반복 인터페이스인
java.util.Spliterator<T>
를 사용한다.Spliterator
은 특정 특성을 기반으로 요소의 일부를 다른Spliterator
로 분리할 수 있다.- 위처럼 독특한 성질로 인해 스트림 API의 핵심 요소가 되며, 스트림은 부분적인 시퀀스들을 병렬로 처리하면서 자바의 Collection API 타입을 수회할 수 있게 한다.
Spliterator의 특성
- 동시성
- 고유성
- 불변성
- 널아님
- 순서
- 정렬
- 크기
- 부분크기
6.3 스트림 파이프라인 구축하기
스트림 생성하기
- stream 메서드는 List나 Set과 같은 Collection 기반의 자료 구조에서 새로운 스트림 인스턴스를 생성하는 가장 간단한 방법
실습
- 스트림의 요소를 다루는 것은 중간 연산을 통해 이루어진다. 이 연산들은 크게 변환(
MAP
), 선택(FILTER
), 또는 일반적인 스트림 동작 수정을 수정하는 것.
요소 선택
Predicate
를 이용한 필터링 혹은 요소의 개수를 기반으로 선택함으로써 이루어진다.
Steam filter(Predicate<? super T> predicate)
- Predicate의 결과가 true인 경우 해당 요소는 후속 처리를 위해 선택.
Steam dropWhile(Predicate<? super T> predicate)
- Predicate가 true가 될때까지 통과하는 모든 요소를 폐기.
- 해당 기능은 ORDERED(순서가 있는) 스트림을 위해 설계되었다.
Steam takeWhile(Predicate<? super T> predicate)
- Predicate가 false가 될때까지 통과하는 모든 요소를 폐기.
Steam limit(long maxSize)
- 연산을 통과하는 요소의 최대 개수를 maxSize로 제한
Steam skip(long n)
- limit의 반대 개념으로, 앞에서부터 n개의 요소를 건너뛰고 나머지 요소들을 다음 스트림 연산으로 전달
Steam distinct()
- Object#equals (Object)를 통해 요소들을 비교하며 중복되지 않은 요소만 반환.
- 해당 연산은 요소를 비교하기 위해 모든 요소를 버퍼에 저장
Steam sorted()
java.util.Comparable
에 부합하는 경우 자연스럽게 정렬된다.
Steam sorted(Comparator<? super T> comparator)
- 사용자 정의 comparator를 사용하여 정렬하는 더 유연한 방식 제공
요소 매핑
Stream map(Function<? super T, ? extends R> mapper)
- mapper함수가 요소에 적용되고 새로운 요소가 스트림으로 반환
Stream flatMap(Function<? super T, ? extends Stream<? extendsR>> mapper)
- mapper함수가 요소에 적용되지만, 새로운 요소를 반환하는 대신 Stream을 반환해야 한다.
- flatMap 연산은 컬렉션 또는 Optional과 같은 컨테이너 형태의 요소를 '펼쳐서' 새로운 다중 요소를 포함하는 새로운 스트림으로 만든다.
Stream mapMulti(BiConsumer<? super T, ? super Consumer> mapper)
- mapMulti 연산은 매퍼가 스트림 인스턴스를 반환할 필요가 없고, Consumer가 요소를 슽트림을 통해 더 아래로 전달합니다.
- map과 flatMap의 두 연산을 하나로 압축할 수 있다.
스트림에서 Peek사용
- 스트림의 요소에 개입하지 않고 스트림을 살짝 들여다 본다.
- peek 연산은 주로 디버깅을 지원하기 위해 설계되었다.
스트림 종료하기
- 최종연산은 요소를 실제로 처리하여 결과나 사이드 이펙트를 생성하기 위한 슽릠 파이프라인의 마지막 단계
요소 축소
- 축소 연산은 fold 연산이라고도 하며, 누적 연산자를 반복적으로 적용하여 스트림의 요소들을 하나의 결괏값으로 만든다.
- 축소는 아래 세 가지로 구분
- 요소
- 데이터 처리의 핵심은 데이터 요소를 처리하는 것
- 초기값
- 모든 데이터의 축적은 특정 지점에시 시작. 이 시작점을 '초기값'이락 ㅗ한다.
- 누적 함수
- 축소 로직은 현재 요소와 이전의 결과 또는 초기값만으로 작동한다.
- 요소
T reduce(T identity, BinaryOperator accumulator)
- 이 메서드에서 identity는 accumulator 연산을 시작할 때의 초기값, 즉 '시드'값
Optional reduce(BinaryOperator accumulator)
- 이 연산은 초기값을 필요로 하지 않는다. 스트림의 첫 번째 요소가 초기값으로 사용된다.
- 스트림이 어떠한 요소도 포함하고 있지 않다면 반호나값은 비어있는 Optional가 된다.
U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator combiner)
- map과 reduce를 결합한 변형.
- 스트림에는 T타입의 요소가 포함되어 있지만 최종적으로 원하는 축소 결과가 U타입인 경우 이 메서드를 사용
- 혹은 map과 reduce를 명시적으로 사용할 수 있다.
컬렉터를 활용한 요소 집계
- Stream의 종료 연산자인 collect는 요소들을 집계할 때 Collector를 사용한다.
- 스트림 요소들을 단일 결과로 합치기 위해 누적 연산자를 반복적으로 적용하는 대신, 이 연산들은 가변 결과 컨테이너를 중간 자료 구조로 활용한다.
Supplier<a> supplier()
- 수집 과정 중 사용되는 가변 결과 컨테이너의 새 인스턴스 반환
Biconsumer<A, T> accumulator()
- 결과 컨테이너와 현재 요소를 인수로 받아, 타입 T의 스트림 요소들을 타입 A의 컨테이너에 추가.
BinaryOperator<A> combiner()
- 병렬로 스트림을 처리할 때 여러 누적기들이 동시에 작업을 수행할 수 있다.
Function<A, R> finisher()
- 중간 결과 컨테이너를 타입 R의 반환 객체로 변환하는 역할
- 최종 결과
- List, Map 또는 단일값을 포함하여 수집된 인스턴스를 의미.
java.util.Collection 타입으로 수집
- toCollection(Supplier collectionFactory)
- toList()
- toSet()
- toUnmodifiableList()
- toUnmodifiableSet()
java.util.Map 타입으로 수집
- toMap()
- toConcurrentMap()
- toUnmodifiableMap()
그룹화된 java.util.Map으로 수집
- groupingBy()
- groupingByConcurrent()
분할된 java.util.Map으로 수집
- partitionBy()
산술 및 비교 연산
- averagingInt(ToIntFunction<? super T> mapper)
- summingInt(ToIntFunction<? super T> mapper)
- summarizingInt(ToIntFunction<? super T> mapper)
- counting()
- minBy(Comparator<? super T> comparator)
- maxBy(Comparator<? super T> comparator)
문자열 연산
- joining()
고급 응용 사례
- reducing()
- collectingAndThen(Collector<T, A, R> downstream, Function<R, RR> finisher)
- mapping(Function<? super T, ? extends U> mapper, Collector<? super U, A, R> downstream)
- filtering(Predicate<? super T> predicate, Collector<? super T, A, R> downstream)
- teeing(Collector<? super T, A1, R1> downstream1, Collector<? super T, A2, R2> downstream2, BiFunction<R1, R2, R> merger)
요소를 집적 집계
List 반환
- 컬렉터 기반의 워크플로 없이 요소를 집계하므로 더 적은 메모리가 필요.
- collect(Collectors.toList())를 사용할 때와 동일하게 반환되는 목록의 구현 탕비이나 직렬화에 대한 보장이 없음
배열 반환
- Object[] toArray()
- A[] toArray(IntFunction<A[]> generator)
요소 찾기 및 매칭
- Optional findFirst()
- 첫번째로 만나는 스트림의 요소 반환
- Optioanl findAny()
- 임의의 요소 반환, 비어있다면 Optional를 반환
- boolean allMatch(Predicate<? super T> predicate)
- 모든 요소가 일치하면 true
- boolean anyMatch(Predicate<? super T> predicate)
- 일치하는 요소가 하나라도 존재하면 true
- boolean noneMatch(Predicate<? super T> predicate)
- 일치하는 요소가 스트림에 없으면 true
요소 소비
- 사이드 이펙트 전용 연산
- forEach(Consumer<? super T> action)
- 요소마다 주어진 동작을 수행, 병렬 스트림에서 최적의 성능을 위해 실행 순서는 명시적으로 비결정적
- void forEachOrdered(Consumer<? super T> action)
- 스트림이 Ordered 상태라면 만나게 되는 순서에 따라 요소에 대한 action 실행
연산 비용
6.4 스트림 사용 여부 선택
- 작업의 복잡도
- 작업의 내용을 쉽게 파악할 수 있다면 간단한 for-each 반복문을 사용하는 것이 더 나을 수 있다.
스트림 파이프라인이 얼마나 함수적인가?
- 함수형 접근 방식에 적합하지 않다면 스트림이 제공하는 모든 혜택과 안정성을 보장받지 못할 것.
처리 되는 요소의 수는?
- 스트림 파이프 라인 구성에 필요한 오버헤드는 처리하는 요소의 수에 비례하여 감소
반응형
'스터디 > 함수형 프로그래밍 with 자바' 카테고리의 다른 글
Chapter 08. 스트림을 활용한 병렬 데이터 처리 (0) | 2024.06.20 |
---|---|
Chapter 07. 스트림 사용하기 (0) | 2024.06.16 |
Chapter 05. 레코드 (Record) (0) | 2024.05.30 |
Chapter 04. 불변성 (0) | 2024.05.21 |
Chapter 03. JDK의 함수형 인터페이스 (1) | 2024.05.16 |