
스트림을 사용하다 보면 어느 순간부터
“음… 이건 그냥 쓰긴 쓰는데, 정확히는 모르겠네?”
싶은 부분들이 생기기 시작합니다.
책을 읽다가 햇갈렸던 부분이 생겼습니다.
- 무한 스트림은 어떻게 만들지? iterate와 generate는 뭐가 다르지?
- flatMap은 왜 꼭 필요한 걸까?
- 컬렉션이 null일 때 스트림은 어떻게 안전하게 만들지?
- groupingBy랑 partitioningBy는 언제 무엇을 써야 할까?
- reduce와 collect는 둘 다 ‘결과를 만든다’는데, 정확한 차이가 뭘까?
"헷갈렸던 부분들을 직접 코드로 실험해보고 정리한 내용"을 담았습니다.
CHAPTER 05 스트림 활용
🤔 무한 스트림 생성하는 방법
Q. 무한 스트림을 생성하는 방법에는 iterate 와 generate가 있는데요.
이 둘의 차이점은 무엇일까요?
스트림에서는 iterate()와 generate()를 사용해 무한 스트림을 만들 수 있습니다.
둘의 차이는 다음과 같습니다.
- Stream.iterate(초기값, 조건, 연산자)
- 초기값과 연산자를 기반으로 다음 값을 계산
- 내부 상태를 불변 객체(immutable) 로 취급하며 안전함
- Stream.generate(Supplier)
- Supplier가 제공하는 값으로 스트림 생성
- 내부 상태를 직접 변경할 수 있어 부작용(side effect) 발생 가능
책의 예제처럼 피보나치 수열을 만드는 과정을 각각의 메서드를 활용해봅시다.
Supplier<Integer> fib = new Supplier<>() {
private int previous = 0;
private int current = 1;
@Override
public Integer get() {
int oldPrevious = this.previous;
int nextValue = this.previous + this.current;
this.previous = this.current;
this.current = nextValue;
return oldPrevious;
}
};
@Test
void test() {
List<Integer> iterateList = Stream.iterate(new int[]{0, 1},
t -> t[0] <= 514229,
t -> new int[]{t[1], t[0] + t[1]})
.map(t -> t[0])
.sorted()
.collect(Collectors.toList());
List<Integer> generateList = Stream.generate(fib)
.takeWhile(t -> t <= 514229)
.sorted()
.collect(Collectors.toList());
assertThat(iterateList.equals(generateList)).isTrue();
}
// iterate()
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, 17711, 28657, 46368, 75025, 121393, 196418, 317811, 514229]
// generate()
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, 17711, 28657, 46368, 75025, 121393, 196418, 317811, 514229]
두 방식 모두 동일한 피보나치 수열을 잘 만들어냅니다.
Q. 병렬 스트림으로 처리한다면 어떤 차이가 날까요?
@Test
void parallelTest() {
List<Integer> iterateList = Stream.iterate(new int[]{0, 1},
t -> t[0] <= 514229,
t -> new int[]{t[1], t[0] + t[1]})
.parallel()
.map(t -> t[0])
.sorted()
.collect(Collectors.toList());
List<Integer> generateList = Stream.generate(fib)
.parallel()
.takeWhile(t -> t <= 514229)
.sorted()
.collect(Collectors.toList());
assertThat(iterateList.equals(generateList)).isTrue();
}
// 정상적인 피보나치 수열 출력
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, 17711, 28657, 46368, 75025, 121393, 196418, 317811, 514229]
// iterate()
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, 17711, 28657, 46368, 75025, 121393, 196418, 317811, 514229]
// generate() 실행할 때마다 다르게 나온다.
// [0, 1, 1, 1, 2, 3, 5, 8, 8, 13, 21, 21, 34, 55, 89, 144, 233, 233, 377, 610, 987, 1597, 2584, 2584, 4181, 6765, 10946, 17711, 28657, 28657, 46368, 75025, 75025, 121393, 196418, 196418, 317811, 514229, 514229]
결과 차이
- iterate()
- 불변 객체를 기반으로 값을 생성 → 병렬에서도 안전
- generate()
- 외부 상태(previous, current)를 변경
- 병렬 실행 시 값이 뒤섞여 부작용 발생
- 실행할 때마다 결과가 달라짐
스트림 병렬 처리에서는 반드시 상태를 변경하지 않는 람다, 즉 순수 함수(pure function)를 사용해야 합니다.
🤔 flatMap 이란?
Q. flatMap은 어떤 상황에서 사용하는 것일까요?
어떻게 동작하는 것일까요?
flatMap()은 중첩된 스트림을 하나의 스트림으로 평평하게 만드는 작업(flattening)을 합니다.
예시: 문자열을 문자 스트림으로 분리
Stream<String> words = Stream.of("Hello", "World");
List<String> result = words
.map(word -> word.split(""))
.flatMap(Arrays::stream)
.distinct()
.collect(Collectors.toList());

- Hello가 split("")에 의해 ["H", "e", "l", "l", "o"] 로 분리되고, World가 split("")에 의해 ["W", "o", "r", "l", "d"]로 분리된다.
- Arrays.stream(T[] array)를 사용해 ["H", "e", "l", "l", "o"] 와 ["W", "o", "r", "l", "d"]를 각각 Stream으로 만든다.
- flatMap()을 사용해 여러 개의 Stream을 1개의 Stream으로 평평하게 합치고, Stream의 소스는 ["H", "e", "l", "l", "o", W", "o", "r", "l", "d"] 가 된다.
- distinct()에 의해 중복된 소스(l, o)가 제거된다.
- 중복이 제거된 ["H", "e", "l", "o", "W", "r", "d"]가 collect(toList())에 의해 수집된다.
예시 : 2차원 배열
int[][] sample = new int[][]{
{1, 2},
{3, 4},
{5, 6},
{7, 8},
{9, 10}
};
IntStream intStream = Arrays.stream(sample)
.flatMapToInt(array -> Arrays.stream(array));
intStream.forEach(System.out::println);
결과
1
2
3
4
5
6
7
8
9
10
🤔 null에 안전한 스트림 생성 방법
null에 안전한 스트림은 어떻게 생성할까요?
컬렉션이 null일 때 바로 stream()을 호출하면 NPE가 발생합니다.
안전한 방식 1
public Stream collectionAsStream(Collection collection) {
return collection != null ? collection.stream() : Stream.empty();
}
안전한 방식 2 (Optional 활용)
public Stream collectionAsStream(Collection collection) {
return Optional.ofNullable(collection)
.map(Collection::stream)
.orElseGet(Stream::empty);
}
🤔 스트림 내에서 특정 null 값을 빼고 싶다면?
List<String> filtered = Optional.ofNullable(list)
.orElseGet(Collections::emptyList)
.stream()
.filter(Objects::nonNull)
.collect(Collectors.toList());
CHAPTER 6 스트림으로 데이터 수집
🤔 groupingBy vs partitioningBy 차이
groupingBy — Function 기준으로 그룹화
Map<Dish.Type, List<Dish>> caloricDishesByType =
menu.stream()
.collect(groupingBy(Dish::getType));
스트림의 각 요리에서 Dish.Type과 일치하는 모든 요리를 추출하는 함수를
groupbingBy 메서드로 전달했습니다.
이 함수를 기준으로 스트림이 그룹화되므로 이를 분류함수라고 부릅니다.
다수준 그룹화도 가능합니다.
groupingBy(Dish::getType,
groupingBy(dish -> {
if (dish.getCalories() <= 400) return DIET;
else if (dish.getCalories() <= 700) return NORMAL;
else return FAT;
})
)
partitioningBy — Predicate 기준으로 참/거짓 분류
Map<Boolean, List<Dish>> partitioned =
menu.stream()
.collect(partitioningBy(Dish::isVegetarian));
- key가 무조건 true / false
- 오직 두 그룹으로만 분할됨
- Predicate만 전달할 수 있음
“두 그룹”으로 나누는 상황에는 partitioningBy가 groupingBy보다 성능이 더 좋습니다.
🤔 reduce vs collect 차이
둘 다 최종 연산이지만 목적과 동작 방식이 다릅니다.
reduce
- 주로 값을 하나로 축소할 때 사용
- 초기값 + BinaryOperator 필요
Optional<Integer> product = numbers.stream()
.reduce((a, b) -> a * b);
collect
- 스트림을 컬렉션, 맵, DTO 등으로 수집
- 스트림의 요소를 수집하여 다양한 컨테이너에 넣거나 결과를 변환하는 데 사용됨
- Collector를 사용하며, 병렬 스트림에서 reduce보다 더 안전하고 효율적
List<Integer> squared = numbers.stream()
.map(x -> x * x)
.collect(Collectors.toList());
collect는 병렬 스트림에서 효과적으로 동작하는 반면
reduce는 병렬 처리에 대한 효율성이 떨어질 수 있습니다.
일반적으로 데이터를 수집하거나 변환할 때는 collect를 사용하고,
최종 결과를 계산할 때는 reduce를 사용하는 것이 일반적인 패턴입니다.
마무리
이번 글에서는 무한 스트림, flatMap, null-safe 처리,
groupingBy·partitioningBy, reduce·collect 등
실무에서 자주 등장하지만 헷갈리기 쉬운 요소들을 다시 보았습니다.
스트림은 알아갈수록 더 강력하고, 동시에 더 조심스럽게 다뤄야 하는 도구라 생각합니다.
앞으로도 책을 읽으며 떠오른 질문들을 하나씩 풀어내고,
실험한 내용을 차곡차곡 쌓아갈 예정이니 다음 글도 기대해주세요.
읽어주셔서 감사합니다. ☺️
'Java-Spring' 카테고리의 다른 글
| "모던 자바 인 액션" 궁금했던 것들을 질문으로 정리해봤습니다 (1탄) (0) | 2025.01.24 |
|---|---|
| Java 8에 추가된 Optional에 대해 알아보기 (0) | 2024.11.10 |