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 비교
| 구분 | map | flatMap |
|---|---|---|
| 역할 | 각 요소 변환 | 각 요소 변환 후 펼치기 |
| 결과 구조 | 구조 유지 | 중첩 구조 제거 |
| 대표 형태 | T → R | T → 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>로 평탄화