← 전체 글

Stream API의 map과 flatMap

map? flatMap?

Java Stream API에서는 데이터를 변환할 때 주로 map()과 flatMap()을 사용한다.

두 메서드 모두 데이터를 다른 형태로 변환한다는 점은 비슷하지만, 결과의 구조에서 차이가 있다.

간단하게 먼저 정리하면

map
→ 하나의 값을 하나의 값으로 변환

flatMap
→ 하나의 값을 여러 값으로 변환한 뒤
   하나의 Stream으로 펼침

이라고 볼 수 있다.

아래에서 더욱 자세하게 알아보자.


1. map이란

map()은 Stream의 각 요소를 다른 값으로 변환하는 메서드이다.

예를 들어 이름 목록이 있다고 해보자.

List<String> names = List.of(
        "kim",
        "lee",
        "park"
);

각 이름을 대문자로 바꾸고 싶다면 map()을 사용할 수 있다.

List<String> result = names.stream()
        .map(String::toUpperCase)
        .toList();

결과는 다음과 같다.

kim   → KIM
lee   → LEE
park  → PARK
[KIM, LEE, PARK]

즉 각각의 요소가 하나의 다른 요소로 변환된다.

Stream<String>

kim
lee
park

 ↓ map

KIM
LEE
PARK

Stream<String>

2. map의 활용 사례

객체에서 특정 값만 추출할 때도 자주 사용한다.

다음과 같은 User 객체가 있다고 해보자.

public class User {

    private String name;
    private int age;

    public String getName() {
        return name;
    }
}

사용자 목록에서 이름만 가져오고 싶다면

List<String> names = users.stream()
        .map(User::getName)
        .toList();

처럼 작성할 수 있다.

변환 과정은 다음과 같다.

User 객체
 ↓
getName()
 ↓
String
User("kim")
User("lee")
User("park")

 ↓ map(User::getName)

"kim"
"lee"
"park"

따라서 map()은 다음과 같이 각 요소를 다른 값으로 변환할 때 적합하다.

User → String

String → Integer

Order → OrderDto

3. flatMap이 필요한 이유

이번에는 조금 다른 상황을 생각해보자.

학생마다 여러 과목을 수강하고 있다고 가정한다.

List<List<String>> subjects = List.of(
        List.of("Java", "Spring"),
        List.of("DB", "Redis"),
        List.of("Docker", "AWS")
);

현재 구조는 다음과 같다.

List

├─ [Java, Spring]
├─ [DB, Redis]
└─ [Docker, AWS]

즉 List 안에 또 다른 List가 들어가 있다.

이 데이터를 하나의 과목 목록으로 만들고 싶다.

원하는 결과는 다음과 같다.

Java
Spring
DB
Redis
Docker
AWS

이럴 때 flatMap()을 사용할 수 있다.


4. map을 사용하면 어떻게 될까

먼저 map()을 사용해보자.

Stream<Stream<String>> result = subjects.stream()
        .map(List::stream);

각 List<String>을 Stream<String>으로 변환한다.

[Java, Spring]
      ↓
Stream(Java, Spring)

[DB, Redis]
      ↓
Stream(DB, Redis)

[Docker, AWS]
      ↓
Stream(Docker, AWS)

하지만 전체 결과는

Stream
 ├─ Stream(Java, Spring)
 ├─ Stream(DB, Redis)
 └─ Stream(Docker, AWS)

형태가 된다.

즉

Stream<Stream<String>>

처럼 Stream 안에 또 Stream이 들어간 구조가 된다.

우리가 원하는 것은 이런 형태가 아니다.

Stream<String>

하나의 Stream으로 만들고 싶다.


5. flatMap이란

flatMap()은 각 요소를 Stream으로 변환한 뒤 여러 Stream을 하나의 Stream으로 펼쳐준다.

List<String> result = subjects.stream()
        .flatMap(List::stream)
        .toList();

동작 과정을 보면 다음과 같다.

[Java, Spring]
[DB, Redis]
[Docker, AWS]

 ↓ List::stream

Stream(Java, Spring)
Stream(DB, Redis)
Stream(Docker, AWS)

 ↓ flatMap

Java
Spring
DB
Redis
Docker
AWS

최종 결과는

[Java, Spring, DB, Redis, Docker, AWS]

가 된다.

즉 중첩된 구조를 한 단계 평평하게 만든다.

그래서 이름도

flat
→ 평평하게 만든다

map
→ 변환한다

라는 의미로 이해하면 쉽다.


6. map과 flatMap의 가장 큰 차이

두 메서드의 차이는 결과 구조를 보면 쉽게 이해할 수 있다.

map()을 사용하면

A
 ↓
B

처럼 하나의 요소가 하나의 요소로 변환된다.

예를 들어

User
 ↓
String

이다.

반면 flatMap()은

A
 ↓
여러 개의 B
 ↓
하나의 Stream으로 합침

과 같은 구조이다.

예를 들어

Student
 ↓
여러 Subject
 ↓
모든 Subject를 하나의 Stream으로 합침

이다.


7. 객체를 활용한 flatMap 예시

실제 객체를 사용해서 생각해보자.

학생 한 명이 여러 과목을 가지고 있다고 가정한다.

public class Student {

    private String name;
    private List<String> subjects;

    public List<String> getSubjects() {
        return subjects;
    }
}

학생 목록이 있다.

Kim
→ Java, Spring

Lee
→ DB, Redis

Park
→ Docker, AWS

모든 학생이 수강하는 과목을 하나의 목록으로 만들고 싶다면 다음과 같이 사용할 수 있다.

List<String> subjects = students.stream()
        .flatMap(student -> student.getSubjects().stream())
        .toList();

동작 과정은 다음과 같다.

Kim
 ↓
[Java, Spring]

Lee
 ↓
[DB, Redis]

Park
 ↓
[Docker, AWS]

        ↓ flatMap

Java
Spring
DB
Redis
Docker
AWS

결과는 다음과 같다.

[Java, Spring, DB, Redis, Docker, AWS]

8. 같은 상황에서 map을 사용하면

같은 코드에 map()을 사용하면 어떻게 될까.

List<List<String>> subjects = students.stream()
        .map(Student::getSubjects)
        .toList();

결과는 다음과 같다.

[
    [Java, Spring],
    [DB, Redis],
    [Docker, AWS]
]

즉 List<List<String>> 형태가 유지된다.

반면 flatMap()을 사용하면

List<String> subjects = students.stream()
        .flatMap(student -> student.getSubjects().stream())
        .toList();

결과가

[
    Java,
    Spring,
    DB,
    Redis,
    Docker,
    AWS
]

처럼 하나의 List로 합쳐진다.


9. map과 flatMap 비교

구분mapflatMap
역할각 요소 변환각 요소 변환 후 펼치기
결과 구조구조 유지중첩 구조 제거
대표 형태T → RT → Stream<R>
결과Stream<R>Stream<R>
주요 활용객체 변환, 값 추출중첩 컬렉션 처리

특히 다음 차이를 기억하면 된다.

map

List<List<String>>
 ↓
List<List<String>>
flatMap

List<List<String>>
 ↓
List<String>

10. 언제 map을 사용할까

각 데이터에서 하나의 결과만 얻는다면 map()을 사용하면 된다.

users.stream()
        .map(User::getName)
User
 ↓
name

또는 DTO로 변환하는 경우에도 많이 사용한다.

users.stream()
        .map(UserResponse::from)
        .toList();
User
 ↓
UserResponse

즉

하나의 데이터
→ 하나의 데이터

라면 map()을 먼저 생각하면 된다.


11. 언제 flatMap을 사용할까

하나의 객체 안에 여러 개의 데이터가 있고, 그것들을 하나로 합치고 싶다면 flatMap()을 사용할 수 있다.

예를 들어

주문
→ 여러 상품

학생
→ 여러 과목

게시글
→ 여러 댓글

사용자
→ 여러 권한

같은 구조이다.

주문마다 여러 상품을 가지고 있다고 하면

List<Product> products = orders.stream()
        .flatMap(order -> order.getProducts().stream())
        .toList();

처럼 모든 주문의 상품을 하나의 Stream으로 만들 수 있다.

Order 1
→ Product A
→ Product B

Order 2
→ Product C
→ Product D

       ↓ flatMap

Product A
Product B
Product C
Product D

12. 핵심 정리

map()과 flatMap()은 모두 Stream의 데이터를 변환할 때 사용한다.

하지만 처리하는 구조가 다르다.

map()은

하나의 요소
 ↓
하나의 요소

로 변환한다.

예를 들어

User
 ↓
UserDto

와 같은 경우에 사용한다.

반면 flatMap()은

하나의 요소
 ↓
여러 요소
 ↓
하나의 Stream으로 펼침

의 구조를 가진다.

예를 들어

Student
 ↓
List<Subject>
 ↓
Subject들을 하나의 Stream으로 합침

과 같은 경우에 사용한다.

한 문장으로 정리하면

map은 각 요소를 다른 값으로 변환하고, flatMap은 각 요소에서 만들어진 여러 값을 하나의 Stream으로 펼칠 때 사용한다.

특히 다음 형태를 기억하면 구분하기 쉽다.

map
T → R

flatMap
T → Stream<R>
   ↓
Stream<R>로 평탄화
단일 책임 원칙(SRP)와 개방-폐쇄 원칙(OCP)
© 2026 Evan. All Rights Reserved. / RSS / Sitemap
Powered by Astro