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