프로그래밍/Kotlin

Flow.map() vs Flow.transform()

Lou Park 2023. 4. 2. 23:12
fun getPagedFavoritePosts(): Flow<PagingData<PostEntity>> = Pager(
        config = PagingConfig(25),
        pagingSourceFactory = {
            GenericPagingSource { afterKey ->
                redditApi.loadFavoritePosts(getUserName(), afterKey)
            }
        }
    ).flow.transform { pagingData -> emit(pagingData.map { (it.data as PostDto).map() }) }

우연히 오픈소스 코드를 보다가, 나라면 map을 썼을 것 같던 구문에 transform을 사용한 것을 발견했다. transform? 처음 들어봤는데 이름이 map과 왠지 비슷한 일을 할 것같아서 찾아보았다.

 

map

Flow의 각 값을 변환하여 새로운 요소로 매핑하여 Flow를 반환한다.

val squaredFlow = flowOf(1, 2, 3, 4).map {
    it * it 
}

 

transform

Flow의 각 값을 변환하는 함수를 적용한다. transform의 receiver는 FlowCollector이므로 요소를 변환하거나, 건너 뛸 수도 있다. 또 emit 함수를 여러번 호출하여 여러 요소를 추가할 수도 있다.

val squaredFlow = flowOf(1, 2, 3, 4).transform { 
    emit(it * it) 
}

이 예시는 map에서도 비슷하지만, 예컨대 이런 것도 가능하다는 것이다.

// 홀수의 제곱만을 가지는 Flow
val squaredOddFlow = flowOf(1, 2, 3, 4).transform {
    if (it % 2 == 1) {
        emit(it * it)
    }
}

 

메모리 사용량?

그럼 전부 map보다 유연한 transform을 쓰는 것이 좋지않나...생각이 들던 찰나, 메모리 사용량을 살펴보기로 했다. 미묘하지만 똑같이 사용했을 경우 map이 메모리 사용량이 더 적었다.

runBlocking {
    flowOf(1, 2, 3, 4).map {
        it * it
    }.collectLatest {
        printMemory()
    }
}
map()
Used memory: 6111 KB
Used memory: 8161 KB
Used memory: 8192 KB
Used memory: 8685 KB
runBlocking {
    flowOf(1, 2, 3, 4).transform<Int, Int> {
        emit(it * it)
    }.collectLatest {
        printMemory()
    }
}
transform()
Used memory: 6111 KB
Used memory: 8687 KB
Used memory: 8687 KB
Used memory: 9178 KB

하지만 사실은 map도 내부적으론 transform을 사용중이다. 유일한 차이점은 map에서 사용하는 것이 unsafeFlow라는 건데...
어째서 메모리 사용량의 차이를 부르는지는 잘 모르겠다. 어쨌든 map, filter 한방에 필요하다면 transform을 사용하자.

// map
public inline fun <T, R> Flow<T>.map(crossinline transform: suspend (value: T) -> R): Flow<R> = transform { value ->
    return@transform emit(transform(value))
}
@PublishedApi
internal inline fun <T, R> Flow<T>.unsafeTransform(
    @BuilderInference crossinline transform: suspend FlowCollector<R>.(value: T) -> Unit
): Flow<R> = unsafeFlow { 
    collect { value ->
        return@collect transform(value)
    }
}

// transform
public inline fun <T, R> Flow<T>.transform(
    @BuilderInference crossinline transform: suspend FlowCollector<R>.(value: T) -> Unit
): Flow<R> = flow { 
    collect { value ->
        return@collect transform(value)
    }
}

 

참고자료

https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/map.html
https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/transform.html