Android

안드로이드 [Kotlin] - Bye LiveData, Hi StateFlow!

🤖 Play with Android 🤖 2023. 1. 8. 21:52
728x90


데이터 홀더 클래스

UI의 역할은 화면에 애플리케이션의 데이터를 표시하고 사용자와의 상호작용의 기본이 되는 지점이다. 사용자 상호작용(Ex: 버튼 누르기) 또는 외부 I/O 작업(네트워크 응답)으로 인해 데이터가 변할 때마다 변경사항을 반영하도록 UI가 업데이트되어야 한다.

 

 

 

 

LiveData

LiveData는 관찰 가능한 데이터 홀더 클래스이다. 다른 관찰 가능한 클래스와 달리 LiveData는 생명주기를 인식한다. 즉 Activity, Fragment, Service 등의 생명주기를 인식하여 앱 개발 시 보다 더 유지보수하기 쉽게, 테스트하기 쉽게 만들어주는 라이브러리이다.

 

공식문서에서 말하는 LiveData를 사용했을 때 장점은 아래와 같다.

  • Ensures your UI matches your data state(UI와 Data의 상태를 일치시킨다) :  LiveData는 observer pattern을 사용하고, Observer 객체의 데이터 변화를 감지한다. 그렇기 때문에, 어떤 데이터를 update 했을 때, 다시 데이터를 조회하는 코드를 쓰지 않아도 된다.
  • No Memory Leak(메모리 누수를 방지한다) : LiveData는 Activity, Fragment, Service의 생명 주기가 끝나는 즉시 데이터 옵저빙을 멈추기 때문에 누수를 걱정하지 않아도 된다. 만약 Activity, Fragment, Service의 생명 주기가 끝났는데도 데이터 변화를 감지한다면 메모리를 낭비하게 될 것이다.
  • No crashes due to stopped Activity(멈춘 Activity 때문에 앱이 충돌하는 일이 없다) : 생명주기 중에 InActive(OnStop(), OnDestroy())된 Activity가 있다면 LiveData는 어떤 이벤트도 받지 않는다.
  • 추가적으로 Room, Paging3 등 다른 Jetpack 라이브러리와의 높은 호환성도 장점이다.

위 내용을 봤을 때, LiveData는 단순히 옵저버 패턴을 통해 데이터의 변화를 관찰하고 UI를 업데이트하기에 용이하다는 것을 넘어 Activity 나 Fragment 등의 생명주기에 따른 관리도 잘해주는 라이브러리라는 것을 알 수 있다.


그렇기에 LiveData는 안드로이드 플랫폼에서 데이터를 관리하는 데에 지금까지도 굉장히 많이 사용되고 있으며, 주로 함께 사용하는 라이브러리로는 AAC DataBinding, AAC ViewModel 등이 있다.

 

 

LiveData의 단점

아키텍처 관점에서 LiveData의 단점

UI Layer(Presentation Layer)

  • Activity, Fragment 등 View와 ViewModel 등을 포함한다.
  • 즉 화면과 입력에 대한 처리를 하는 부분을 담당하는 Layer이다.
  • 이때 Activity와 Fragment , 즉 View는 데이터를 소유하는 것이 아니라 데이터를 표시하기만 하는 역할을 하므로 LiveData 인스턴스를 보유해서는 안된다. LiveData 객체는 주로 AAC ViewModel에서 관리하게 된다. 
  • 그리고 UI Layer는 Domain Layer에 대한 의존성을 가지고 있다.

 

Data Layer

  • Data Layer는 일반적으로 데이터에 대한 조회, 추가, 업데이트 및 삭제(CRUD) 호출을 실행하거나 시간 경과에 따른 데이터 변경사항에 관해 알림을 받는 함수를 작성한다.
  • Domain 계층의 Repository 인터페이스에 대한 구현을 Data Layer에서 한다. 

 

Domain Layer

  • 도메인 레이어는 UI 레이어와 데이터 레이어 사이에 있는 선택적 Layer이다.
  • 도메인 레이어는 복잡한 비즈니스 로직이나 여러 ViewModel에서 재사용되는 간단한 비즈니스 로직의 캡슐화를 담당한다.
  • 도메인레이어를 사용하면 UI Layer와 Data Layer의 의존성을 낮출 수 있다.
  • 만약 규모가 있는 프로젝트에서 ViewModel에서 다수의 Repository에 접근해야 하는 경우가 발생한다면 Domain Layer를 추가해 UseCase에서 데이터를 가공하여 ViewModel로 제공하고, ViewModel 안에서는 가공된 데이터를 그대로 받아 사용하도록 할 수 있다.

 

이때 중요한 포인트가 나온다.

  • Domain 계층은 안드로이드에 의존성을 가지지 않은 순수 Java 및 Kotlin 코드로만 구성해야 한다.
  • LiveData는 안드로이드 플랫폼에 속해 있기 때문에 순수 Java / Kotlin을 사용해야 하는 Domain Layer에서 사용하기에 적합하지 않다.

 

비동기 작업 측면에서 LiveData의 단점

LiveData는 UI와 밀접하게 연관되어 있어 오직 메인스레드(Main Thread)에서만 읽고 쓸 수 있다. 따라서 Data Layer에서 데이터를 처리할 때는 사용하기 어렵다. 데이터를 I/O 할 때에는 메인스레드(Main Thread)가 아닌 작업스레드(Worker Thread)에서 비동기 방식으로 처리되어야 하기 때문이다.

 

 

 

 

 

Flow의 등장

안드로이드 개발 언어로 코틀린이 자리 잡기 전까지는 위와 같은 문제점을 안고 있으면서도 안드로이드 진영에서는 별다른 선택권이 없었다. 그러나 코틀린 코루틴이 발전하면서, Flow 가 등장하게 되었고 Flow를 이용해서 LiveData를 대체할 수 있을지에 대한 기대가 생기기 시작했다.

 

Flow의 장점

 

  • Flow는 순수 Kotlin 라이브러리로 안드로이드 플랫폼에 종속적이지 않다.
  • zip(), onStart(), catch() 등 다양한 연산자를 제공하여 더 편리한 기능을 제공한다.

 

Flow의 한계

하지만 Flow는 LiveData를 대체하기에 몇 가지 치명적인 단점이 존재했는데

  • Flow는 생명주기를 인지할 수 없다.
  • Flow는 상태라는 것이 존재하지 않아 데이터 홀더로써 역할을 할 수 없다.
  • Flow는 콜드 스트림으로 연속해서 들어오는 데이터를 처리할 수 없으며, collect라는 트리거가 동작했을 때만 생성되고 값을 반환할 수 있다.

 

콜드 스트림 vs 핫 스트림

 

콜드 스트림(Cold Stream)

  • 하나의 소비자(Consumer)에게 값을 보낸다.
  • 생성된 이후에 누간가 소비하기 시작하면 데이터를 발행한다.
  • 예) 상태가 변하지 않는 값을 읽을 때(DB를 읽거나 URL을 통해 서버 값을 읽는 경우)

핫 스트림(Hot Stream)

  • 하나 이상의 소비자(Consumer)에게 값을 보낸다.
  • 데이터 발행이 시작된 이후부터 모든 소비자에게 같은 데이터를 발행하고, 구독자가 없는 경우에도 데이터를 발행한다.
  • 예) 상태가 변하는 값을 읽을 때

 

 

StateFlow

  • StateFlow는 데이터 홀더 역할을 하면서 Flow의 데이터 스트림 역할까지 한다. 
  • LiveData와 마찬가지로 value 프로퍼티를 통해서 현재 상태 값을 읽을 수 있다.
  • value는 LiveData와 마찬가지로 마지막 값 만을 보관한다.
  • Android Studio Arctic fox 버전부터는 AAC Databinding 에도 StateFlow가 호환된다.
@HiltViewModel
class TestViewModel @Inject constructor(
    private val testRepository: TestRepository
): ViewModel() {

    private val _testString = MutableStateFlow<String>("")
    val testString: StateFlow<String> = _testString

    fun getData() {
        viewModelScope.launch {
            _username.value = Repository.loadUserName()
        }
    }
}

 

MutableLiveData 앞에는 _ 가 붙은 네이밍 컨벤션을 확인할 수 있고 LiveData는 커스텀 게터를 통해 MutableLiveData에 접근하고 있는데, 이것은 https://kotlinlang.org/docs/coding-conventions.html#names-for-backing-properties 코틀린 공식 홈페이지의backing properties 관련 코틀린 컨벤션에 따른 표현이다.

 

 

 

StateFlow의 생명주기 인식

View가 STOPPED 상태가 되면 LiveData는 소비자를 자동으로 등록 취소하는 반면, StateFlow 또는 다른 흐름에서 수집의 경우 자동으로 중지하지 않는다. 이렇게 되면 해당 생명주기가 Destroy 될 때까지 StateFlow가 데이터를 수집한다는 단점이 있다. 이렇게 된다면 앱이 백그라운드로 갔을 경우에도 지속해서 데이터를 수집하므로 메모리 문제를 유발한다.

 

repeatOnLifecycle

  • repeatOnLifecycle 내부 코드를 보면 Lifecycle의 상태에 따라 데이터를 수집할지, 멈출지를 설정할 수 있다.
  • repeatOnLifecycle을 사용하면 View가 포그라운드에 있을 때로 한정 지어 데이터를 수집할 수 있다.
  • 좀 더 코드적으로 말하자면 repeatOnLifecycle 블록 내부에 있는 flow에 대한 collect는 View가 포그라운드에 있을 때만 진행된다.

코드예시

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private val viewModel: ViewModel by viewModels()
    private lateinit var adapter: Adapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                 viewModel.sampleDataList.collect { data ->
                     adapter.submitList(data)
            }
        } 
    }
}

 

여기서 주의할 점은 반대로 말하면 백그라운드에서 수행되어야 하는 무거운 작업은 repeatOnLifecycle 블록 안에서 수행하면 안 된다.

 

 

 

 

확장함수 활용하여 코드 중복 방지하기

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private val viewModel: ViewModel by viewModels()
    private lateinit var adapter: Adapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                 viewModel.sampleDataList.collect { data ->
                     adapter.submitList(data)
            }
        } 
    }
}

위 코드는 repeatOnLifecycle을 통해 ViewModel의 데이터를 받아와 RecyclerView의 어댑터에 submitList 하는 코드를 형상화한 것이다. 딱 봐도 LiveData를 사용할 때 보다 보일러 플레이트 코드가 늘어난 것이 보인다. 

이를 확장함수를 통해 간단하게 사용해 보자.

 

 

Activity에서 StateFlow 데이터 수집하기

fun <T> AppCompatActivity.collectStateFlow(flow: Flow<T>, collect: suspend (T) -> Unit) {
    lifecycleScope.launch {
        repeatOnLifecycle(Lifecycle.State.STARTED) {
            flow.collect(collect)
        }
    }
}
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private val viewModel: ViewModel by viewModels()
    private lateinit var adapter: Adapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        collectStateFlow(viewModel.sampleDataList) { data ->
            adapter.submitList(data)
        }
    }
}

기존 6줄이었던 코드가 3줄로 줄어들었다. 만약 View에 RecyclerView가 많아진다면 줄어드는 보일러플레이트 코드는 더 많아질 것이다.

 

 

Fragment에서 StateFlow 데이터 수집하기

fun <T> Fragment.collectLatestStateFlow(flow: Flow<T>, collector: suspend (T) -> Unit) {
    viewLifecycleOwner.lifecycleScope.launch {
        viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
            flow.collectLatest(collector)
        }
    }
}

Activity와 똑같아 보이지만 한 가지 큰 차이가 있다. 바로 "viewLifecycleOwner"이다. Fragement에서 StateFlow를 수집할 때는 꼭 일반 LifecycleOwner가 아닌 ViewLifecycleOwner를 사용해야 한다.

 

간단히 이유를 말하자면 

  • Fragment LifeCycle은 onAttch ~ onDestroy의 생명주기를 가지며, Fragment ViewLifeCycle 은 createView ~ destoryView를 가진다. 즉 ViewLifeCycle이 Fragment의 LifeCycle보다 짧다.
  • Activity는 onDestory()가 호출되지 않은 상황에서 onCreate()가 호출될 일이 없지만 Fragment는 onDestroy()가 호출되지 않은 상태에서 onCreateView()가 여러 번 호출될 수 있다.
  • 만약 Lifecycle 에 따라 호출하는 CallBack 함수가 있다면 해당 LifeCycle이 Fragment 일 때, LifecycleOwner를 사용한다면 경우에 따라 fragment Destory 가 되지 않았는데 CallBack 함수가 여러 번 호출되는 상황을 경험할 수도 있다. 즉 Fragment는 이미 바뀌었는데 아직 살아있다고 착각하는 것이다. 경우에 따라 Fragment 가 Destory 되지 않았는데 새로운 observer가 등록되어 복수의 Observer가 호출되는 현상이 발생할 수 있다. (StackOverFlow에 관련된 질문들이 많다.)

 

 

 

 

여기까지 LiveData와 StateFlow의 차이, 그리고 StateFlow를 실제 코드에서 사용하는 법에 대해 알아보았다.