brunch

You can make anything
by writing

C.S.Lewis

LiveData에서 Flow로 변형해보기

LiveData -> Flow(StateFlow & SharedFlow)


안녕하세요. 서비스 개발팀에서 Android 개발을 하고 있는 신입 두루라고 합니다.

최근 프로젝트를 진행하면서 기존 LiveData를 Flow로 적용했던 내용에 대해서 소개하려고 합니다.


StateFlow & SharedFlow에 대해서 이야기를 하기 전에 기존에 LiveData는 어떤 것인지에 대해서 이야기를 해보려고 합니다.



LiveData 

 LiveData의 공식 문서를 보면 관찰 가능한 데이터 홀더 클래스라고 합니다. 

 또한 LiveData는 생명주기를 인식할 수 있는 장점이 있으며, Observer 클래스를 통해서 Lifecycle의 State가 STARTED, RESUME 상태일 경우 활성화시키며, 그 외의 State에 대해서는 비활성화할 수 있는 장점 또한 존재를 합니다. 


공식문서를 살펴보면 LiveData의 장점들은 아래와 같이 명시되어 있습니다. 

UI와 데이터 상태의 일치 보장 : LiveData의 값이 변경될 경우, Observer 객체에 알려서 데이터 변경 시 알 수 있는 특징이 있습니다.  
메모리 누수 없음 : LiveData는 생명주기와 긴밀하게 연결되어 있어서 연결된 생명주기가 종료 시 자동으로 삭제가 되는 특징이 있습니다.  
중지된 활동으로 인한 비정상 종료 없음 : 위에서 말했던 활성화 상태의 특징을 가지고 있습니다. 생명주기의 상태에 따라서 활성화되고 비활성화되므로 비활성화 시 에는 LiveData가 이벤트에 대해서 받지 않게 됩니다.  
생명주기를 더 이상 수동으로 처리하지 않음 : 처음 있는 장점과 연관이 되어 있으며, 생명주기의 상태를 개발자가 더 이상 인식하지 않아도, 내부적으로 자동으로 인식을 하게 해주는 특징이 있습니다.  
최신 데이터 유지 : LiveData가 비활성화되었다가 활성화될 때 최신 데이터를 받아올 수 있습니다.(이 장점으로 인하여 발생하는 문제에 대해서는 아래에서 같이 살펴보도록 하겠습니다.
적절한 구성 변경 : 화면 회전등에 대한 이벤트에 대해서도 값을 가지고 있는 특징이 있습니다.

 

그렇다면 이러한 장점이 많은 LiveData를 왜 Flow로 마이그레이션을 하려고 할까요?
LiveData는 Android 플랫폼에 종속적이나, Flow는 순수 Kotlin 라이브러리라는 차이점이 존재합니다.
Flow API의 zip, flapMapMerge 등의 다양한 함수들을 활용하여 좀 더 편리한 기능들을 제공한다는 차이점도 있습니다.
그렇다면 LiveData에서 Flow로 전환할 때 단점은 어떤 것들이 있을까요?

안타깝게도 LiveData의 생명주기를 인지할 수 있는 장점이 Flow에서는 알 수 있는 방법이 없습니다.

또한 Flow는 상태가 없으므로, 값이 할당된 것인지, 현재 값이 무엇인지에 대해서 알기가 어렵습니다.

또한 LiveData에서는 값이 변경될 때마다 알 수 있는 장점이 있지만, Flow의 경우 Cold Stream방식으로 연속해서 계속 들어오는 데이터를 처리할 수 없으며, collect 되었을 때만 생성되고 값을 반환하는 차이점이 존재합니다.


Hot Stream? Cold Stream? 

Hot Stream : collect() 또는 subscribe를 호출할 때마다 flow block 가 호출되지 않는다. 즉 collect() 시점 이후에 emit 된 데이터를 전달받게 됩니다(1개의 스트림에서 다중 구독자한테 전달, 구독자가 없어도 전달이 가능해집니다. 
Cold Stream : collect() 또는 subscribe를 호출할 때마다 flow block 이 재 실행됩니다. (1~10까지 emit 하는 것이 있으면 collect 할 때마다 매번 1~10을 전달을 받는다, 1개의 스트림에는 1명의 구독자가 존재)  


바로 이러한 Flow의 한계점들을 보완하기 위해서 나온 것이 StateFlow & SharedFlow가 있습니다.


그렇다면 과연 StateFlow & SharedFlow란 무엇인지 살펴봅시다.

StateFlow 

StateFlow는 현재 상태와 새로운 상태 업데이트를 collector에 내보내는 Observable 한 상태 홀더 Flow이며, LiveData의 장점처럼 값을 가지고 있습니다.

아래와 같이 StateFlow의 내부 코드를 살펴보면 value라는 값을 가지고 있으며, 이전에 있던 값과 다른 값이 들어왔을 때 collect로 수집이 가능해진다는 특징을 가지고 있습니다.


그렇다면 이러한 StateFlow에는 어떠한 특징이 있을지 살펴봅시다.

StateFlow는 항상 값을 가지고 있으며(위에 내부 코드로 보았듯 value를 가지므로), 오직 한 가지 값을 가지고 있습니다.
이러한 value를 가지고 있어야 하므로, StateFlow는 초기값이 존재해야 한다는 특징도 있습니다.
StateFlow는 collector의 수에 상관없이 항상 구독하는 있는 것의 가장 최신 값을 가져올 수 있습니다. 
또한 위에서 말했던 Flow의 cold stream과 다르게 hot stream 데이터 홀더 클래스입니다.


StateFlow의 경우 value를 가지고 있으므로, 상태 값 & 화면을 구성하는 데이터들에 대해서 사용하는 것이 좋지 않을까 라는 생각이 듭니다. 


SharedFlow

SharedFlow는 전체적으로 StateFlow와 유사하지만, 차이점으로는 SharedFlow는 아래의 코드를 보면 알 수 있지만, value값이 존재를 하지 않으며, replayCache가 존재를 합니다.

replayCache는 몇 개의 값을 저장해 두고 받을지 개발자가 설정을 할 수 있습니다.

내부 코드를 보면 알 수 있듯이, Flow → SharedFlow → StateFlow 이러한 형식으로 구성되어 있는 것을 알 수 있습니다.


그렇다면 SharedFlow는 어떠한 특징이 있을지 살펴봅시다.

SharedFlow는 StateFlow와 달리 위의 코드에서도 볼 수 있듯이, 값을 가지지 않으며, 초기값을 가지고 있지 않아도 됩니다.
또한 위의 replayCache에서의 몇 개까지 값을 캐싱하고 있을지에 대해서 인자로 정의를 할 수 있습니다.
그리고 위에서 말했던 Flow의 cold stream과 다르게 SharedFlow 또한 hot stream 데이터 홀더 클래스입니다.
SharedFlow의 경우 파라미터로 아래와 같이 replay, extraBufferCapacity, onBufferOverflow를 받을 수 있습니다. 

replay : 이전 이벤트를 방출할지에 대한 정의 (O : 방출 X, n : n개 전부터 방출)

extraBufferCapacity : 추가 버퍼 생성 여부 (1 : 생성) 

onBufferOverflow : 버퍼를 초과할 경우 어떻게 처리를 할지 

        - DROP_OLDEST : 오래된 데이터부터 삭제 

        - SUSPEND : 버퍼가 꽉 차게 되면 emit시 blocking 

        - DROP_LATEST : 최근 데이터부터 삭제 

 이렇게 파라미터 값을 받아서 정의를 해줄 수 있는 특징이 있습니다.


SharedFlow의 경우 StateFlow와 다르게 value를 가지고 있지 않으므로, emit시 바로 collect(수집)이 되므로, 클릭 이벤트 & 전환 등의 이벤트들에 대해서 사용하는 것이 좋지 않을까 라는 생각이 듭니다.




그렇다면 이제 StateFlow & SharedFlow를 어떻게 사용하는지에 대해서 살펴보도록 합시다.

StateFlow (ViewModel)

StateFlow (Activity or Fragment)


SharedFlow (ViewModel)

SharedFlow (Activity or Fragment)


위의 예시를 보다 보면 launchWhenStarted라는 키워드가 나오는데 이것이 무엇인지에 대해서 살펴보도록 하겠습니다.
launchWhenStarted란 위에 말했던 LiveData의 장점이자 Flow의 한계였던 생명주기를 인식 못한다는 점을 위해서 나오게 되었습니다.
기존 lifecycleScope.launch를 사용할 경우 해당 생명주기가 Destroy 될 때까지 수집한다는 단점이 존재하게 됩니다.
이렇게 된다면, 앱이 백그라운드로 갔을 경우에도 Flow는 계속 수집을 하게 되고, 최악의 경우 메모리 문제로 앱이 종료될 수도 있는 상황이 발생하게 됩니다.


이러한 문제를 해결하기 위해서 나온 repeatOnLifecycle, launchWhenStarted, launchWhenResumed, launchWhenCreated이 있습니다.

아래의 repeatOnLifecyle의 내부 코드를 살펴보면 Lifecycle의 상태에 따라서 collect(수집)을 할지, 멈출지에 대해서 설정을 할 수 있습니다.


launchWhenStarted 또한 내부 코드를 살펴보면 Lifecycle의 상태에 따라서 collect(수집)을 할지, 멈출지에 대해서 설정할 수 있습니다.


위의 내부 코드들을 보면 이 둘에 대해서 차이점은 없을지에 대한 의문이 생기게 되는데, 차이점으로는 아래와 같이 동작한다는 차이점이 존재합니다.

repeatOnLifecycle의 경우 생명주기의 상태에 따라서 (시작 → 정지 → 재시작)
lifecycleWhen***의 경우 생명주기의 상태에 따라서 (시작 → 정지 → 재개)


그럼 여기까지 살펴보다 보면 한 가지 궁금증이 생기게 되는데, 그럼 Flow를 StateFlow or SharedFlow로 사용할 수 있는 방법은 없을까?라는 의문이 생기게 됩니다.
물론 Flow → SharedFlow → StateFlow로 되어 있으므로, Flow를 StateFlow & SharedFlow로 변환을 하는 것이 가능합니다.
아래의 예시처럼 Flow → StateFlow, Flow → SharedFlow로 변형이 가능합니다.


Flow → StateFlow 

    - stateIn : Flow를 StateFlow로 변환하는 작업입니다. 

    - scope : StateFlow가 Flow로부터 데이터를 받을 CoroutineScope 

    - started : Flow로부터 언제부터 데이터를 받을지, 멈출지 명시 

    - initialValue : StateFlow에 저장될 초기값    


Flow → SharedFlow 

    - sharedIn : Flow를 SharedFlow로 변환하는 작업입니다. 

    - scope : SharedFlow가 Flow로부터 데이터를 받을 CoroutineScope

    - started : Flow로부터 언제부터 데이터를 받을지, 멈출지 명시 

    - replay : 이전 이벤트를 방출 (0 = 방출 X, n= n개 전부터 방출)    


+ 추가적으로 위의 예시를 보다 보면 collectLatest라는 키워드가 나옵니다. 마지막으로 이 키워드에 대해서 살펴보면서 마무리하려고 합니다.
collect란?
Flow는 emit을 통해서 데이터를 발행하는 역할을 하며,       
Flow에서 발행하는 데이터들은 collect의 action에서 데이터를 순차적으로 받아 suspend fun을 실행하도록 구성이 되어 있습니다. 

collect : 새로운 데이터가 발행되더라도 이전 데이터의 처리가 끝날 때까지 대기를 합니다.

collectLatest : 새로운 데이터가 발행되면 이전 데이터의 처리를 취소 & 새로운 데이터의 처리를 시작합니다.

collectIndexed : 새로운 데이터가 들어오면 데이터와 함께 인덱스 값도 발행을 합니다.


이렇듯 Flow를 collect 하는 방법에도 collect, collectLatest, collectIndexed가 있으므로, 상황에 따라서 맞는 수집 방법을 선택해서 사용하면 될 것 같습니다.

(참고로 Ui에 보여줘야 하는 데이터의 경우 Android 공식문서에서는 collectLatest를 사용하도록 권장을 하고 있습니다.)



이상으로 LiveData → Flow(StateFlow, SharedFlow)로 변형을 하면서 알게 된 내용들에 대해서 살펴보았습니다.

이전에 LiveData를 주로 사용하다 Flow(StateFlow & SharedFlow)에 대해서 알게 되면서 적용을 하면서 하는데 잘못 사용하고 했던 경험도 있었지만, 그렇게 하면서 어떤 것들인지에 대해서 많이 배울 수 있었던 시간이었습니다.

혹시 기존 프로젝트에서 LiveData를 사용하신다면 Flow(StateFlow & SharedFlow)로 변환하는 시간을 가져보는 것도 좋을 것 같다는 생각이 듭니다.

(앞으로 Flow에 대한 점점 더 많은 기능들이 나오고 있고, 테스트 등을 위해서 변환 작업을 해보는 것도 좋을 것 같습니다.)

긴 글 읽으시느라 정말 고생 많으셨습니다!

브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari