/////
Search
Duplicate
5️⃣

리듀싱

지금까지 살펴본 연산들의 최종값은 boolean(allMatch 등), void(forEach) 또는 Optional 객체(findAny 등)을 반환했다. 또한 collect로 스트림의 요소를 리스트로 모으는 방법도 살펴봤다.
이 절에서는 리듀스 연산을 이용해서 ‘메뉴의 모든 칼로리의 합계를 구하시오’, ‘메뉴에서 칼로리가 가장 높은 요리는?’ 같이 스트림 요소를 조합해서 더 복잡한 질의를 표현하는 방법을 설명한다.
이러한 질의를 수행하려면 Integer 같은 결과가 나올 때까지 스트림의 모든 요소를 반복적으로 처리해야 한다.
이런 질의를 리듀싱 연산이라고 한다. 함수형 프로그래밍 언어 용어로는 이 과정이 마치 종이를 작은 조각이 될 때까지 반복해서 접는 것과 비슷하다는 의미로 폴드라고 부른다.

1. 요소의 합

reduce 메서드를 살펴보기 전에 for-each 루프를 이용해서 리스트의 숫자 요소를 더하는 코드를 확인하자.
int sum = 0; for (int x : numbers) { sum += x; }
Java
복사
numbers의 각 요소는 sum 변수에 반복적으로 더해진다. 리스트에서 하나의 숫자가 남을 때까지 reduce 과정을 반복한다. 코드에서는 파라미터를 두 개 사용했다.
sum 변수의 초깃값 0
리스트의 모든 요소를 조합하는 연산(+)
위 코드를 복붙하지 않고 모든 숫자를 곱하는 연산을 구현할 수 있다면? 재사용성이 올라갈 것이다.
이런 상황에서 reduce를 이용하면 애플리케이션의 반복된 패턴을 추상화할 수 있다.
reduce를 이용한다면 다음처럼 스트림의 모든 요소를 더할 수 있다.
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
Java
복사
reduce는 두 개의 인수를 갖는다.
초깃값 0, 두 요소를 조합해서 새로운 값을 만드는 BinaryOperator<T>
예제에서는 람다 표현식 (a,b) → a + b를 사용했다.
reduce로 다른 람다, 즉 (a, b) → a * b를 넘겨주면 모든 요소에 곱셈을 적용할 수 있다.
int sum = numbers.stream().reduce(0, (a, b) -> a * b);
Java
복사
메서드 참조를 이용해서 이 코드를 좀 더 간결하게 만들 수 있다. 자바 8에서 Integer 클래스는 두 숫자를 더하는 정적 sum 메서드를 제공하기 때문에 람다 코드를 구현할 필요가 없다.
int sum = numbers.stream().reduce(0, Integer::sum)
Java
복사
초깃값이 없을 경우
초기값을 받지 않도록 작성된 reduce도 있다. 이 reduceOptional 객체를 반환한다.
Optional<Intger> sum = numbers.stream().reduce((a, b) -> (a + b));
Java
복사
Optional<Integer>를 반환하는 걸까? 스트림에 아무 요소도 없는 상황을 생각해보자. 이런 상황이라면 초깃값이 존재하지 않을 경우 reduce는 합계를 반환할 수 없다.
따라서 합계가 없을 수도 있음을 가리키도록 Optional 객체로 감싼 결과를 반환한다. 이제 reduce로 어떤 작업을 할 수 있는지 살펴보자.

2. 최댓값과 최솟값

최댓값과 최솟값을 찾을 때도 reduce를 활용할 수 있다. reduce를 이용해서 스트림에서 최댓값과 최솟값을 찾는 방법을 살펴보자. reduce는 두 인수를 받는다.
초깃값
스트림의 두 요소를 합쳐서 하나의 값으로 만드는 데 사용할 람다
스트림의 각 요소에 덧셈 계산을 수행하는 람다가 적용되는 모습을 보여준다. 따라서 두 요소에서 최댓값을 반환하는 람다만 있으면 최댓값으 구할 수 있다.
reduce 연산은 새로운 값을 이용해서 스트림의 모든 요소를 소비할 때까지 람다를 반복 수행하면서 최댓값을 생산한다.
다음처럼 reduce를 이용해서 스트림의 최댓값을 찾을 수 있다.
Optional<Integer> max = numbers.stream().reduce(Integer::max); // 최댓값 Optional<Integer> max = numbers.stream().reduce(Integer::min); // 최솟값
Java
복사
map-reduce 패턴
mapreduce를 연결하는 기법을 맵 리듀스 패턴이라고 한다.
쉽게 병렬화하는 특징 덕분에 구글이 웹 검색에 적용하면서 유명해졌다.
여기서는 스트림 요소의 값을 1로 매핑하고 모두 더하는 방식으로 개수를 구했다. 4장에서는 count로 스트림 요소 수를 세는 방법을 살펴봤었다.
reduce 메서드의 장점과 병렬화
기존의 단계적 반복으로 합계를 구하는 것과 reduce를 이용해서 합계를 구하는 것은 어떤 차이가 있을까?
reduce를 이용하면 내부 반복이 추상화되면서 내부 구현에서 병렬로 reduce를 실행할 수 있게 된다.
반복적인 합계에서는 sum 변수를 임계 영역에 배치해야 하므로 쉽게 병렬화하기 어렵다.
강제적으로 동기화시키더라도 결국 병렬화로 얻어야 할 이득이 스레드 간의 소모적인 경쟁 때문에 상쇄되어 버린다는 사실을 알게 될 것이다.
사실 이 작업을 병렬화하려면 입력을 분할하고, 분할된 입력을 더한 다음에, 더한 값을 합쳐야 한다.
스트림 연산 : 상태 없음상태 있음
스트림 연산은 마치 은탄환 같다. 스트림을 이용해서 원하는 연산을 쉽게 구현할 수 있으며 streamparallelStream으로 바꾸는 것만으로도 병렬성을 얻을 수 있다.
map, filter등은 입력 스트림에서 각 요소를 받아 0 또는 결과를 출력 스트림으로 보낸다.
따라서 이들은 상태가 없는 연산이다.
reduce, sum, max 같은 연산은 결과를 누적할 내부 상태가 필요하다.
예제의 내부 상태는 작은 값으로 우리 예제에서는 int 또는 double을 내부 상태로 사용했다.
스트림에서 처리하는 요소 수와 관계없이 내부 상태의 크기는 한정되어 있다.
sorteddistinct같은 연산은 filtermap처럼 스트림을 입력으로 받아 다른 스트림을 출력하는 것처럼 보일 수 있는데 명백히 다르다.
스트림의 요소를 정렬하거나 중복을 제거하려면 과거의 이력을 알고 있어야한다. 저장해두어야한다!
예를 들어 어떤요소를 출력 스트림으로 추가하려면 모든 요소가 버퍼에 추가되어 있어야 한다. 연산을 수행하는 데 필요한 저장소 크기는 정해져있지 않다.
따라서 데이터 스트림의 크기가 크거나 무한이라면 문제가 생길 수 있다.
이러한 연산을 내부 상태를 갖는 연산이라고 한다.