flatMap과 map은 둘 다 함수형 프로그래밍에서 사용되는 메소드로 컬렉션 내부의 요소들에 대해 변환 작업을 하는 데 사용한다.
map
map은 'A를 B로 바꾼다' 한마디로 정리할 수 있다. 컬렉션의 각 요소를 주어진 조건에 따라 변화시키고 새로운 컬렉션을 반환한다. 원래의 컬렉션과 동일한 크기를 가진다는 것이 특징이다. flatMap과 비교해보면 크기의 의미가 무엇인지를 알 수 있다.
val numbers = listOf(1, 2, 3, 4, 5)
val squares = numbers.map { x -> x * x }
// [1, 4, 9, 16, 25]
정수 1, 2, 3, 4, 5가 들어있는 컬렉션의 요소들을 제곱하고 싶다면 map을 이용하면 된다. A -> B로 바꾸는 것이 map이기 때문이다. 컬렉션을 순회하기 위해 map을 사용하는 경우가 종종 있는데 목적에 맞지 않는 사용은 되도록이면 지양할 필요가 있다. for loop를 쓰도록 하자. map을 사용했다면 변화라는 의도가 내포되어 있어야 한다.
flatMap
map과 비슷하게 생긴 이것은 유사하지만 return 값이 컬렉션이 아닌 평면화되는 차이점이 있다. 메소드의 이름에서도 알 수 있듯이 flat하게 만든다. 각 요소를 함수에 적용하여 변환된 값을 하나의 평면화된 컬렉션으로 반환한다.
val sentences = listOf("Hello world", "Louis world") // [Hello world, Louis world]
val words = sentences.flatMap { it.split(" ") }
// [Hello, world, Louis, world]
val sentences = listOf("Hello world", "Louis world")
val words = sentences.map { it.split(" ") }
// [[Hello, world], [Louis, world]]
평면화라는 말이 쉽게 이해가 가지 않을 수도 있다. map을 통해 똑같은 작업을 수행하면 그 차이를 구분할 수 있다. 올바른 사용은 아니지만 map을 이용해 요소들을 순회하며 공백을 잘라내면 words는 2차원 리스트가 된다. 각 요소를 또 하나의 리스트로 만들고 이를 매 요소마다 수행한다.
반면 flatMap은 요소 각각에 원하는 처리를 해준 후 하나의 평면화된 1차원 컬렉션을 반환한다. 다차원 구조를 단일한 1차원의 형태로 만드는 것이 바로 평면화이다. flatMap 또한 map처럼 각 요소마다 새로운 리스트를 만든다. 그 후에 하나의 평면화된 리스트로 반환한다.
Reactor에서의 map과 flatMap
데이터 스트림을 변환하는 용도로 사용되며, 쓰임에서 차이가 있다.
map
val numbers = Flux.just("1", "2", "3", "4", "5")
.map { it.toInt() }
Flux는 1~N개의 데이터를 처리하는 것을 의미하고 이와 비교되는 개념이 Mono(0~1개)이다. just() 메소드를 통해 스트림을 생성할 수 있는데 이를 사용할 때는 반드시 데이터가 존재해야 한다. 위 코드에서 Flux.just("1", "2", "3", "4", "5")의 의미는 1, 2, 3, 4, 5 데이터를 차례대로 발생시키고 완료를 의미하는 complete 신호를 발생시킨다.
이렇게 발생되는 데이터를 map이라는 메소드를 이용하여 A -> B를 수행할 수 있다. 위에서 설명한 map의 일반적인 활용법과 똑같다. 다만 리액터에서는 이렇게 변화된 데이터가 새로운 데이터 스트림으로 반환된다.
flatMap
val words = Flux.just("Louis", "Stella", "Maris")
.flatMap { word -> Mono.just("$word length is ${word.length}") }
데이터 스트림에서 값을 추출하고 해당 값에 대한 다른 데이터 스트림을 생성하는 데 사용된다. 위에서 언급했던 것처럼 새로운 데이터 스트림이 중첩된 스트림의 결과를 가져올 수 있으므로 평면화된 단일 스트림 데이터를 반환한다. 각 문자열에 대한 Mono를 생성하며, 이를 평면화하여 하나의 데이터 스트림으로 반환한다. 각 요소에 대해 비동기적으로 Mono를 처리하고 이를 Flux 형태로 반환할 수 있다.
val words = Flux.just("apple", "banana", "cherry")
.map { word -> "$word length is ${word.length}" }
만약 map을 사용한다면, Mono를 반환하던 함수를 사용해도 각 요소마다 새로운 데이터 스트림을 생성할 수 없기 때문이다. 문자열의 개수와 동일한 수의 요소를 포함하는 Flux가 생성된다.
새로운 데이터 스트림을 생성해야 하는 이유는 비동기 처리, 데이터 베이스 조회, 외부 API 호출등에 필요하기 때문이다. 데이터 처리 중에 다른 비동기 작업을 수행해야 하는 경우 해당 작업을 Mono나 Flux로 감싸서 flatMap을 활용해 새로운 데이터 스트림을 생성할 수 있기 때문이다. 또한 데이터 베이스를 조회할 때 그 결과를 위의 작업과 같이 수행할 수 있고, 마지막으로 외부 API 호출 결과를 감싸 새로운 데이터 스트림을 생성할 수 있기 때문이다.
val numbers = Flux.just(1, 2, 3, 4, 5)
.flatMap { number ->
Flux.range(1, number)
.map { subNumber -> subNumber * 2 }
}
새로운 데이터 스트림을 만들면 위와 같은 활용을 할 수 있다. 각 요소에 대해 Flux.range() 함수를 호출하여 새로운 데이터 스트림을 만들고, flatMap을 통해 통합하여 하나의 Flux로 반환한다.
즉, map은 값을 변환할 때 사용하고 flatMap은 값에 따라 다른 데이터 스트림을 생성할 때 사용한다.
'Java & Kotlin & Spring' 카테고리의 다른 글
i18n을 도입하여 다국어(국제화) 서비스 제공 (1) | 2023.10.04 |
---|---|
스프링 ServiceLocatorFactoryBean으로 팩토리 메소드 패턴 구현 (1) | 2023.08.23 |
반복되는 필드를 모아 Entity를 만드는 @MappedSuperClass (1) | 2023.08.16 |
Mac에서 JDK 제거하기 (0) | 2022.12.17 |
댓글