Java Stream flatMap으로 문자열 리스트의 각 문자 출력하기

Java에서 Stream을 처음 배울 때, map()과 flatMap()의 차이를 머리로는 알겠는데 코드에서는 자꾸 헷갈리는 경우가 많습니다. 특히 Java, Stream, flatMap으로, 문자열, 리스트의, 문자 출력하기 같은 문제는 작아 보이지만, 실제로는 “중첩된 데이터를 어떻게 풀어내는지”를 이해하는 데 꽤 도움이 됩니다. 저도 수업에서 이 예제를 다룰 때, 학생들이 문자열을 문자로 바꾸는 부분보다 Arrays.stream()을 왜 한 번 더 써야 하는지에서 가장 많이 멈추는 걸 자주 봤습니다.

먼저 막히는 지점: map()으로는 왜 원하는 모양이 안 나올까

문자열 리스트를 다루다 보면 처음에는 이런 생각을 하게 됩니다. “각 문자열을 문자로 바꾸면 되니까 map()이면 충분하지 않나?” 그런데 map()은 요소 하나를 다른 요소 하나로 바꾸는 데 가깝습니다. 반면 지금처럼 "Hello"를 H, e, l, l, o처럼 여러 값으로 풀어야 할 때는, 변환 결과가 다시 여러 개가 됩니다. 이때 flatMap()이 필요합니다.

실제로 실습해 보면, map()은 결과가 한 겹 더 남아 있는 느낌을 줍니다. 반면 flatMap()은 여러 조각을 하나의 흐름으로 합쳐 줍니다. 그래서 문자열 리스트를 문자 단위로 출력하는 문제에서는 flatMap()이 훨씬 자연스럽습니다.

실습 문제: 문자열 리스트의 모든 문자를 한 줄씩 출력하기

다음 문자열 리스트가 있다고 해 보겠습니다.

["Hello", "World", "Java"]

목표는 각 문자열을 문자 단위로 쪼갠 다음, 모든 문자를 순서대로 출력하는 것입니다. 이 문제는 단순 출력처럼 보이지만, 스트림 연산의 흐름을 확인하기에 좋습니다. 문자열을 배열로 바꾸고, 다시 스트림으로 바꾸고, 마지막에 평탄화하는 과정이 들어 있기 때문입니다.

import java.util.Arrays;
import java.util.List;

public class FlatMapExample {
    public static void main(String[] args) {
        List<String> words = Arrays.asList("Hello", "World", "Java");

        words.stream()
                .flatMap(word -> Arrays.stream(word.split("")))
                .forEach(System.out::println);
    }
}

실행 결과는 아래와 같습니다.

H
e
l
l
o
W
o
r
l
d
J
a
v
a

코드가 실제로 어떻게 흘러가는지 따라가 보기

먼저 words.stream()으로 List<String>을 스트림으로 바꿉니다. 이 단계까지는 비교적 익숙합니다. 리스트를 스트림으로 바꾸면 뒤에 오는 연산들을 연결할 수 있습니다.

그다음 부분이 핵심입니다.

.flatMap(word -> Arrays.stream(word.split("")))

여기서 word.split("")은 문자열을 한 글자씩 나눈 배열을 만듭니다. "Hello"라면 ["H", "e", "l", "l", "o"]가 됩니다. 그런데 배열은 바로 스트림 연산에 붙일 수 없으므로, Arrays.stream(...)으로 다시 스트림으로 바꿔 줍니다. 결과적으로 문자열 하나가 문자 스트림 하나로 바뀝니다.

문제는 문자열이 여러 개라는 점입니다. "Hello", "World", "Java" 각각이 별도의 문자 스트림이 되는데, flatMap()은 이 여러 스트림을 한 줄로 이어 붙입니다. 그래서 마지막 forEach()에서 문자를 하나씩 받을 수 있습니다.

수업에서 자주 본 실수는 split("")보다 flatMap()의 역할을 놓치는 경우였다

개인적으로 이 주제를 정리하면서 가장 눈에 띈 부분은, 초보자들이 문자열 분리 자체보다 “왜 스트림이 두 번 등장하는지”를 이해하기 어려워한다는 점이었습니다. 수업에서는 종종 split("")만 맞게 쓰고도, map()으로 끝내려다가 결과가 Stream<Stream<String>> 비슷한 모양으로 남아버리는 실수를 봅니다. 이때 중요한 건 “문자를 만든다”보다 “문자 스트림들을 하나로 합친다”는 관점입니다. 직접 코드를 실행해 보면, flatMap()이 있어야 출력 단계가 단순해진다는 사실을 바로 확인할 수 있습니다.

split("")과 Arrays.stream()을 같이 써야 하는 이유

여기서 실수하기 쉬운 부분을 따로 정리해 보면 이해가 빠릅니다.

  • split("")은 문자열을 배열로 바꿉니다.
  • Arrays.stream()은 배열을 스트림으로 바꿉니다.
  • flatMap()은 여러 스트림을 하나로 합칩니다.

이 세 단계가 각각 역할이 다릅니다. 가끔은 split("")만으로 충분할 것처럼 느껴지지만, 실제로는 배열이기 때문에 스트림 파이프라인에 바로 연결되지 않습니다. 반대로 Arrays.stream()만 사용해도 문자열을 문자 단위로 나누지 않으면 의미가 없습니다. 둘을 함께 써야 목적이 맞아떨어집니다.

조금 더 실무적으로 보면, 출력 전에 확인할 점이 있다

실무나 과제 채점 기준에서는 “문자가 출력되긴 하는데 공백이나 빈 문자열이 섞이는지”를 먼저 확인하는 편입니다. 문자열 처리에서 split("")은 간단해서 좋지만, 데이터가 항상 깔끔하다고 가정하면 안 됩니다. 예를 들어 빈 문자열이 섞여 있거나, 공백이 많은 입력이 들어오면 결과가 예상과 달라질 수 있습니다. 이런 경우에는 출력 전에 filter()를 붙여 조건을 정리하는 방식도 고려해야 합니다. 다만 이번 예제의 목적은 flatMap() 흐름을 익히는 것이므로, 불필요하게 복잡하게 만들 필요는 없습니다.

같은 문제를 조금 변형해 보면 이해가 더 빨라진다

문자 하나씩 출력하는 것에 익숙해졌다면, 아래처럼 변형해 보는 것이 좋습니다.

1. 중복 문자를 제거하고 출력하기

import java.util.Arrays;
import java.util.List;

public class DistinctFlatMapExample {
    public static void main(String[] args) {
        List<String> words = Arrays.asList("Hello", "World", "Java");

        words.stream()
                .flatMap(word -> Arrays.stream(word.split("")))
                .distinct()
                .forEach(System.out::println);
    }
}

이 경우 distinct()는 중복 문자를 한 번만 남깁니다. 예를 들어 l이나 o처럼 여러 번 등장하는 문자는 하나만 출력됩니다. 다만 문자열의 순서를 유지하면서 중복만 제거하기 때문에, 데이터 정리 목적에는 유용하지만 원래 문자열의 모든 글자를 확인해야 하는 상황에서는 적합하지 않을 수 있습니다.

2. 앞에서 두 개 문자만 보고 싶다면

import java.util.Arrays;
import java.util.List;

public class LimitFlatMapExample {
    public static void main(String[] args) {
        List<String> words = Arrays.asList("Hello", "World", "Java");

        words.stream()
                .flatMap(word -> Arrays.stream(word.split("")))
                .limit(5)
                .forEach(System.out::println);
    }
}

limit(5)를 붙이면 앞에서 다섯 글자만 출력합니다. 이런 식으로 flatMap() 뒤에 distinct(), limit() 같은 메서드를 묶어 보면, 단순히 “출력만 하는 코드”가 아니라 “흐름을 정리하는 코드”로 발전합니다.

직접 해볼 수 있는 작은 연습 문제

문자열 리스트를 아래처럼 바꿔 보세요.

["Spring", "Boot", "Stream"]

그다음 아래 두 가지를 직접 확인해 보면 좋습니다.

1. 모든 문자를 출력하기 2. 중복 문자를 제거한 뒤 출력하기

이 연습을 할 때는 결과만 보지 말고, 중간 단계가 어떤 타입으로 바뀌는지 같이 적어 보면 이해가 훨씬 빠릅니다. 예를 들어 List<String> → Stream<String> → Stream<String>(문자열을 쪼갠 결과) → 최종 출력 순서로 생각하면, flatMap()의 역할이 훨씬 선명해집니다.

정리해 두면 헷갈림이 줄어드는 기준

  • 문자열을 문자 단위로 나눌 때는 split("")을 사용한다.
  • 배열을 스트림 처리하려면 Arrays.stream()이 필요하다.
  • 여러 스트림을 하나로 합칠 때는 flatMap()이 맞다.
  • 중복 제거가 필요하면 distinct()를 붙일 수 있다.
  • 출력 개수를 제한하려면 limit()을 함께 고려할 수 있다.

이 기준만 잡아도, 비슷한 문제에서 map()과 flatMap()을 헷갈릴 가능성이 크게 줄어듭니다.

참고 자료

  • [Oracle Java Stream API](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/stream/Stream.html)
  • [Oracle Comparator API](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Comparator.html)

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다