Optional 에서 flatmap 과 map
자꾸 헷갈리는 map vs flatMap, 제대로 정리해보자
자바에서 map과 flatMap은 스트림이나 Optional을 다룰 때 자주 쓰인다.
하지만 둘의 차이점은 처음 접하면 상당히 헷갈린다.
이 글에서는 Stream과 Optional을 중심으로 map과 flatMap의 개념, 용도, 차이를 예제와 함께 정리해보자.
1. map과 flatMap, 가장 큰 차이점은?
간단하게 말하자면 이렇다:
map | flatmap | |
결과 타입 | 값을 다른 값으로 변환 | 값을 스트림 또는 옵셔널로 감싸서 반환한 뒤, 그 껍데기를 벗겨서 평면화 |
중첩 제거 여부 | 중첩된 구조 유지 | 중첩된 구조 제거 |
1. map 만 사용
List<String> animals = List.of("cat", "dog");
List<String[]> result = animals.stream()
.map(animal -> animal.split(""))
.collect(Collectors.toList());
결과
[ ["c", "a", "t"], ["d", "o", "g"] ] // List<String[]>
map은 스트림의 각 요소를 배열로 바꿨지만, 배열 자체가 스트림에 들어가므로 중첩된 구조가 된다.
2. flatmap 사용
List<String> result = animals.stream()
.map(animal -> animal.split(""))
.flatMap(Arrays::stream)
.collect(Collectors.toList());
결과
["c", "a", "t", "d", "o", "g"] // List<String>
flatMap은 각 String[]을 스트림으로 만든 다음, 전체 스트림을 평면화해준다.
즉, 여러 개의 작은 스트림을 하나의 스트림으로 합친다.
3. Optional에서의 map vs flatMap
Optional에도 map과 flatMap이 있다.
차이는 Stream과 비슷하지만, 여긴 단일 값이기 때문에 더 단순한 구조다.
Optional<String> name = Optional.of("animal");
Optional<Integer> length1 = name.map(String::length); // Optional<Integer>
Optional<Optional<Integer>> length2 = name.map(s -> Optional.of(s.length())); // Optional<Optional<Integer>>
Optional<Integer> length3 = name.flatMap(s -> Optional.of(s.length())); // Optional<Integer>
- map은 값을 감싼다 → 중첩 구조 생김
- flatMap은 껍데기를 벗기고 펼친다 → 중첩 제거
4. 실전 예제: 자동차 보험 찾기
public Optional<Insurance> nullsafeFindCheapeastInsurance(Optional<Person> person,Optional<Car> car){
return person.flatMap(p -> car.map(c->findCheapestIns(p,c)));
}
private Insurance findCheapestIns(Person p, Car c) {
return null;
}
이 코드는 Optional과 Optional가 모두 존재할 때만 보험을 찾는다.
중요한 건 findCheapestIns의 반환값이 Optional<Insurance>가 아니라 단순 객체라는 점이다:
그렇기 때문에 map 안에서 flatMap을 쓰면 컴파일 오류가 발생한다:
private Insurance findCheapestIns(Person p, Car c) {
return null;
}
이유는 간단하다. findCheapestIns(p, c)가 Optional을 반환하지 않는데, flatMap은 반드시 Optional을 반환하는 함수를 요구하기 때문이다.
flatmap 와 map 의 메서드 시그니처
// map
<U> Optional<U> map(Function<? super T, ? extends U> mapper)
// flatMap
<U> Optional<U> flatMap(Function<? super T, ? extends Optional<? extends U>> mapper)
- map: U를 반환하는 함수
- flatMap: Optional<U>를 반환하는 함수
5. 중첩 Optional 예제
Optional<Optional<Insurance>> insurance = person.map(p ->
car.map(c -> findCheapestIns(p, c)));
// Optional<Optional<Insurance>> 형태가 된다.
이럴 땐 flatMap을 써서 중첩을 없애주는 게 깔끔하다:
return person.flatMap(p ->
car.map(c -> findCheapestIns(p, c)));
6. 함수가 Optional을 반환할 때 flatMap 사용
아래 함수는 Optional<Insurance>를 반환한다.
private Optional<Insurance> findCheapestIns(Person p) {
return Optional.of(new Insurance());
}
이럴 땐 map을 쓰면 Optional<Optional<Insurance>>가 나오고,
flatMap을 써야 평면화가 된다.
Optional<Insurance> result = person.flatMap(p -> findCheapestIns(p));
public Optional<Insurance> nullsafeFindCheapeastInsurance(Optional<Person> person,Optional<Car> car){
//테스트
Optional<Insurance> insurance1 = car.map(c -> findCheapestIns(c));
Optional<Insurance> insurance2 = person.flatMap(p -> findCheapestIns(p));
Optional<Optional<Insurance>> insurance = person.map(p -> car.map(c -> findCheapestIns(p, c)));
//테스트 끝
return person.flatMap(p -> car.map(c->findCheapestIns(p,c)));
}
private Insurance findCheapestIns(Person p, Car c) {
return null;
}
private Insurance findCheapestIns( Car c) {
return null;
}
private Optional<Insurance> findCheapestIns(Person c) {
return null;
}
마무리 정리
map | flatMap | |
단순 변환 | o | x |
결과가 Optional 또는 Stream일 때 | 중첩 발생 | 평면화 (unwrap) |
반환 함수 시그니처 | T → U | T → Optional<U> / Stream<U> |
- Stream의 flatMap: Stream<Stream<T>> → Stream<T>
- Optional의 flatMap: Optional<Optional<T>> → Optional<T>
처음엔 헷갈리지만, 핵심은 간단하다:
map은 그냥 감싸고, flatMap은 껍데기까지 벗긴다.
예제와 함께 반복해서 연습하면 자연스럽게 감이 잡힌다.
실무에서도 Optional, Stream, Flux 같은 다양한 상황에서 자주 마주치는 패턴이니 꼭 익혀두자.