본문 바로가기
안드로이드/코틀린

(Kotlin) Flow - 1

by 나이아카 2023. 8. 10.

 LiveData와 Flow의 차이점을 정리하다보니 아직 블로그에 Flow에 대한 내용을 올린 적이 없다는 사실을 알게 되었습니다. 확실히 아직 100% 이해하고 쓴다고 생각하지 않아 서술하지 않은 것 같은데 이 참에 공부하면서 조금씩 끄적거려 봐야겠습니다.


 flow를 인터넷에 검색하게 되면, Coroutine Flow라고 코루틴을 붙여서 많이들 사용합니다. 이는 Flow가 안드로이드 공식문서상으로도 코루틴을 기반으로 비동기로 여러 값들을 제공한다고 되어있기 때문입니다. 사실상 Flow와 Coroutine은 떼놓기 힘든 개념이라고 볼 수 있겠습니다. 

 이 Flow란 친구는 Coroutine의 데이터 스트림으로서 코루틴을 이용한 반응형 프로그래밍을 쉽게 작성할 수 있도록 지원하는 요소입니다. 반응형 프로그래밍에서는 항상 데이터를 흐름을 파악해 지속적으로 변경사항을 전파해 데이터의 변경에 따른 UI update나 추가적인 코드의 실행들을 매끄럽게 진행할 수 있도록 하는 패러다임입니다. 반응형 프로그래밍에서는 새로운 데이터가 들어올 때 마다 데이터를 전달하는 publisher와 데이터를 전달 받기 위해 publisher를 구독 하는 subscriber가 있습니다(자세한 내용은 반응형 프로그래밍을 다루는 게시글이 아니므로 생략하겠습니다!). 이러한 프로그래밍에서 Flow는 데이터를 발행하는 Publisher와 subscriber를 이어주는 데이터 스트림의 역할을 하게 됩니다.

공식 문서에서의 Flow

 데이터 스트림을 설명하기 위해 공식문서에서는 위와 같은 그림을 사용하고 있는데요. 먼저 데이터 스트림에는 Producer(데이터 생산자), Intermediary(중개자), Consumer(데이터 소비자)를 가지고 있습니다.

 생산자는 데이터를 생산하는 역할을 하며 주로 remote server(API 호출 등과 같은 백엔드와의 통신)나 local db(Room, SQlite등을 이용한 저장소)에서 데이터를 받아오는 코드를 의미합니다. Flow에서 emit 코드가 존재하는 class를 생산자라고 생각하면 되겠습니다.

 중개자의 경우에는 꼭 데이터 스트림에서 필수적인 요소는 아닌데, 먼저 생산자에게서 받은 데이터가 Response 형태의 데이터이고 소비자에게 전달해야 할 데이터가 UiState라고 가정한다면, Reponse 내부에 있는 데이터를 개발자가 미리 정의해둔 코드에 따라 중개자가 UiState로 가공해서 값을 전달하는 역할을 합니다. 이는 converter로 대표되는 여러가지 형태의 코드를 통해 느낌을 파악하실 수 있습니다. 이 중개자의 경우, Flow에서 지원하는 여러가지 중간 연산자(map, filter, onEach 등)를 이용할 수 있습니다.

 마지막으로 소비자는 collect를 통해 데이터 수신을 대기하는 코드를 의미합니다. 주로 안드로이드에서는 viewModel 내부에서 아래 코드와 비슷한 형태로 작성되게 됩니다.

viewModelScope.launch {
    flow.collect { data ->
        //...
    }
}

 위 코드를 통해 viewModel에 미리 정의되어 있던 SharedFlow 나 StateFlow에 데이터가 변경된 데이터가 전달되어 DataBinding등을 통해 UI를 새로 구성하게 되는 것입니다.

 

 위의 설명이 Flow의 기본적인 개념이라고 볼 수 있겠습니다. 우리는 주로 방금까지 설명했던 Flow API의 Flow와 StateFlow, SharedFlow로 나눠서 용도에 맞게 사용할 예정입니다.

 먼저 flow를 블럭을 통해 생성하는 코드입니다.

class WithdrawalUserUseCase @Inject constructor(private val repo : UserRepository) {

    operator fun invoke(userId: Int, secretKey : String) : Flow<CommonResponse> = flow {
        runCatching {
            emit(CommonResponse.Loading)
            repo.withdrawalUser(userId, secretKey)
        }.onSuccess {
            emit(it)
        }.onFailure { e ->
            e.printStackTrace()
            emit(CommonResponse.Failed(999, e.message?:"error"))
        }
    }
}

 위는 제가 사용하는 코드 중, 회원탈퇴를 위한 코드인데 중요 코드들은 다 다른 곳에 있어서 그대로 가져왔습니다. 이 부분에서는 Loading과 성공, 그리고 실패시 각각 emit을 하고 있는 것을 확인할 수 있습니다. 이렇게 작성한 코드는 ColdStream의 특성을 지니고 있는데, 이는 이 flow를 수집하는 각 Collector(소비자)들이 flow를 호출할 때 마다 새로운 데이터 스트림을 생성해 데이터를 받아온다는 것을 의미합니다. 이는 위 코드인 WithdrawalUserUseCase를 각기 다른 2개의 Coroutine에서 호출된 경우, 둘 다 다른 데이터 스트림으로 판단되어 서로 독립적으로 데이터를 제공한다는 것을 의미합니다.

 이렇게 원하는 방향으로 CommonResponse(sealed class)를 방출하기로 했으니 이 방출한 데이터를 어디서 가져오는 지 살펴보겠습니다. 

fun withdrawalUser() = viewModelScope.launch {
    runCatching {
        withdrawalUserUseCase(
            RunnerBeApplication.mTokenPreference.getUserId(),
            BuildConfig.WITHDRAWAL_API_KEY
        ).collect {
            when (it) {
                is CommonResponse.Success<*> -> _withdrawalState.emit(UiState.Success(it.code))
                is CommonResponse.Failed -> {
                    if (it.code <= 999) _withdrawalState.emit(UiState.NetworkError)
                    else _withdrawalState.emit(UiState.Failed(it.message))
                }
                is CommonResponse.Loading -> _withdrawalState.emit(UiState.Loading)
                else -> _withdrawalState.emit(UiState.Empty)
            }
        }
    }.onFailure {
        _withdrawalState.emit(UiState.NetworkError)
    }
}

 위의 코드는 viewModel 내에 존재하는 코드입니다. 여기서 collect를 통해 withdrawalUserUseCase가 emit으로 뱉는 데이터들을 불러올 수 있습니다. 이때 불러온 데이터를 다시 재가공하여(여기서는 특별한 구현부는 없습니다.) 또 다시 State라는 MutableStateFlow에게 전달해줍니다.

 여기서 UiState(Sealed class)라는 데이터를 받는 _withdrawalState는 아래와 같습니다.

private val _withdrawalState: MutableStateFlow<UiState> = MutableStateFlow(UiState.Empty)
val withdrawalState: StateFlow<UiState> get() = _withdrawalState

 여기서 MutableStateFlow와 StateFlow가 나옵니다. Mutable이 붙은 모습으로 보아 StateFlow는 변경이 불가능한 형태인 것을 확인할 수 있습니다. 이런 StateFlow에는 여러가지 특징이 존재하는데, 보기 쉽게 특징들을 나열해보겠습니다.

  • LiveData와 다르게 생명주기를 가지고 있지 않기 때문에 viewLifecycleOwner.lifecycleScope.launch{}를 통해 직접 flow를 제어해줘야 합니다.
  • 현재 상태 및 상태 업데이트시 collect에 데이터를 전달하는 홀더 Flow입니다. 이는 LiveData처럼 데이터의 수집이 일어날 때 항상 구독중인 다른 구독자에게 데이터를 반환한다는 것을 의미합니다.
  • 하지만 StateFlow의 경우, 코드를 보면 default 값이 파라미터로 전달되어야 한다는 것을 알 수 있습니다. 이는 위의 코드에서 MutableStateFlow의 파라미터로 UiState.Empty가 들어간 것으로 확인할 수 있습니다. 이는 UI에 StateFlow를 연결해두는 경우, default 데이터를 잘 정의해두면 UI의 '정보 없음' 형태를 데이터로 가질 수 있게 됨을 의미합니다.
  • 이렇게 default 값을 정의하는 이유는 StateFlow의 경우, collect를 시작할 때 항상 StateFlow가 마지막으로 가지고 있던 데이터를 바로 호출하기 때문입니다.
  • flow 빌더와 다르게 HotStream입니다. 이는 구독자가 StateFlow를 구독할 때 마다 새롭게 데이터스트림을 여는 것이 아니라 이미 실행되고 있는 flow의 값을 전달받는다는 것을 의미합니다. 실제로 위 코드에서 withdrawalState를 collect한 시점이 withdrawalUser 메소드를 실행하기 전이라면 UiState.Empty가, 실행해서 이미 성공 response를 전달했다면 UiState.Success가 collect 될 것입니다.
  • 또한 StateFlow는 값이 변하지 않는 경우 동일한 값을 저장하더라도 데이터를 구독자에게 전달하지 않습니다. 만약 위 코드를 기준으로 _withdrawalState.emit(UiState.Empty)를 연속해서 2번 실행시키더라도 withdrawalState를 collect하고 있는 곳에서는 1번의 UiState.Empty만 전달됩니다. 이는 LiveData의 중복 값 관련 문제를 심플하게 해결할 수 있도록 도와줍니다.

 

 위와 같은 StateFlow를 이용하면 많은 문제들이 해결됩니다. 더욱이 collect하고 난 이후의 동작은 거의 동일하기 때문에 주로 설정의 차이로 동작이 달라지게 됩니다.

private val _actionState: MutableSharedFlow<UiAction> = MutableSharedFlow()
val actionState: SharedFlow<UiAction> get() = _actionState

 가장 기본적인 형태의 SharedFlow입니다. 이 상태에서 사용하는 경우 StateFlow와의 차이점은 크게 2가지입니다.

  • collect를 시작하는 경우, collect 이전의 값을 참조하지 않기 때문에 default 값이 필요하지 않습니다.
  • 또한 현재 가지고 있는 값과 동일한 값이 들어오더라도 StateFlow와 다르게 동작합니다.(중복에 대한 검사가 없습니다.)

 

위와 같은 특징으로 인해 StateFlow는 XML 단계에서 LiveData를 대신해줄 수 있게 되었으며(default 값이 존재하고, 항상 값을 가지고 있는 특성이 UI에 딱 맞음) SharedFlow는 XML에 DataBinding할 수 없지만(값이 존재하지 않을 수도 있고, 이전 값이 유지되지 않는 sharedFlow는 UI의 특성에 맞지 않음) 대신 click과 같은 액션에 도움을 줄 수 있게 되었습니다.


이제 겨우 Flow가 뭘까 하고 사용하는 단계 같습니다. 그런데 코틀린으로 안드로이드를 작성하다가 자바로 안드로이드를 작성할 일이 있어 자바를 이용하니, 코틀린의 Flow, Coroutine등 자바에서는 사용할 수 없는 코드들이 너무나도 편리했음을 느끼게 됩니다. 잘 알지도 못하면서 사용하는 주제에 없으면 이렇게 불편하다는 게 참... 사용하면 할 수록 코틀린에 대해서 열심히 공부해야겠다는 생각이 듭니다.

'안드로이드 > 코틀린' 카테고리의 다른 글

Generic 이란  (1) 2023.12.08
Android - 시스템 앱 알림 상태 확인  (0) 2023.10.25
AAC Navigation의 특징  (0) 2023.06.02
JvmStatic 어노테이션  (0) 2023.03.16
Kotlin - Object 키워드(with SingleTon)  (1) 2023.02.17

댓글