일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- 클래스
- 서비스 vs 쓰레드
- 안드로이드 유닛 테스트
- 서비스 쓰레드 차이
- 안드로이드 레트로핏 사용법
- 안드로이드 os 구조
- 안드로이드 유닛테스트란
- 안드로이드 라이선스 종류
- rxjava cold observable
- 안드로이드 유닛 테스트 예시
- ANR이란
- 2022 플러터 설치
- jvm 작동 원리
- rxjava disposable
- 큐 자바 코드
- android retrofit login
- 스택 큐 차이
- rxjava hot observable
- 2022 플러터 안드로이드 스튜디오
- 플러터 설치 2022
- 객체
- android ar 개발
- 자바 다형성
- jvm이란
- Rxjava Observable
- 안드로이드 라이선스
- ar vr 차이
- 스택 자바 코드
- 안드로이드 레트로핏 crud
- 멤버변수
- Today
- Total
나만을 위한 블로그
[Android] Flow vs LiveData 본문
Flow와 LiveData는 API를 통해 데이터를 받아올 때 자주 사용되는 요소들이다. MVVM 패턴으로 앱을 설계한 경우 뷰모델에서 데이터 바인딩을 통해 UI에 변경된 데이터를 표시할 수 있도록 뷰모델 안에 프로퍼티를 설정하는데 MutableStateFlow 또는 MutableLiveData로 타입을 설정한다.
그러나 개발하다 보면 결국 LiveData를 쓰는 게 더 낫지 않나 생각될 때도 있고, Flow를 쓸 경우 asLiveData()를 써서 Flow를 LiveData로 전환해 사용하는 예시도 가끔 있어서 뭘 어떻게 써야 할지 혼동이 올 수 있다. 그래서 두 개를 간단하게 정리하고 둘의 차이를 확인하는 포스팅을 쓰게 됐다.
Flow와 LiveData는 예전에 각각 포스팅한 적이 있지만 각각의 정의를 한번 더 확인하고 넘어간다. 참고하려면 아래 링크를 확인하면 된다.
https://onlyfor-me-blog.tistory.com/478
https://onlyfor-me-blog.tistory.com/310
먼저 Flow의 정의는 아래와 같다.
https://developer.android.com/kotlin/flow
코루틴에서 Flow는 단일 값만 반환하는 suspend function(정지 함수)과 달리 여러 값을 순차적으로 내보낼 수 있는 유형이다. 예를 들면 Flow를 써서 DB에서 실시간 업데이트를 수신할 수 있다. Flow는 코루틴 기반으로 빌드되며 여러 값을 제공할 수 있다. Flow는 비동기식으로 계산할 수 있는 데이터 스트림의 개념이다. 내보낸 값은 동일한 유형이어야 한다. 예를 들어 Flow<Int>는 정수값을 내보내는 Flow다.
Flow는 값 시퀀스를 생성하는 Iterator와 매우 비슷하지만 suspend function을 써서 값을 비동기적으로 생성, 사용한다. 예를 들어 Flow는 기본 쓰레드를 차단하지 않고 다음 값을 생성할 네트워크 요청을 안전하게 만들 수 있다.
데이터 스트림에는 3가지 항목이 있다.
- 생산자 : 스트림에 추가되는 데이터 생산. 코루틴 덕분에 flow에서 비동기적으로 데이터가 생산될 수도 있다
- (선택사항) 중개자 : 스트림에 내보내는 값이나 스트림 자체를 수정할 수 있다
- 소비자 : 스트림의 값을 사용한다
안드로이드에서 Repository는 일반적으로 UI 데이터 생산자다. 이 때 UI는 최종적으로 데이터를 표시하는 소비자다. UI 레이어가 사용자 입력 이벤트의 생산자고 계층 구조의 다른 레이어가 이 이벤트를 쓰기도 한다. 생산자, 소비자 사이의 레이어는 일반적으로 다음 레이어의 요구사항에 맞게 조정하기 위해 데이터 스트림을 수정하는 중개자의 역할을 한다...(중략)
LiveData는 아래와 같이 설명하고 있다.
https://developer.android.com/topic/libraries/architecture/livedata?hl=ko
LiveData는 식별 가능한(또는 관찰 가능한, Observable) 데이터 홀더 클래스다. 일반 식별 가능한 클래스와 달리 LiveData는 생명 주기를 인식한다. 즉, 활동, 프래그먼트, 서비스 등 다른 앱 구성요소의 생명 주기를 고려한다. 생명 주기 인식을 통해 LiveData는 액티비티 생명 주기 상태에 있는 앱 구성요소 관찰자만 업데이트한다.
Observer 클래스로 표현되는 관찰자의 생명주기가 STARTED 또는 RESUMED 상태면 LiveData는 관찰자를 활성 상태로 간주한다. LiveData는 활성 관찰자에게만 업데이트 정보를 알린다. LiveData 객체를 보기 위해 등록된 비활성 관찰자는 변경사항에 관한 알림을 받지 않는다.
LifeCycleOwner 인터페이스를 구현하는 객체와 페어링된 관찰자를 등록할 수 있다. 이 관계를 사용하면 관찰자에 대응되는 LifeCycle 객체의 상태가 DESTROYED로 변경될 때 관찰자를 삭제할 수 있다. 이는 특히 액티비티, 프래그먼트에 유용하다. 액티비티와 프래그먼트는 LiveData를 안전하게 관찰할 수 있고 생명주기가 끝나는 즉시 수신 거부되어 누수를 걱정하지 않아도 되기 때문이다
LiveData를 사용하면 다음의 이점이 있다
- UI와 데이터 상태 일치 보장 : LiveData는 관찰자 패턴을 따른다. LiveData는 기본 데이터가 바뀔 때 Observer 객체에 알린다. 코드를 통합해 이런 Observer 객체에 UI를 업데이트할 수 있다. 이렇게 하면 앱 데이터가 바뀔 때마다 관찰자가 대신 UI를 업데이트하므로 개발자가 업데이트할 필요가 없다
- 메모리 누수 없음 : 관찰자는 LifeCycle 객체에 결합돼 있으며 연결된 생명주기가 끝나면 자동 삭제된다
- 중지된 액티비티로 인한 비정상 종료 없음 : 액티비티가 백스택에 있을 때를 비롯해 관찰자의 생명주기가 비활성 상태에 있으면 관찰자는 어떤 LiveData 이벤트도 받지 않는다
- 생명주기를 수동으로 처리하지 않음 : UI 구성요소는 관련 데이터를 관찰하기만 할 뿐 관찰을 중지하거나 재시작하지 않는다. LiveData는 관찰하는 동안 관련 생명주기 상태 변경을 인식하므로 모든 것을 자동으로 관리한다
- 최신 데이터 유지 : 생명주기가 비활성화되면 다시 활성화될 때 최신 데이터를 수신한다. 예를 들어 백그라운드에 있던 액티비티는 포그라운드로 돌아온 직후 최신 데이터를 받는다
- 적절한 구성 변경 : 기기 회전 같은 구성 변경으로 인해 액티비티 또는 프래그먼트가 재생성되면 사용 가능한 최신 데이터를 즉시 받게 된다
- 리소스 공유 : 앱에서 시스템 서비스를 사용할 수 있도록 싱글톤 패턴을 쓰는 LiveData 객체를 확장해 시스템 서비스를 래핑할 수 있다. LiveData 객체가 서비스에 한 번 연결되면 리소스가 필요한 모든 관찰자가 LiveData 객체를 볼 수 있다
정리하면 아래와 같다.
- Flow는 여러 값을 내보내는 데이터 스트림이며, 코루틴 기반으로 빌드된다
- LiveData는 안드로이드 생명주기를 인식하는 데이터 홀더 클래스다. 때문에 액티비티, 프래그먼트에서 사용하기 좋음
또한 공식문서의 설명대로라면 굳이 Flow가 아닌 LiveData를 써도 별 문제는 없어 보인다. 그렇다면 뭐 때문에 안드로이드 개발자들은 Flow와 LiveData를 비교하는 것인가?
https://developer.android.com/topic/libraries/architecture/livedata#livedata-in-architecture
LiveData는 생명주기를 인식해서 액티비티, 프래그먼트 등 항목의 생명주기를 따른다. LiveData를 써서 이런 생명주기 소유자와 ViewModel 객체 등 수명이 다른 객체 간에 통신할 수 있다. ViewModel은 기본적으로 UI 관련 데이터를 로드, 관리하는 역할을 하므로 LiveData 객체를 보유하는 데 적합하다. LiveData 객체를 ViewModel에 만들고 이를 사용해 UI 레이어에 상태를 노출한다. 액티비티, 프래그먼트는 상태 보유가 아닌 데이터를 표시하는 역할을 하므로 LiveData 인스턴스를 보유해선 안 된다. 액티비티와 프래그먼트가 데이터를 갖지 않도록 하면 단위 테스트 작성도 쉬워진다.
데이터 영역 클래스에서 LiveData 객체를 작업하고 싶을 수 있지만 LiveData는 비동기 데이터 스트림을 처리하도록 설계되지 않았다. LiveData 변환, MediatorLiveData를 써서 LiveData 객체 작업을 할 수는 있지만 이 접근 방식에는 단점이 있다. 즉 데이터 스트림 결합 기능이 매우 제한적이고 변환을 통해 만들어진 객체를 포함해 모든 LiveData 객체가 기본 쓰레드(=메인 쓰레드)에서 관찰된다...(중략)...앱의 다른 레이어에서 데이터 스트림을 써야 한다면 Flow를 사용한 다음 asLiveData()를 써서 ViewModel의 LiveData로 변환하는 게 좋다. 자바로 빌드된 코드베이스의 경우 콜백이나 RxJava와 함께 Executor를 쓰는 게 좋다
안드로이드 디벨로퍼에선 "데이터 스트림을 써야 한다면" 이라는 조건을 붙이면서 Flow를 사용한 다음 asLiveData()로 LiveData를 변환하는 것을 권장하고 있다. 결국 Flow를 쓰라는 뜻이다.
그리고 LiveData의 한계점이 명시돼 있다. 모든 LiveData 객체의 관찰은 메인 쓰레드에서 이뤄진다는 것이다. 뷰모델을 통해 뷰를 업데이트하는 경우 그다지 문제가 안 될 수도 있지만 Data Layer에서 사용한다면 문제가 된다. 데이터를 받아오는 곳이 서버 또는 DB라면 무거운 작업이 되기 때문에 메인 쓰레드가 아닌 별도의 워커 쓰레드에서 처리해야 ANR 등을 피할 수 있기 때문이다.
또한 클린 아키텍처 관점에서도 LiveData의 단점이 드러난다. 이에 대해 아래 링크에서 잘 설명하고 있다.
https://readystory.tistory.com/207
Domain Layer
앱의 비즈니스 로직에서 필요한 UseCase, Model을 포함하고 있다. UseCase는 각 개별 기능 또는 비즈니스 로직 단위고 Presentation, Data Layer에 대한 의존성을 갖지 않고 독립적으로 분리돼 있다. Domain Layer는 안드로이드에 의존성을 가지지 않은 순수 자바 / 코틀린 코드로만 구성한다. 여기엔 Repository 인터페이스도 포함돼 있다. LiveData의 문제는 여기서도 발생한다. 우리는 너무도 자연스럽게 Domain Layer에서 LiveData를 쓰곤 하는데 만약 계층별로 멀티 모듈로 프로젝트를 구성하고 있다면 LiveData만을 위해 안드로이드 의존성을 갖도록 해야 할 수도 있게 된다. 그리고 LiveData는 안드로이드 SDK에 포함돼 있어서 단위 테스트 시에도 별도의 테스트 지원 모듈을 의존해야 한다. 요약하면 클린 아키텍처 관점에서 LiveData의 문제점은 아래와 같다.
- LiveData는 UI에 밀접하게 연관돼 있기 때문에 Data Layer에서 비동기 방식으로 데이터를 처리하기에 자연스러운 방법이 없다
- LiveData는 안드로이드 플랫폼에 속해 있기 때문에 순수 자바 / 코틀린을 써야 하는 Domain Layer에서 쓰기에 부적합하다
안드로이드 개발 언어로 코틀린이 자리 잡기 전까진 위와 같은 문제점을 안고 있어도 안드로이드 진영에선 별다른 선택권이 없었다. 그러나 코틀린 코루틴이 발전하면서 Flow가 등장했고 많은 안드로이드 커뮤니티에선 이 Flow를 이용해 LiveData를 대체할 수 있을지에 대한 기대가 생겼다. 그러나 Flow를 통해 LiveData를 대체하는 건 쉬운 일이 아니다.
- Flow는 스스로 안드로이드 생명주기를 알지 못한다. 그래서 생명주기에 따른 중지, 재개가 어렵다
- Flow는 상태가 없어 값이 할당됐는지, 현재 값은 무엇인지 알기가 어렵다
- Flow는 콜드 스트림 방식이다. 연속해서 계속 들어오는 데이터를 처리할 수 없고 collect 되었을 때만 생성되고 값을 반환한다. 만약 하나의 Flow Builder에 대해 다수의 collector가 있다면 collector 하나마다 하나씩 데이터를 호출하기 때문에 비용이 비싼 DB 접근, 서버 통신 등을 수행한다면 여러 번 리소스 요청을 하게 될 수 있다
이를 위해 코틀린 1.41에 Stable API로 등장한 것이 StateFlow, SharedFlow다
https://yoon-dailylife.tistory.com/71
LiveData는 일반적으로 데이터 전송을 위한 용도로 쓰인다. Flow 또한 고유한 방식으로 데이터를 전송하고 비동기 작업을 수행하는 기능이 있다. 둘 다 한계와 장점이 존재한다.
LiveData는 생명주기 문제를 처리하기 위해 번거롭지 않게 데이터를 관찰하는 데 사용된다. Flow는 지속적인 데이터 통합에 사용되며 비동기 프로그래밍에도 단순화되었다. Room을 사용하는 경우 LiveData를 써서 DB에서 UI로 데이터를 보내 기존 문제를 해결할 수 있다. 그러나 향후 DB에 변경사항이 생길 때 LiveData는 무력해진다...(중략)...예를 들어 Repository에서 Flow의 flowOn()을 써서 쉽게 쓰레드를 변경할 수 있다. 그리고 collector가 emitter보다 느린 경우 방출된 값을 건너뛰는 conflate()를 호출해 처리할 수 있다...(중략)
LiveData와 Flow는 서로를 보완하는 역할을 한다. LiveData는 구성 변경 시에 안정성을 제공하고 최신 데이터를 뷰로 전달하는 역할을 한다. Flow는 UseCase, Repository, DataSources Layer와 긴밀하게 작동해 데이터를 수집 및 처리해서 서로 다른 코루틴 범위에서 작업을 실행한다. 그래서 ViewModel, View 사이의 상호작용은 LiveData, 더 깊은 레이어와 쓰레딩 같은 더 복잡한 처리는 Flow가 처리한다.
지금까지 내용을 정리해본다.
- LiveData는 비동기 데이터 스트림을 처리할 수 있게 설계되지 않았고, 메인 쓰레드에서만 관찰이 이뤄진다는 단점이 있다
- 클린 아키텍처 관점에서 보면 LiveData는 안드로이드 구성요소기 때문에 순수한 자바, 코틀린 코드만을 요구하는 도메인 계층에서 사용하기 부적합하다
- 이러한 LiveData의 단점을 해소하기 위해 Flow를 사용할 수 있다
- 그러나 Flow는 스스로 생명주기를 알지 못하고 값을 갖고 있는지 여부를 알 수 없으며 연속해서 데이터가 들어올 경우 처리할 수 없다
그래서 Room 또는 레트로핏을 사용할 경우 코루틴의 suspend 키워드로 비동기 인터페이스를 만든 뒤 Repository에서 Flow를 받아온 다음, ViewModel에서 Flow에 asLiveData()를 붙여 LiveData로 변환한 다음 View에서 관찰하는 구조를 채용할 수 있다. 그림으로 그린다면 아래와 같겠다.
그리고 StateFlow, SharedFlow라는 새로운 API가 등장하면서 국면이 바뀌었다.
https://developer.android.com/kotlin/flow/stateflow-and-sharedflow
StateFlow는 현재 상태와 새로운 상태 업데이트를 수집기에 내보내는 관찰 가능한 상태 홀더 흐름이다. value 속성을 통해서도 현재 상태값을 읽을 수 있다. 상태를 업데이트하고 Flow에 보내려면 MutableStateFlow 클래스의 value 속성에 새 값을 할당한다. 안드로이드에서 StateFlow는 관찰 가능한 변경 가능 상태를 유지해야 하는 클래스에 아주 적합하다. 코틀린 Flow의 예시를 따라 View가 UI 상태 업데이트를 listen하고 구성 변경에도 기본적으로 화면 상태가 지속되도록 LatestNewsViewModel에서 StateFlow를 노출할 수 있다
class LatestNewsViewModel(
private val newsRepository: NewsRepository
) : ViewModel() {
// Backing property to avoid state updates from other classes
private val _uiState = MutableStateFlow(LatestNewsUiState.Success(emptyList()))
// The UI collects from this StateFlow to get its state updates
val uiState: StateFlow<LatestNewsUiState> = _uiState
init {
viewModelScope.launch {
newsRepository.favoriteLatestNews
// Update View with the latest favorite news
// Writes to the value property of MutableStateFlow,
// adding a new element to the flow and updating all
// of its collectors
.collect { favoriteNews ->
_uiState.value = LatestNewsUiState.Success(favoriteNews)
}
}
}
}
// Represents different states for the LatestNews screen
sealed class LatestNewsUiState {
data class Success(val news: List<ArticleHeadline>): LatestNewsUiState()
data class Error(val exception: Throwable): LatestNewsUiState()
}
MutableStateFlow 업데이트를 담당하는 클래스가 생산자고, StateFlow에서 수집되는 모든 클래스가 소비자다. Flow Builder를 써서 빌드된 Cold Stream과 달리 StateFlow는 Hot Stream이다. Stream에서 수집해도 생산자 코드가 트리거되지 않는다. StateFlow는 항상 활성 상태고 메모리 안에 있으며 가비지 컬렉션 루트에서 참조가 없는 경우에만 가비지 컬렉션에 사용할 수 있다. 새 소비자가 Flow에서 수집을 시작하면 Stream의 마지막 상태와 후속 상태가 수신된다. LiveData 같이 관찰 가능한 다른 클래스에서 이 동작을 찾을 수 있다. View는 다른 Flow와 마찬가지로 StateFlow를 listen한다
class LatestNewsActivity : AppCompatActivity() {
private val latestNewsViewModel = // getViewModel()
override fun onCreate(savedInstanceState: Bundle?) {
...
// Start a coroutine in the lifecycle scope
lifecycleScope.launch {
// repeatOnLifecycle launches the block in a new coroutine every time the
// lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
repeatOnLifecycle(Lifecycle.State.STARTED) {
// Trigger the flow and start listening for values.
// Note that this happens when lifecycle is STARTED and stops
// collecting when the lifecycle is STOPPED
latestNewsViewModel.uiState.collect { uiState ->
// New value received
when (uiState) {
is LatestNewsUiState.Success -> showFavoriteNews(uiState.news)
is LatestNewsUiState.Error -> showError(uiState.exception)
}
}
}
}
}
}
StateFlow, LiveData는 비슷한 점이 있다. 둘 다 관찰 가능한 데이터 홀더 클래스이며 앱 아키텍처에 사용될 때 비슷한 패턴을 따른다. 그러나 StateFlow, LiveData는 아래와 같이 다르게 작동한다
- StateFlow의 경우 초기 상태를 생성자에 전달해야 하지만 LiveData는 전달하지 않는다
- View가 STOPPED 상태가 되면 LiveData.observe()는 소비자를 자동으로 등록 취소하는 반면 StateFlow는 다른 Flow에서 수집하는 경우 자동으로 수집을 중지하지 않는다. 동일한 동작을 실행하려면 LifeCycle.repeatOnLifecycle {}에서 Flow를 수집해야 한다
StateFlow와 LiveData는 MVVM 패턴에서 서로 대체 가능한 존재다. 그리고 안드로이드 스튜디오 Arctic Fox 버전부터는 데이터 바인딩에 StateFlow가 호환되도록 패치되었다.
여기까지의 내용을 정리하면 아래와 같다.
- StateFlow는 순수하게 코틀린으로 만들어진 API기 때문에 도메인 계층(UseCase, Repository 등)에서 사용해도 클린 아키텍처 관점에서 벗어나지 않는다
- 쓰레드보다 가벼운 코루틴을 써서 비용이 비싼 서버 호출 + 데이터 스트림 처리가 가능하기 때문에 LiveData를 사용하는 것에 비해 향상된 성능으로 사용할 수 있다
- StateFlow는 SharedFlow 인터페이스를 구현하고, SharedFlow는 Flow 인터페이스를 구현하기 때문에 filter, map 등을 통한 데이터 조작이 가능하다
StateFlow를 쓸 때는 잘 몰랐는데 이것저것 찾아서 공부하고 나니 아주 매력적인 코틀린 라이브러리라는 걸 새삼스레 알게 됐다.
StateFlow와 데이터 바인딩을 사용하는 예제는 아래의 링크를 참고하면 되겠다. 깃허브 예제 링크도 제공하고 있다.
https://readystory.tistory.com/208
'Android' 카테고리의 다른 글
[Android] Jetpack Navigation, Room DB, Flow 같이 사용하기 - 3 - (0) | 2022.12.13 |
---|---|
[Android] Jetpack Navigation, Room DB, Flow 같이 사용하기 - 2 - (0) | 2022.12.13 |
[Android] Jetpack Navigation, Room DB, Flow 같이 사용하기 - 1 - (0) | 2022.12.12 |
[Android] Navigation 사용 시 FragmentDirections가 자동 생성되지 않을 때 (0) | 2022.12.09 |
[Android] MVP vs MVVM (0) | 2022.12.05 |