본문 바로가기

Kotlin

Zip vs Combine

개요

상품 상세 화면에서 상품 상세 정보(DetailState)와 퀵 메시지(QuickMessageUiModel) 정보의 결합을 통해 UI 상태 관리를 할 데이터(QuickMessageViewState)를 생성해야했다.

상품 상세정보는 Server API를 통해 화면 진입 시 먼저 가져오고, 퀵 메시지 정보는 Firebase Remote Config를 통해 가져온다.

이 과정에서 당연히 상황에 알맞은 연산자를 사용해야 했음에도 불구하고 가볍게 생각하고 지나쳐버렸다.

(Zip을 사용해야 하는 상황인데… Combine을..?)

Before

    /**
     * 상품 상세 정보와 퀵 메시지 결합
     */
    fun getCombinedData(): Flow<Pair<DetailState, QuickMessageUiModel>> {
        return combine(_detailState, _quickMessage) { detailState, quickMessage ->
            Pair(detailState, quickMessage)
        }
    }

그 후에는 update 되어있는 상품 상세 정보(detailState)와 퀵 메시지 데이터(quickMessage)를 결합하였다.

바로 이 부분에서 문제가 되었다.
combine의 용도를 잘 모른다면, 퀵 메시지와 상품 상세 정보와의 연관성을 모른다면 그냥 지나칠 수 있는 코드이다. (난 알았는데 왜,,? 🤷‍♂️)

이 코드는 반드시 Combine보다 Zip 연산자를 사용하는 것이 맞다.

Combine과 Zip에 대해 설명하기 전에 상황을 먼저 간략하게 설명하자면, 각각의 퀵 메시지를 클릭하면 어떠한 동작을 하게 된다.

이 때 퀵 메시지의 내용과 상품 상세 정보에서 받아온 itemIdx를 함께 알고 있어야한다.
즉 퀵메시지와 상품 상세정보는 매칭이 되어야 하고, 두 개가 짝을 이뤘을 때 방출이 되는 것이 맞다.

zip vs combine

이 둘의 공통점은 ‘결합’을 한다는 것이고 아래와 같은 차이점이 있다.

zip

  • 여러 Flow의 요소를 동기화하고 결합해야 하는 경우에 적합.
  • output Flow는 두 input Flow 모두 발행할 때만 방출하고 그렇기에 순서가 유지된다.
  • 두 input Flow 중 하나가 다른 Flow보다 더 자주 발행하는 경우 느린 Flow가 따라잡을 때까지 대기한 후 결합된 요소를 발행한다.

ex) 사용자 ID와 사용자의 이름을 매칭하여 방출할 때

private fun main(): Unit = runBlocking {

    println("\nCombine")
    val combine = getIds().combine(getNames()) { id, name ->
        "$id: $name"
    }

    combine.collect {
        log(it)
    }
}

private fun getIds() = flow {
    for (i in 0 until 3) {
        delay(100L)
        emit(ids[i])
    }
}

private fun getNames() = flow {
    for (i in 0 until 3) {
        delay(200L)
        emit(names[i])
    }
}

combine

  • 별다른 동기화 요구 사항 없이 여러 Flow의 요소를 결합하고 입력 흐름이 생성될 때 마다 업데이트를 내보내려는 경우에 유용.
  • input Flow 중 하나가 발행할 때마다 output이 발행된다.
  • 제공된 transform 함수는 각 Flow에서 최신 요소를 결합하여 결합된 요소를 생성한다.
  • output Flow는 한 개의 inputFlow보다 자주 요소를 발행할 수 있고, input Flow가 발행하는 속도에 따라 달라진다.

ex) 온도와 습도를 함께 보여주는 온도계에서 온도와 습도가 각각 업데이트 될 때 마다 방출할 때

private fun main(): Unit = runBlocking {
    val zip = getIds().zip(getNames()) { id, name ->
        "$id: $name"
    }
    println("\nZip")
    zip.collect {
        log(it)
    }

    println("\nCombine")
    val combine = getIds().combine(getNames()) { id, name ->
        "$id: $name"
    }

    combine.collect {
        log(it)
    }
}

private fun getIds() = flow {
    for (i in 0 until 3) {
        delay(100L)
        emit(ids[i])
    }
}

private fun getNames() = flow {
    for (i in 0 until 3) {
        delay(200L)
        emit(names[i])
    }
}

위와 같은 차이점들로 인해 둘이 함께 방출되어야 하는 내 상황에서는 combine을 쓰는 것은 비효율적이다

After

    /**
     * 상품 상세 정보와 퀵 메시지 결합
     */
    fun bindGetItemDetailQuickMessage() {
        viewModelScope.launch {
            _detailState.zip(fetchQuickMessage()) { itemDetail, quickMessage ->
                QuickMessageViewState(itemDetail, quickMessage)
            }.collect { result ->
                _quickMessageState.update {
                    it.copy(
                        detail = result.detail,
                        quickMessage = result.quickMessage
                    )
                }
            }
        }
    }
반응형