Development/Android

Android LiveData에서 Flow로 옮겨가야하는 이유

Jamie 2026. 3. 31. 10:01
반응형

안녕하세요. Jamie입니다.

 

Android에서 Flow 많이 사용하시죠? LiveData 많이 사용하신다구요?

 

그래도 현재 포스팅기준 (2026년) ‘아직’은 사용가능한데, 결국 Flow로 옮겨오셔야할겁니다.

 

오늘 포스팅은 그래야하는 이유에 대해서 소개합니다.

 

자 일단 여러가지 이유가 있는데, 각각의 도메인과 기술스택의 선점때문에 이유는 다르실것 같습니다.

 

왜 LiveData를 안쓰는가? 혹은 못쓰는가?

현재 2026년 기준 가장 큰 이유는 KMP (Kotlin Multiplatform)가 대세로 떠오르면서 그렇습니다.

 

이제 도메인이나 데이터 영역에 안드로이드에 종속되어있는 라이브러이인 Live Data 를 쓰는건 설계상 결합도를 높이는 행위로 간주합니다.

 

KMP 자체가 안드로이드에서만을 목적으로 나온 플랫폼이 아니기 때문이죠?

또한 LiveData 는 Backpressure 처리가 안되고, 복잡한 비즈니스 로직을 처리하기엔 연산자가 너무 부족합니다.

 

 

플랫폼 의존성 Android 종속적 (androidx.lifecycle) Pure Kotlin (KMP 호환 가능)
도메인/데이터 레이어 사용 권장 안 함 (Android 의존성 때문) 사용 권장 (비즈니스 로직에 최적)
연산자(Operators) 매우 제한적 (map, switchMap 등) 방대하고 강력함 (filter, combine, flatMap 등)
스레딩 제어 기본적으로 Main Thread 중심 Dispatcher를 통한 세밀한 제어 가능
상태 관리 초기값 불필요, null 허용 StateFlow는 초기값 필수, null 안전성 강화

 

Backpressure(배압)이란?

보통 배압은 데이터 생산자(Producer)가 데이터를 발행하는 속도가 소비자(Consumer)가 처리하는 속도보다 훨씬 빠를 때 발생하는 과부하 현상을 말합니다.

 

예를 들어서 생산자가 소비자에서 바로바로 보여주기 힘들만한 많은 데이터를 빠르게 보내고 있을때를 가정해보겠습니다.

  • RxJava/Flow의 경우에는 자체적으로 데이터를 버퍼에 쌓고, 발생 속도를 낮추는 전략이 있습니다.
  • LiveData의 방식은 “최신 데이터만 보여주고, 나머지는 버린다” 는 전략으로 해결합니다.
    짧은 시간에 1,2,3,4,5를 보내면 마지막 5만 전달 받습니다.
    LiveData는 애초에 데이터의 흐름(Stream) 전체를 보존하기보단 현재 상태(state)를 UI에 보여주는데 최적화되어있기 때문에 중간데이터가 누락되는것이 자연스러운 설계입니다.

 

예제 코드를 한번 보겠습니다.

import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

class MainViewModel : ViewModel() {
    val countLiveData = MutableLiveData<Int>()

    fun simulateFastUpdates() {
        // 루프를 돌며 매우 빠르게 값을 변경
        for (i in 1..100) {
            // postValue는 백그라운드 스레드에서 Main 스레드로 값을 전달함
            countLiveData.postValue(i) 
        }
    }
}

 

 

결과는 다음과 같습니다. 여러번 재 실행시켜도 똑같네요.

---------------------------- PROCESS ENDED (28458) for package com.cplaygr.sample.livedatawithflow ----------------------------
---------------------------- PROCESS STARTED (29050) for package com.cplaygr.sample.livedatawithflow ----------------------------
2026-03-29 23:09:36.454 29050-29050 MainActivity             D  observe: 100
---------------------------- PROCESS ENDED (29050) for package com.cplaygr.sample.livedatawithflow ----------------------------
---------------------------- PROCESS STARTED (29670) for package com.cplaygr.sample.livedatawithflow ----------------------------
2026-03-29 23:10:22.829 29670-29670 MainActivity             D  observe: 100
---------------------------- PROCESS ENDED (29670) for package com.cplaygr.sample.livedatawithflow ----------------------------
---------------------------- PROCESS STARTED (29750) for package com.cplaygr.sample.livedatawithflow ----------------------------
2026-03-29 23:10:25.455 29750-29750 MainActivity             D  observe: 100
---------------------------- PROCESS ENDED (29750) for package com.cplaygr.sample.livedatawithflow ----------------------------
---------------------------- PROCESS STARTED (29831) for package com.cplaygr.sample.livedatawithflow ----------------------------
2026-03-29 23:10:29.939 29831-29831 MainActivity             D  observe: 100

 

 

왜 이렇게 동작하는지 한번 알아보겠습니다. LiveData의 AOSP를 찾아서 가져와 보겠습니다.

protected void postValue(T value) {
    boolean postTask;
    synchronized (mDataLock) {
        postTask = mPendingData == NOT_SET;
        mPendingData = value;
    }
    if (!postTask) {
        return;
    }
    ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
}

private final Runnable mPostValueRunnable = new Runnable() {
    @Override
    public void run() {
        Object newValue;
        synchronized (mDataLock) {
            newValue = mPendingData;
            mPendingData = NOT_SET;
        }
        //noinspection unchecked
        setValue((T) newValue);
    }
};

 

 

조금 더 자세히 볼수 있지만 우선 개념만 파악하고 가겠습니다.

postValue를 실행시키는 순간 ArchTaskExecutor를 통해 postToMainThread로 Runnable을 전달하고 있습니다.

 

mPostValueRunnable은 우리가 일반적으로 알고 있는 Runnable이에요. newValue가 들어오면 setValue를 해버립니다.

 

즉, Main Thread에 작업을 예약해놓고 기다리기 때문에 1부터 100까지의 루프 도는 속도가 Main 쓰레드가 값을 처리하는

속도보다 훨씬 빠르기 때문에 LiveData는 가장 마지막에 들어온 값으로 덮어쓰기를 실행합니다.

 

결국 UI는 가장 최신 상태만 반영하게 됩니다.

 

 

자 이제 Backpressure를 해결한 Flow의 코드를 한번 살펴보도록 하겠습니다.

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch

class FlowVieModel : ViewModel() {

    // 1. 모든 이벤트를 놓치지 않기 위해 SharedFlow 선언
    // extraBufferCapacity를 통해 버퍼 공간을 확보하여 backpressure 대응
    private val _countFlow = MutableSharedFlow<Int>(extraBufferCapacity = 100)
    val countFlow = _countFlow.asSharedFlow()

    init {
        simulateFastUpdates()
    }

    fun simulateFastUpdates() {
        viewModelScope.launch {
            for (i in 1..100) {
                // emit은 suspend 함수로, 버퍼가 꽉 차면 자리가 생길 때까지 대기하거나
                // 설정된 정책에 따라 모든 값을 순차적으로 전달합니다.
                _countFlow.emit(i)
            }
        }
    }
}

 

 

자 그리고 주의할점은 Activity/Fragment에 Flow를 붙일때는 LiveData처럼 그냥 바로 observe 하면 안됩니다.

Flow에 붙일때는 아래와 같이 Coroutine을 이용해야합니다.

 

lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        flowViewModel.countFlow.collect { count ->
            // 여기서 1부터 100까지 유실 없이 수집됩니다.
            Log.d(TAG, "Received: $count")
        }
    }
}

 

 

결과를 한번 보겠습니다.

---------------------------- PROCESS ENDED (29831) for package com.cplaygr.sample.livedatawithflow ----------------------------
2026-03-29 23:27:11.642   606-606   MainActivity             D  Received: 1
2026-03-29 23:27:11.642   606-606   MainActivity             D  Received: 2
2026-03-29 23:27:11.642   606-606   MainActivity             D  Received: 3
2026-03-29 23:27:11.642   606-606   MainActivity             D  Received: 4
2026-03-29 23:27:11.642   606-606   MainActivity             D  Received: 5
2026-03-29 23:27:11.642   606-606   MainActivity             D  Received: 6
2026-03-29 23:27:11.642   606-606   MainActivity             D  Received: 7
2026-03-29 23:27:11.642   606-606   MainActivity             D  Received: 8
2026-03-29 23:27:11.642   606-606   MainActivity             D  Received: 9
2026-03-29 23:27:11.642   606-606   MainActivity             D  Received: 10
.................... // 로그가 많아서 생략 // .........................
2026-03-29 23:27:11.642   606-606   MainActivity             D  Received: 11
2026-03-29 23:27:11.643   606-606   MainActivity             D  Received: 92
2026-03-29 23:27:11.643   606-606   MainActivity             D  Received: 93
2026-03-29 23:27:11.643   606-606   MainActivity             D  Received: 94
2026-03-29 23:27:11.643   606-606   MainActivity             D  Received: 95
2026-03-29 23:27:11.643   606-606   MainActivity             D  Received: 96
2026-03-29 23:27:11.643   606-606   MainActivity             D  Received: 97
2026-03-29 23:27:11.643   606-606   MainActivity             D  Received: 98
2026-03-29 23:27:11.643   606-606   MainActivity             D  Received: 99
2026-03-29 23:27:11.643   606-606   MainActivity             D  Received: 100

 

 

정말 만약에 Flow를 LiveData처럼 쓰려면 (Backpressure를 포기하고 UI에 보여주기만을 원한다면)

  • 다만 데이터가 스트림으로 올 경우 데이터는 유실 됩니다.
Kotlin
// ViewModel
val countLiveData = _countFlow.asLiveData() // Flow를 LiveData로 변환

// Activity/Fragment
viewModel.countLiveData.observe(this) { value ->
    Log.d(TAG, "observe: $value")
}

 

 

 

출처

반응형