BackEnd/자바

Optional 에서 flatmap 과 map

ssseung 2022. 4. 7. 00:09

자꾸 헷갈리는 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 같은 다양한 상황에서 자주 마주치는 패턴이니 꼭 익혀두자.

반응형