CodeGym /행동 /JAVA 25 SELF /flatMap와 mapMulti 메서드

flatMap와 mapMulti 메서드

JAVA 25 SELF
레벨 32 , 레슨 0
사용 가능

1. 소개

학생들의 목록이 있고, 각 학생마다 취미 목록이 있다고 가정해 봅시다. 예를 들어:

List<List<String>> hobbies = List.of(
    List.of("수영", "체스"),
    List.of("축구"),
    List.of("프로그래밍", "독서", "영화")
);

여러분의 과제: 모든 취미를 하나의 목록으로 모아 학생들이 전반적으로 무엇에 관심이 있는지 알아내는 것입니다. 논리적으로는 map을 사용해 보려 할 겁니다:

List<Stream<String>> streams = hobbies.stream()
    .map(list -> list.stream())
    .collect(Collectors.toList());

무엇을 얻었나요? 스트림들의 목록, 즉 Stream<Stream<String>>입니다. 우리가 원하는 것은 각 취미를 바로 다룰 수 있는 단순한 Stream<String>이었죠.

여러분 앞에 상자가 있고, 그 안에 또다른 장난감 상자들이 들어 있다고 상상해 보세요. map은 각 상자를 꺼내 “상자” 자체를 보여줍니다(Stream<Stream<String>>). 하지만 우리는 상자를 뒤적이지 않고 장난감 전체(Stream<String>)를 바로 보고 싶습니다.

2. flatMap: 중첩 컬렉션을 “펼치기”

상자를 “풀어서” 평탄한 스트림을 얻으려면 flatMap이 필요합니다.
각 요소에 대해 스트림을 반환하는 함수를 받고, 그 모든 내부 스트림을 하나로 “평탄화”합니다.

flatMap 문법

Stream<T> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)

쉽게 말하면: flatMap은 각 요소에 대해 Stream을 반환해 주길 기대하고, 그렇게 반환된 것들을 하나의 큰 스트림으로 “폅니다”.

예시: 모든 학생의 취미를 합치기

List<String> allHobbies = hobbies.stream()
    .flatMap(list -> list.stream())
    .collect(Collectors.toList());

System.out.println(allHobbies);
// [수영, 체스, 축구, 프로그래밍, 독서, 영화]

각 취미 목록에 대해 list.stream()을 호출해(한 학생의 취미 스트림) 얻고, flatMap이 모든 스트림을 하나의 큰 스트림으로 “평탄화”했습니다 — 이제 우리는 “취미들의 스트림”이 아니라 모든 취미를 직접 볼 수 있습니다.

시각적 도식

hobbies.stream()
    |
    |---> [수영, 체스]           -> stream
    |---> [축구]                 -> stream
    |---> [프로그래밍, ...]      -> stream
    |
flatMap: 모든 것을 하나의 Stream<String>으로 결합

왜 map으로는 해결되지 않을까?

map을 사용하면 Stream<Stream<String>>이 되므로 불편합니다. 예를 들어 모든 문자열을 바로 순회할 수 없고, 추가적인 순회가 필요합니다.

3. flatMap의 실전 사용 예

문자열을 문자로 분해하기 (List<String> → Stream<Character>)

문자열 목록이 있고, 모든 문자열에 등장하는 문자 전체의 스트림을 얻고 싶다고 합시다:

List<String> words = List.of("Java", "Stream");

List<Character> characters = words.stream()
    .flatMap(word -> word.chars().mapToObj(ch -> (char) ch))
    .collect(Collectors.toList());

System.out.println(characters);
// [J, a, v, a, S, t, r, e, a, m]

설명:

  • word.chars()IntStream(문자 코드의 스트림)을 반환합니다.
  • mapToObj는 코드를 문자로 바꿉니다.
  • flatMap은 얻어진 문자 스트림들을 하나로 합칩니다.

Optional 다루기: Stream<Optional<T>> → Stream<T>

Optional 객체들의 목록이 있고, 실제로 값이 존재하는 것들만의 스트림을 얻고 싶다고 합시다:

List<Optional<String>> optionals = List.of(
    Optional.of("Java"),
    Optional.empty(),
    Optional.of("Stream")
);

List<String> present = optionals.stream()
    .flatMap(opt -> opt.stream())
    .collect(Collectors.toList());

System.out.println(present);
// [Java, Stream]

포인트:
Optional<T>은(Java 9부터) stream() 메서드를 제공하여 빈 스트림 또는 단일 요소 스트림을 반환합니다. flatMap은 모든 비어 있지 않은 값을 하나의 스트림으로 합칩니다.

4. 새로운 mapMulti: flatMap이 필요 없을 때

왜 mapMulti가 도입되었을까?

Java 16에 도입된 mapMultiflatMap과 유사하게 동작하지만 조금 더 효율적이고 유연합니다.

flatMap은 강력하지만, 각 요소에 대해 0개, 1개 또는 여러 개의 요소를 반환하고 싶더라도 반드시 새로운 Stream을 만들어야 한다는 단점이 있습니다. 단지 요소들을 “펼치기”만 하면 되는 경우에는 비효율적일 수 있죠.

mapMulti는 중간 구조를 만들지 않고 Consumer를 통해 결과 스트림에 필요한 값들을 직접 추가할 수 있게 해 주는, flatMap의 개선된 버전입니다. 스트림을 반환하는 대신, 결과 스트림에 어떤 요소들을 추가할지 Consumer로 직접 지정합니다.

mapMulti 문법

<R> Stream<R> mapMulti(BiConsumer<? super T, ? super Consumer<R>> mapper)

각 요소마다 함수 mapper가 호출되며, 이 함수는 요소 자체와 새로운 값을 “담을” Consumer를 전달받습니다.

예시: 한 번에 필터링과 펼치기

숫자 목록이 있고, 짝수는 스트림에 두 번 추가하고 홀수는 아예 추가하지 않으려 한다고 합시다(즉, 필터링과 “펼치기”를 동시에 수행):

List<Integer> numbers = List.of(1, 2, 3, 4);

List<Integer> result = numbers.stream()
    .mapMulti((number, consumer) -> {
        if (number % 2 == 0) {
            consumer.accept(number);
            consumer.accept(number); // 두 번 추가
        }
        // 홀수는 아무 것도 하지 않음(필터링)
    })
    .collect(Collectors.toList());

System.out.println(result);
// [2, 2, 4, 4]

flatMap과의 비교

같은 작업을 flatMap으로 작성하면 다음과 같습니다:

List<Integer> result = numbers.stream()
    .flatMap(number -> number % 2 == 0
        ? Stream.of(number, number)
        : Stream.empty())
    .collect(Collectors.toList());

여기서는 각 요소마다 Stream.of(...) 또는 Stream.empty()를 만들어야 하므로, 비효율적일 수 있습니다.

5. flatMap을 쓸 때와 mapMulti를 쓸 때

  • map — 각 요소를 하나의 요소로 변환할 때.
  • flatMap — 각 요소를 여러 요소의 스트림으로 변환할 때.
  • mapMulti — 중간 스트림을 만들지 않고 여러 요소를 생성하고 싶을 때(특히 뜨거운 루프에서 더 효율적).

또 다른 예: Map<Integer, List<String>>를 Stream<Pair(Integer, String)>로 펼치기

id → 취미 목록이라는 맵이 있다고 합시다.

Map<Integer, List<String>> studentHobbies = Map.of(
    1, List.of("수영", "체스"),
    2, List.of("축구"),
    3, List.of("프로그래밍", "독서", "영화")
);

List<String> allHobbies = studentHobbies.values().stream()
    .flatMap(Collection::stream)
    .collect(Collectors.toList());

System.out.println(allHobbies);
// [수영, 체스, 축구, 프로그래밍, 독서, 영화]

이제 (id, 취미) 쌍을 얻고 싶다면:

List<String> pairs = studentHobbies.entrySet().stream()
    .flatMap(entry -> entry.getValue().stream()
        .map(hobby -> entry.getKey() + ": " + hobby))
    .collect(Collectors.toList());

System.out.println(pairs);
// [1: 수영, 1: 체스, 2: 축구, 3: 프로그래밍, 3: 독서, 3: 영화]

mapMulti(Java 16+)로 작성하면:

List<String> pairs = studentHobbies.entrySet().stream()
    .mapMulti((entry, consumer) -> {
        for (String hobby : entry.getValue()) {
            consumer.accept(entry.getKey() + ": " + hobby);
        }
    })
    .collect(Collectors.toList());

장점:
mapMulti는 중간 스트림을 만들지 않고 결과 스트림으로 값을 직접 “배출”합니다.

시각화: flatMap vs mapMulti

메서드 함수가 무엇을 반환하는가 어떻게 결합하는가 중간 컬렉션?
map
단일 요소 단순히 아니오
flatMap
스트림(Stream) 결합 예(중간 Stream)
mapMulti
Consumer (0..n회) 결합 아니오(직접 추가)

6. flatMap과 mapMulti 사용 시 흔한 실수

오류 №1: Stream<T> 대신 Stream<Stream<T>>를 얻음. 컬렉션의 컬렉션을 다룰 때 mapflatMap 대신 쓰는 경우가 종종 있습니다. 그 결과 불필요한 반복문을 추가로 작성하게 됩니다.

오류 №2: 반환 타입이 올바르지 않음. flatMap의 함수는 Stream을 반환해야 하며, List나 배열을 반환하면 안 됩니다.

오류 №3: 비효율성. 간단한 경우에는 flatMap이 잘 동작하지만, 각 요소마다 Stream.ofStream.empty()를 만들어야 한다면 과할 수 있습니다. 이런 작업에는 mapMulti가 더 좋습니다.

오류 №4: mapMulti는 오래된 Java 버전에서 동작하지 않음. mapMulti는 Java 16에서 추가되었습니다. 더 오래된 JDK에서는 이 메서드를 사용할 수 없습니다.

오류 №5: null 문제. flatMap에서 null을 반환하지 마세요 — “빈” 경우에는 항상 Stream.empty()를 반환해야 합니다.

오류 №6: 중간 컬렉션 남발. 추가적인 리스트나 스트림을 만들지 말고, 가능하다면 mapMulti에서 consumer로 요소를 직접 추가하세요.

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION