Uing? Uing!!

[내 맘대로 정리한 안드로이드] LiveData의 데이터 누락: Observer는 '모든' 이벤트를 100% 받아올 수 있을까? 본문

Android

[내 맘대로 정리한 안드로이드] LiveData의 데이터 누락: Observer는 '모든' 이벤트를 100% 받아올 수 있을까?

Uing!! 2021. 7. 16. 00:57
반응형

MVVM 구조를 사용한다면 흔히 뷰모델에서 LiveData를 사용하여 데이터나 이벤트를 변경하고,

액티비티 등의 onCreate 부에서 이 LiveData에 Observer를 달아 데이터가 변경되었을 때 화면에 반영하곤 한다.

그렇다면 이렇게 설계했을 때에는 항상,

Q. LiveData로 들어가는 모든 값들이 observe될 수 있을까?

결론만 말하자면, NO이다.

두 가지 이유가 있는데 하나는 1) observer의 상태, 다른 하나는 2) postValue의 동작 방식과 관련이 있다.

 

1) Observer가 Active하지 않은 경우

기본적으로 LiveData는 액티비티, 프래그먼트 등의 수명주기와 긴밀하게 연결되어 작동한다.

특히 안드로이드 Developers의 LiveData 개요를 보면 이런 설명이 있다.

Observer 클래스로 표현되는 관찰자의 수명 주기가 STARTED 또는 RESUMED 상태이면 LiveData는 관찰자를 활성 상태로 간주합니다. LiveData는 활성 관찰자에게만 업데이트 정보를 알립니다. LiveData 객체를 보기 위해 등록된 비활성 관찰자는 변경사항에 관한 알림을 받지 않습니다.

'활성 상태'가 아닌 observer는 LiveData의 값 변경을 인식할 수 없다는 것이다.

그리고 이런 설명도 덧붙어 있다.

대부분의 경우 앱 구성요소의 onCreate() 메서드는 LiveData 객체 관찰을 시작하기 적합한 장소이며 그 이유는 다음과 같습니다.
* 시스템이 활동이나 프래그먼트의 onResume() 메서드에서 중복 호출을 하지 않도록 하기 위해서입니다.
* 활동이나 프래그먼트에 활성 상태가 되는 즉시 표시할 수 있는 데이터가 포함되도록 하기 위함입니다. 앱 구성요소는 STARTED 상태가 되는 즉시 관찰하고 있던 LiveData 객체에서 최신 값을 수신합니다. 이는 관찰할 LiveData 객체가 설정된 경우에만 발생합니다.

이 말인 즉슨, observer가 활성 상태(위 경우에는 STARTED)가 되는 순간, '마지막으로 변경된 값'만을 라이브데이터에서 수신한다는 뜻이 되겠다.

이를 확인하기 위해 액티비티의 onCreate 위치에서, LiveData에 0부터 4까지의 Int 값을 연달아 집어넣어 보았다.

class MainActivity : AppCompatActivity() {
    private val vm: MainViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        vm.intLiveData.observe(this, {
            prinln(it)
        })
        vm.startUpdatingInt() 
    }
}
class MainViewModel : ViewModel(){
    val intLiveData = MutableLiveData<Int>()

    fun startUpdatingInt() {
        for (i in 0 until 5) intLiveData.value = i
    }
}

결과는, '4'라는 하나의 값만이 콘솔에 출력된다.

4

이유는 간단하다. 메소드가 마지막 숫자인 4를 셀 때까지 아직 STARTED(활성) 상태에 도달하지 못했기 때문이다.

그리고 나서 화면이 STARTED상태에 돌입한 후, '가장 마지막으로 변경된 값'인 4를 받아 출력한 것이다.

 

그렇다면 이번에는 startUpdatingInt 메소드를 onResume 이후에 동작하도록 위치를 변경해 보자.

class MainActivity : AppCompatActivity() {
    private val vm: MainViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        vm.intLiveData.observe(this, {
            println(it)
        })
    }
    
    override fun onResume() {
        vm.startUpdatingInt() 
    }
}

이 경우에는 observer가 항상 ACTIVE한 상태이므로,

기대한 것과 같이 모든 값이 출력되는 것을 볼 수 있었다.

0
1
2
3
4

비슷하게 사용자가 버튼을 누를 때 숫자를 세도록 설정하는 경우에도, 

직접 버튼을 눌렀다는 것은 화면이 활성화되어 있는 상태라는 의미이므로 모든 숫자를 observer할 것이다.

 

따라서 모종의 이유로 observer가 여러 값을 연속적으로 받아서 동작해야만 하는 경우에는 우선 onCreate에서 observer등록을 해 준 후에, 

a) 값 변경이 onStart 이후에 이루어지도록 명시하거나

b) 화면이 활성 상태일 때 사용자의 입력을 통해 변경되도록 하는 것이 좋겠다.

 

2) postValue에서의 데이터 누락

그렇다면... observer가 활성 상태인 경우라면 라이브데이터의 '모든'값이 관찰될까?

위에서 이미 두 가지의 이유가 있다고 언급했었으니 아마도 그건 아닐 것이다.

 

1)에서 우리는 숫자를 세는 과정을 onResume()으로 이동시킴으로써 모든 값을 받을 수 있었다.

이때 우리가 사용한 방식은 postValue가 아닌, 라이브데이터에 직접 value값을 설정하는 setValue 방식이었다.

그렇다면 이걸 postValue 방식으로 또 한번 바꿔 보자.

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

    fun startUpdatingInt() {
        for (i in 0 until 5) intLiveData.postValue(i)
    }
}

결과는, 안타깝게도 다시 마지막 값 '4'만이 출력된다.

4

 

라이브데이터의 값이 누락될만한 두 번째 요인은 바로 이 postValue를 사용하는 경우에 있다.

왜 이런 현상이 발생하는지는 postValue 메소드를 들여다보면 확인할 수 있다.

 

setValue와 postValue

postValue를 뜯어보기에 앞서, 라이브데이터의 값을 바꾸는 두 가지 방법인 setValue와 postValue를 비교해 보자.

val intLiveData = MutableLiveData<Int>()

fun setIntValue(i: Int) {
    intLiveData.value = i // set: 메인스레드에서 동기적으로 동작
}

fun postIntValue(i: Int) {
    intLiveData.postValue(i) // post: 백그라운드 스레드에서 비동기적으로 동작
}

두 방식의 가장 큰 차이점은 스레드이다.

postValue로 값을 설정할 경우 백그라운드에서 값이 변경되고 동기적으로 observe되지만,

setValue의 경우에는 UI 스레드에서 직접 값을 변경한다.

 

이에 따른 결과론적인 차이점이라면,

"postValue는 순서보장이 되지 않지만, 백그라운드에서 작업이 가능하다.

반명 setValue는 순서가 보장되지만, UI스레드에서 동작하므로 다른 스레드 내에서는 사용할 수 없다"

정도라고 말할 수 있겠다.

 

이렇게만 본다면 이 글의 제목인 '데이터 누락'과는 상관이 없어 보인다.

하지만 실제 LiveData 클래스 내부의 postValue와 setValue 메소드 구현부를 살펴보면 이야기가 달라진다.

 

아래는 LiveData 클래스에 정의된 postValue와 setValue 메소드이다.

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

@MainThread
protected void setValue(T value) {
    assertMainThread("setValue");
    mVersion++;
    mData = value;
    dispatchingValue(null);
}

두 메소드를 보면, setValue()의 경우에는 단순히 mData를 주어진 값으로 변경하는 작업을 수행하지만,

postValue()의 경우 synchronized가 걸려 있어 여러 작업이 동시에 수행되지 못한다.

멀티스레드 작업에서 동작 순서가 보장되지 않는 만큼, 값이 동시에 변하는 것을 막기 위함으로 보인다.

 

따라서, 기본적으로 setValue()는 데이터를 누락하지 않지만,

postValue는 아주 빠른 속도로 연속해서 데이터가 들어오면 값이 누락된다.

 

그렇다면 아주 빠른 속도가 아니라 텀을 두고 데이터가 들어온다면?

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

    fun startUpdatingInt() {
    	thread { // Thread.sleep이 UI 스레드에서 실행되는 것을 막기 위해 threading
            for (i in 0 until 5) { 
            	Thread.sleep(1000) // 1초 간격으로 postValue
            	intLiveData.postValue(i)
            }
        }
    }
}

이번에는 각 변경 간에 충분한 텀이 있었기 때문에, 모든 값이 잘 출력된다.

0
1
2
3
4

결론

요약하자면,

- observer가 ACTIVE하지 않은 상태에서 들어온 값들은 가장 마지막 값만이 observer에 전달되고,

- postValue()를 사용한 업데이트는 너무 빈번하게 호출되면 누락될 수 있다.

 

결국 LiveData가 '항상' '모든' 변경의 감지를 보장해주는 것은 아니므로,

원치 않는 데이터 누락이 발생하지 않도록 잘 설계할 필요가 있겠다.

 

반응형
Comments