본문 바로가기

백엔드/Java

[Java] 스트림 Reduce 메서드의 내부 로직에 관하여

 

 

 

 

 

 

스트림은 sum(), count() 같은 기본 집계 메서드 이외에도, 다양한 결과물을 출력해 낼 수 있도록 reduce() 메서드도 제공하고 있다. reduce() 메서드의 리턴 타입과 매개변수는 아래와 같다.

 

// 초기 값이 없는 버전
Optional<T> reduce(BinaryOperator<T> accumulator)

// 초기값 identity가 있는 버전
T reduce(T identity, BinaryOperator<T> accumulator)

// 리턴 타입이 int, long, double인 reduce() 메서드도 존재
...

 

초기 값이 없는 reduce() 메서드는 스트림에 요소가 존재하지 않을 경우 'NoSuchElementException'이라는 예외가 발생하지만, 초기값 identity를 매개값으로 주면 스트림에 요소가 없더라도 이 초기값을 디폴트 값으로 리턴하므로 방금과 같은 예외가 발생하지 않는다.

 

 

 

 

 

한 예를 들어보자,

 

// 1,2,3,4의 합을 구해서 리턴하는 예시
Integer result = Stream.of(1, 2, 3, 4).reduce(0, (a,b) -> a+b);

 

위의 예시는 1, 2, 3, 4를 다 더해서 값을 result에 저장하는 스트림을 이용한 코드이다. 여기에서 'reduce(0, (a, b)->a+b)' 코드는 내부적으로 어떻게 동작할까? 언뜻 보기에는 reduce 메서드가 매개변수로 대입한 람다식을 이용해서 BinaryOperator 인터페이스에 정의되어 있는 메서드를 반복 호출하는 것처럼 보인다. 이제 본격적으로 reduce() 메서드의 내부 로직에 관하여 살펴보겠다.

 

 

 

 

 

자바의 Stream 인터페이스에 정의되어 있는 reduce 메서드는 아래와 같다.

 

// 초기값 identity를 매개값으로 받는 reduce 메서드
T reduce(T identity, BinaryOperator<T> accumulator);

/**
 * Performs a reduction on the elements of this stream, using the provided identity value and an 
 * associative accumulation function, and returns the reduced value. This is equivalent to:
 */
 
T result = identity;
for (T element : this stream)      
    result = accumulator.apply(result, element)  
return result;

/**
 * but is not constrained to execute sequentially.
 *
 * The identity value must be an identity for the accumulator function. This means that for all t,
 * accumulator.apply(identity, t) is equal to t. The accumulator function must be an associative 
 * function.
 *
 * This is a terminal operation.
 *
 * Params: identity – the identity value for the accumulating function 
 *         accumulator – an associative, non-interfering, stateless function for combining two 
 *         values
 *
 * Returns: the result of the reduction
 *
 * API Note: Sum, min, max, average, and string concatenation are all special cases of reduction.
 *           Summing a stream of numbers can be expressed as:
 *           
 *           Integer sum = integers.reduce(0, (a, b) -> a+b);
 *            
 *           or:
 *            
 *           Integer sum = integers.reduce(0, Integer::sum);
 *           
 *           While this may seem a more roundabout way to perform an aggregation compared to 
 *           simply mutating a running total in a loop, reduction operations parallelize more gracefully,
 *           without needing additional synchronization and with greatly reduced risk of data races.
 */

 

위의 코드를 살펴보면 역시나 reduce 메서드가 BinaryOperator 인터페이스의 apply 메서드를 for 문을 사용하여 반복 호출 하고 있다. 위의 코드에서 핵심적인 부분은 아래와 같다.

 

T reduce(T identity, BinaryOperator<T> accumulator);

T result = identity;
for (T element : this stream)      
    result = accumulator.apply(result, element)  
return result;

 

핵심적인 부분만 추출한 코드를 보면 내용이 직관적이라서 이해하기가 쉽다. 먼저 초기값 identity를 result 변수에 대입하면서 시작한다. 그다음으로, 매개변수로 대입한 람다식의 (a, b) 매개변수 각각에 (result, element)가 주입되고 있으며, 람다식의 구현 부분으로 계산한 결괏값을 for 반복문을 이용해서 다시 result 변수에 저장하고 있다. 결괏값으로 리턴 받은 result 값은 다시 매개값으로 대입된다. 마지막으로 result를 리턴하면서 끝이 난다.

 

 

 

 

 

원론적인 로직은 방금 말한 것과 같다. 하지만 조금 더 이해를 쉽게 하기 위해서, 조금 전에 예시로 들었던 1, 2, 3, 4의 합을 구하는 코드로 더 세부적인 로직을 설명해 보겠다. 

 

// 1,2,3,4의 합을 구해서 리턴하는 예시
Integer result = Stream.of(1, 2, 3, 4).reduce(0, (a,b) -> a+b);

 

1. 초기값 대입: 초기값 0이 result 변수에 대입

 

2. 첫 번째 반복: a = 0, b = 1 ('a'는 초기값, 'b'는 스트림의 첫 번째 요소)

     - result = 0 + 1 = 1

     - 결괏값 1이 대입된 result 변수가 다음 'a'의 값으로 사용됨

 

3. 두 번째 반복: a = 1, b = 2 ('a'는 이전 결과, 'b'는 스트림의 두 번째 요소)

     - result = 1 + 2 = 3

     - 결괏값 3이 대입된 result 변수가 다음 'a'의 값으로 사용됨

 

4. 세 번째 반복: a = 3, b = 3 ('a'는 이전 결과, 'b'는 스트림의 세 번째 요소)

     - result = 3 + 3 = 6

     - 결괏값 6이 대입된 result 변수가 다음 'a'의 값으로 사용됨

 

5. 네 번째 반복: a = 6, b = 4 ('a'는 이전 결과, 'b'는 스트림의 네 번째 요소)

     - result = 6 + 4 = 10

     - 결괏값 10이 최종 반환

 

 

 

 

 

이렇듯, 사용하려는 메서드의 내부 구조를 자세히 이해하면 메서드의 활용도 다양하게 할 수 있을 것이라고 생각한다.

'백엔드 > Java' 카테고리의 다른 글

[Java] Implicit Narrowing Conversion에 관하여  (0) 2023.02.04