DeveloPiano

[Java] Stream에서 데이터 축약하기: reduce와 Collectors.reducing 본문

Develop/Java

[Java] Stream에서 데이터 축약하기: reduce와 Collectors.reducing

DevPi 2025. 1. 22. 10:26
반응형

Java 8 이후로 스트림을 사용하면 대량의 데이터를 선언적이고 함수형 스타일로 손쉽게 다룰 수 있습니다. 특히 여러 연산을 통해 데이터를 하나의 값으로 축약(리듀스) 하는 기능이 자주 필요한데, 이를 위해 reduce와 Collectors.reducing 메서드를 활용할 수 있습니다. 이 두 방법은 모두 “데이터 집합을 하나의 결과로 축약한다”는 공통된 목적을 수행하지만, 사용하는 시점과 시그니처가 다릅니다. 이번 포스팅에서는 각각을 자세히 살펴보고, 사용 예시와 활용 팁을 알아보겠습니다.


1. reduce 메서드 (Stream 인터페이스)

1.1 reduce란?

reduce 메서드는 스트림의 모든 요소를 하나의 값으로 축약하기 위한 종결 연산(Terminal Operation)입니다. Java 8 스트림에서는 다음과 같은 오버로드 버전을 제공합니다.

Optional<T> reduce(BinaryOperator<T> accumulator);
T reduce(T identity, BinaryOperator<T> accumulator);
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);

 

  • reduce(BinaryOperator<T> accumulator):
    스트림 요소를 순회하며 연산(accumulator)을 수행한 결과를 Optional<T>로 반환합니다. 스트림이 비어 있으면 Optional.empty()가 됩니다.
  • reduce(T identity, BinaryOperator<T> accumulator):
    identity라는 초깃값을 제공하기 때문에, 빈 스트림이라도 identity를 반환하므로 Optional<T>가 아닌 T 타입을 반환합니다.
  • reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner):
    스트림 요소 T를 결과 타입 U로 축약하는 경우에 사용합니다. 병렬 스트림에서도 안전하게 합산할 수 있도록 combiner가 추가되었습니다.

1.2 사용 예시

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

// 1) 기본 방식
Optional<Integer> sum = numbers.stream().reduce((a, b) -> a + b);
sum.ifPresent(System.out::println); // 출력: 15

// 2) 초깃값 제공
int product = numbers.stream()
    .reduce(1, (a, b) -> a * b);
System.out.println(product); // 120

// 3) U 타입으로 결과를 만들기
List<String> words = Arrays.asList("Hello", "Java", "Streams");
String combined = words.stream()
    .reduce("", (acc, s) -> acc + s + " ", (s1, s2) -> s1 + s2);
System.out.println(combined.trim()); // "Hello Java Streams"

 

reduce는 간단한 합산, 곱셈, 문자열 결합 등에서 자주 사용됩니다. 병렬 스트림에서 사용 시, 세 번째 시그니처로 combiner를 지정해 주는 것이 안전합니다.


2. Collectors.reducing 메서드

2.1 Collector로 축약하기

Java 8의 스트림에서는 collect를 통한 Collector 기반의 종결 연산도 자주 사용합니다. 이때 Collectors.reducing 메서드를 이용하면, 축약 로직을 Collector 형태로 정의할 수 있습니다.

public static <T> Collector<T, ?, Optional<T>> reducing(BinaryOperator<T> op)
public static <T> Collector<T, ?, T> reducing(T identity, BinaryOperator<T> op)
public static <T, U> Collector<T, ?, U> reducing(
    U identity,
    Function<? super T, ? extends U> mapper,
    BinaryOperator<U> op)

 

  • reducing(BinaryOperator<T> op):
    스트림 요소를 op로 축약해 Optional<T>를 반환하는 Collector를 생성합니다.
  • reducing(T identity, BinaryOperator<T> op):
    초깃값이 있는 축약 로직으로, 비어 있는 스트림이라도 identity를 반환합니다.
  • reducing(U identity, Function<? super T, ? extends U> mapper, BinaryOperator<U> op):
    먼저 mapper로 스트림 요소 T를 U로 변환한 뒤, op로 축약합니다.

2.2 사용 예시

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

// 1) 기본 reducing Collector
Optional<Integer> sum = numbers.stream()
    .collect(Collectors.reducing((a, b) -> a + b));
sum.ifPresent(System.out::println); // 15

// 2) 초깃값 제공
int product = numbers.stream()
    .collect(Collectors.reducing(1, (a, b) -> a * b));
System.out.println(product); // 120

// 3) 변환 후 축약
List<String> words = Arrays.asList("Hello", "Java", "Streams");
String combined = words.stream()
    .collect(Collectors.reducing(
        "",               // identity
        s -> s + " ",     // mapper
        (s1, s2) -> s1 + s2
    ));
System.out.println(combined.trim()); // "Hello Java Streams"

 

이처럼 Collectors.reducing도 reduce와 유사한 로직을 수행하지만, Collector 형태로 정의되므로, 다른 Collector와 조합(예: groupingBy, partitioningBy)하기 쉽습니다.


3. 언제 reduce vs reducing?

3.1 reduce (Stream 인터페이스)

  • 직접적으로 스트림 요소를 축약해서 단일 결과를 도출하고자 할 때
  • 예: stream.reduce(0, Integer::sum); 처럼 간단한 합계를 구하는 경우
  • 병렬 스트림 지원을 위해 세 번째 시그니처(직접 combiner)를 사용할 수도 있음

3.2 reducing (Collectors 메서드)

  • Collector 기반 종결 연산을 구성할 때
  • collect(Collectors.reducing(...)) 형태로 다른 Collector와 조합해서 복합적인 연산을 수행하기 좋음
Map<Boolean, Optional<Integer>> groupedSum = numbers.stream()
    .collect(Collectors.groupingBy(
        n -> n % 2 == 0,
        Collectors.reducing((a, b) -> a + b)
    ));

짝수와 홀수로 그룹핑한 뒤, 각각의 합을 구할 수 있음


4. 요약

  • reduce: 스트림의 종결 연산으로, 모든 요소를 축약해 하나의 결과를 만든다.
    • 오버로드된 세 가지 시그니처로 유연한 연산이 가능
    • 가장 직접적이고 단순한 축약 연산 방법
  • Collectors.reducing: 축약 로직을 Collector 형태로 정의할 때 사용.
    • .collect(...) 종결 연산에서 다른 Collector와 결합 가능
    • groupingBy, partitioningBy 등과 함께 멀티스텝 연산을 구성하기 좋음

두 방법 모두 데이터 축약을 담당하지만, 어떤 상황에서 사용하느냐가 다릅니다. 간단한 경우 reduce가 충분하지만, 복합적인 수집 로직(예: 그룹핑 후 축약)을 구성한다면 Collectors.reducing을 활용해보세요.

반응형