[안드로이드 공식문서 파헤치기] LiveData의 모든 것! - 1편(Observer Pattern, MutableLiveData)

dada·2022년 8월 18일
3
post-thumbnail

참고 자료
Android Developer 도큐먼트 - LiveData
Android Developer 도큐먼트 - MutableLiveData
디자인패턴, Observer Pattern, 옵저버 패턴

✅공부배경

  • Flow에 대해 공부하던 중 Flow와 LiveData를 비교하는 글들을 보게 되었습니다! Flow의 등장배경과 장단점에 대해 더 잘 이해하기 위해선 LiveData에 대해 지금보다 깊게 공부하고 완전히 제것으로 체화해야함을 느꼈습니다. 그래서 LiveData의 공식문서, 미디엄 블로그, 공식 유튜브를 통해 LiveData를 공부해보겠습니다~!

✅Observer Pattern

  • LiveData를 공부하기에 앞서 옵져버 패턴(관찰자 패턴)에 대한 이해가 선행되어야 합니다. LiveData공식문서를 보면, "LiveData는 관찰자 패턴을 따릅니다. LiveData는 기본 데이터가 변경될 때 Observer 객체에 알립니다."라고 되어있기 때문입니다!

  • 옵져버 패턴은 디자인 패턴 중 하나로, 감시자(관찰자)들이 한곳을 계속 바라보고 있는 패턴입니다. 어떤 이벤트가 일어나면 이벤트를 관찰하던 관찰자들이 바로 반응하는 패턴이 옵져버 패턴입니다

  • 이런 옵져버 패턴을 가지지 않는다면 이벤트를 체크해야하는 오브젝트들은 이런 이벤트가 발생했는지 1초,1분 한시간,, 마다 이벤트 발생 여부를 확인해야 합니다. 이런 방법을 polling 이라고 하는데 필요없는 리소스 낭비가 발생하고 이벤트 발생 감지를 놓치는 경우도 발생할 수 있습니다. 하지만 옵져버 패턴은 이벤트가 일어난 순간 옵져버가 반응하니 바로 이벤트를 감지할 수 있습니다.

  • 옵져버 패턴에는 총 3개의 중요한 구성요소가 있습니다

    • 이벤트를 감지할 Observer 인터페이스: 관찰자 역할을 하는 인터페이스는 메서드로 어떤 이벤트가 동작하는 함수인 update()를 가지고 있습니다

    • Observer인터페이스를 구현하는 실제 Observer class: update() 메서드를 가지기 때문에 이벤트가 일어났을때 각 class별로 특정 동작을 정의할 수 있습니다

    • Observer 인터페이스 구현체를 리스트로 가지고 있는 Class: 옵져버 인스턴스를 리스트에 추가해주기 위한 register(), 이벤트가 일어났을때 소유하고 있는 옵져버들에게 update를 알리는 notifyUpdate() 함수를 가지고 있습니다

✅Observer Pattern 예시

  • 간단한 예시를 통해 옵져버 패턴을 이해해봅시다!

그림출처) 디자인패턴, Observer Pattern, 옵저버 패턴

  • 강아지와 고양이가 Observer(관찰자)가 되고 이러한 옵져버들을 리스트로 소유한 Owner(주인)가 있다고 해봅시다! 주인이 집에 돌아오거나 집합 명령을 내리면(Event 발생) 동물(옵져버,관찰자)들이 반응할 것이고, 고양이는 야옹 강아지는 멍멍이라고 반응(update())하게 하고 싶다고 해봅시다.
interface Observer {
    fun update()
}
  • 가장 먼저 Observer 인터페이스를 정의해야 합니다. 관찰자(감시자) 역할을 하는 Observer인터페이스는 내부 함수로 이벤트가 일어났을 때 동작할 update() 메서드를 가지고 있습니다
//관찰자 1
class Dog: Observer {
    override fun update() {
        println("멍멍")
    }
}

//관찰자 2
class Cat: Observer {
    override fun update() {
        println("야옹")
    }
}
  • Observer 인터페이스를 상속받은 고양이, 강아지는 각각 update()함수에서 야옹, 멍멍을 정의합니다(Event 발생시 서로 다른 동작 처리)
class Owner {
    val observerList = mutableListOf<Observer>()

    fun register(observer:Observer){
        observerList.add(observer)
    }

   fun notifyUpdate(){
       for(observe in observerList){
           observe.update()
       }
   }
}
  • 동물의 주인인 Owner는 내부에 ObserverList를 가지고 있고 ObserverListObserver를 추가하는 register()를 가지고 있습니다. 또한 가지고 있는 Observer들에게 update()를 알리기 위한 notifyUpdate() 함수를 정의하고 있습니다

fun main(){
    val owner=Owner()
    val dog=Dog()
    val cat=Cat()

    owner.register(dog) //옵져버 등록
    owner.register(cat) //옵져버 등록

    owner.notifyUpdate() //오너가 신호를 보냄 = 이벤트 발생
}
  • 이제 Owner, Dog, Cat 인스턴스들을 하나씩 만들고 Owner에게 register()로 Dog,Cat 관찰자를 등록합니다. 이제 Owner가 신호를 보내면(notifyUpdate()를 호출하면) Owner에게 등록되어있는 Dog, Cat들이 반응(update()) 합니다

  • 실제로는 register, notifyUpdate에 여러 인자를 추가하기도 하고 notifyUpdate, update()를 여러개 정의하기도 하고 옵져버를 바라보고 있는 또 다른 옵져버를 등록할 수도 있습니다. 실제 LiveData를 옵져버 패턴에서 바라보면 매우 복잡하지만, 핵심은 Observer 패턴의 핵심 기능인 Observer들을 등록하고 notifyUpdate()함수를 통한 옵져버들을 update()한다는 것이 핵심입니다!

✅LiveData 이해를 위한 사전 개념

  • 옵져버패턴에 대해 알아보았으니 LiveData가 어떤 방식으로 옵져버 패턴을 구현하고 있는지 알아보려고 하는데요. 그 전에 LiveData를 이해하기 위한 사전 개념을 정리하고 가면 이해에 더 도움이 될 것 같습니다

  • LiveData: UI Controller인 Activity, Fragment는 Owner의 이벤트를 받음(유저의 클릭 이벤트 같은 것)->이런 Owner의 이벤트를 관찰하고 바로 업데이트할 수 있는 데이터 홀더 클래스

  • Observer: 관찰자(고양이, 강아지)이고 업데이트 알림을 받는 onChange() 메서드가 정의되어 있음

  • observe(..) 함수: LiveData 클래스에 정의된 함수. LiveData에 Observer 인스턴스를 연결해서 Observer가 라이브데이터를 관찰하도록 함

  • 여기서 Observer인 강아지, 고양이는 UI Controller인 Activity, Fragment(Owner의 이벤트를 감지)의 수명주기가 resume, start상태일때만 명령을 처리할 수 있다고 가정해봅시다!!!!

✅사용법과 비유

class NameActivity : AppCompatActivity() {

    //LiveData를 보유한 ViewModel을 만든다
    private val model: NameViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        //Observer 인터페이스를 구현한 관찰자를 만든다(강아지)
        val nameObserver = Observer<String> { newName ->
            // 강아지의 onChange()가 호출되었을때 할일인 UI 업데이트 로직 작성
            nameTextView.text = newName
        }

        // LiveData의 observe 메서드로 LifecycleOwner, Observer 객체를 전달한다
        //Observer객체는 LiveData로 부터 받아온 최신 데이터를 어떻게 처리할지를 정의하고 있다
            model.currentName.observe(this, nameObserver)
        
        // 받아온 value값을 어떻게 처리할지에 대한 코드를 람다블럭으로 처리하는 경우가 많다
            model.currentName.observe(this){ newName->
               nameTextView.text = newName
        }
    }
}

뷰모델.LiveData.observe(라이프사이클,Observer객체)

  • UI Controller(Activity,Fragment)에서 LiveData에 observer()를 호출한다=강아지 고양이보고 event 발생 관찰하라고 관찰자를 등록한다(LiveData 객체에 Observer 객체를 연결해서 Observer가 구독)
  • 이때 첫번째 인자로 라이프사이클을 전달하니까 UI Controller가 resume()이거나 start() 상태일때만 강아지 고양이가 활성상태라고 간주해서 onChange() 되었다고 알리기 위해서다
  • 두번째 인자로 Observer객체를 전달하는건, Observer 객체인 강아지가 LiveData 객체를 구독하여 변경사항에 관한 알림을 받아 해야할 작업을 정의하기 위함이다(UI를 업데이트 하는 작업을 작성)

✅공식문서를 한줄씩 해석해봅시다

  • 위에서 LiveData를 이해하기 위해 옵져버 패턴에서의 비유를 사용해 필수적으로 알아야할 개념, 간단한 사용법을 훑어봤습니다. 이제 해당 개념, 비유를 통해 LiveData의 공식문서에 나와있는 설명들을 한줄씩 해석해보며 이해해봅시다!
  • 특정 유형의 데이터를 보유할 LiveData의 인스턴스를 생성합니다. 이 작업은 일반적으로 ViewModel 클래스 내에서 이루어집니다.
    = LiveData를 생성합니다. LiveData는 산책가자! 버튼 클릭과 같은 event 내용이 바로 업데이트 되어 저장됩니다

  • LiveData는 Observer 인터페이스를 사용하여 관찰합니다. Observer 인터페이스에는 유일한 메서드인 onChanged가 있습니다. 이 메서드는 LiveData의 값이 변경되었을 때 호출되는 콜백 메서드로 이 메서드 안에서 LiveData의 값을 받아 사용하면 됩니다.

  • onChanged() 메서드를 정의하는 Observer 객체를 만듭니다. 이 메서드는 LiveData 객체가 보유한 데이터 변경 시 발생하는 작업을 제어합니다. 일반적으로 Activity, Fragment 같은 UI 컨트롤러에 Observer 객체를 만듭니다.
    = Owner의 명령을 관찰할 강아지 Observer 객체를 만듭니다. 강아지는 onChanged()메서드를 통해 Owner의 명령을 감지합니다.(앞선 예시의 update()와 동일한 일을 하는 함수라고 생각해봅시다) 일반적으로 Activity, Fragment같은 UI Controller에 Observer 객체 만듭니다

  • observe() 메서드를 사용하여 LiveData 객체에 Observer 객체를 연결합니다. observe() 메서드는 LifecycleOwner 객체를 사용합니다. 이렇게 하면 Observer 객체가 LiveData 객체를 구독하여 변경사항에 관한 알림을 받습니다. 일반적으로 활동이나 프래그먼트와 같은 UI 컨트롤러에 Observer 객체를 연결합니다.
    = LiveData가 가지고 있는 observe()메서드를 이용하여 관찰자인 강아지를 등록합니다. observe()메서드는 첫번째 인자로 LifecyclerOwner객체를 전달합니다. 이렇게 하면 관찰자인 강아지가 LiveData를 구독하여 변경사항에 관한 알림을 받습니다. 일반적으로 Activity, Fragment와 같은 UI컨트롤러에서 강아지를 LiveData와 연결합니다

  • LiveData는 UI Controller가 resume, start일때 옵져브가 활성상태=명령 처리 가능 이라고 간주해서 업데이트 정보를 알리고 모든 관찰자가 트리거됩니다
    = LiveData는 UI Controller가 resume, start일때 강아지가 깨어있는 상태(활성 상태)라고 간주해서 이벤트 발생했다는 update를 호출합니다. 강아지가 자고 있으면 비활성 상태로 간주해서 이벤트를 업데이트 하지 않습니다

  • LiveData는 활성 관찰자에게만 업데이트 정보를 알립니다. LiveData 객체를 보기 위해 등록된 비활성 관찰자는 변경사항에 관한 알림을 받지 않습니다.
    = LiveData는 활성 상태인 강아지에게만 onChange()를 호출합니다 비활성 강아지에겐 변경사항을 호출하지 않습니다

  • Lifecycle 객체의 상태가 DESTROYED로 변경될 때 관찰자를 삭제할 수 있습니다. 이는 특히 활동 및 프래그먼트에 유용합니다. 활동과 프래그먼트는 LiveData 객체를 안전하게 관찰할 수 있고, 수명 주기가 끝나는 즉시 수신 거부되어 누수를 걱정하지 않아도 되기 때문입니다.
    = UI Controller의 상태가 DESTROYED로 변경될 때 관찰자를 삭제할 수 있습니다. Activity, Fragment는 알아서 UI Controller의 수명주기가 끝나는 즉시 수신거부되어 누수를 걱정하지 않아도 됩니다

✅LiveData 사용의 이점

👉 UI와 데이터 상태의 일치 보장

= Observer패턴을 따른다->강아지가 알아서 onChange()된다->데이터 변경시 UI알아서 업데이트 된다

  • LiveData는 Observer 패턴을 따릅니다. LiveData는 기본 데이터가 변경될 때 Observer 객체에 알립니다. 코드를 통합하여 이러한 Observer 객체에 UI를 업데이트할 수 있습니다. 이렇게 하면 앱 데이터가 변경될 때마다 관찰자가 대신 UI를 업데이트하므로 개발자가 업데이트할 필요가 없습니다.

👉 메모리 누수 없음

= 강아지는 UI Controller의 수명주기를 따른다->수명주기 끝나면 강아지는 자동 삭제된다->메모리 누수 없다

  • 관찰자는 Lifecycle 객체에 결합되어 있으며 연결된 수명 주기가 끝나면 자동으로 삭제됩니다.

👉 중지된 Activity로 인한 비정상 종료 없음

= 강아지는 비활성상태일때는 알림을 받지 않는다->Activity가 종료되면 강아지도 알아서 쉬기때문에 받지못할 이벤트 알림을 대기하기 위한 불필요한 로직이 실행되지 않는다

  • 활동이 백 스택에 있을 때를 비롯하여 관찰자의 수명 주기가 비활성 상태에 있으면 관찰자는 어떤 LiveData 이벤트도 받지 않습니다.

👉 수명 주기를 더 이상 수동으로 처리하지 않음

= LiveData는 데이터가 변경될 때 활성 강아지에게만 업데이트를 전달한다-> 수명주기 따라 활성/비활성,onChange()호출 하는게 자동으로 된다

  • UI 구성요소는 관련 데이터를 관찰하기만 할 뿐 관찰을 중지하거나 다시 시작하지 않습니다. LiveData는 관찰하는 동안 관련 수명 주기 상태의 변경을 인식하므로 이 모든 것을 자동으로 관리합니다.

👉 최신 데이터 유지

= 강아지가 활성상태일때 onChange()되어 수신한 데이터는 항상 최신 데이터이다

  • 수명 주기가 비활성화되면 다시 활성화될 때 최신 데이터를 수신합니다. 예를 들어 백그라운드에 있었던 활동은 포그라운드로 돌아온 직후 최신 데이터를 받습니다.

👉 적절한 구성 변경

= 강아지는 onResume(), onStart()일때 데이터를 수신하므로 configuration change와 같은 일로 Activity, Fragment가 재생성되어도 바로 최신 데이터를 수신할 수 있다

  • 기기 회전과 같은 구성 변경으로 인해 활동 또는 프래그먼트가 다시 생성되면 사용 가능한 최신 데이터를 즉시 받게 됩니다.

✅MutableLiveData

  • 우리는 지금까지 UI Controller에서 LiveData 객체를 직접 참조하고 있었습니다. 그렇다면 UI Controller에서 LiveData객체의 값을 직접 변경하려면 어떻게 해야할까요?

  • 사실 이는 불가능하고 해서도 안됩니다. 안드로이드 권장 아키텍쳐 관점에서, UI Controller는 event발생을 trigger하고, 수동적으로 UI를 업데이트하는 일만 해야하기 때문에 UI Controller에 노출되는 LiveData는 값을 수정할 수 없는 것입니다.LiveData에는 저장된 데이터를 업데이트하는 데 공개적으로 사용할 수 있는 메서드가 없습니다.

  • 그래서 안드로이드는 값을 수정할 수 있는 LiveData인 MutableLiveData를 제공합니다. 안드로이드 아키텍쳐 관점에서 MutableLiveData는 UI Controller가 아닌 ViewModel에서 다뤄야 하며 ViewModel은 변경이 불가능한 LiveData객체만 관찰자에게 노출합니다

  • 즉 LiveData는 변하지 않지만(immutable) MutableLiveData는 변할 수 있습니다. MutableLiveData 클래스는 LiveData를 상속하고 있습니다. setValue(T) 및 postValue(T) 메서드를 공개 메서드로 노출하며 LiveData 객체에 저장된 값을 수정하려면 이러한 메서드를 사용해야 합니다.

  • 위는 MutableLiveData의 내부 코드 모습입니다. 그저 LiveData를 상속받아서 LiveData 내부에서만 사용할 수 있는 postValue와 setValue를 override 함으로써 외부에서 사용할 수 있게끔만 하고 그외 LiveData의 생성자를 사용한다는 내용밖에 없습니다. 즉 MutableLiveData는 그저 불변인 LiveData를 상속받아서 postValue와 setValue를 public함수에 호출함으로써 가변성을 부여하기 위함인 것을 추측할 수 있습니다.

  • MutableLiveData : 값의 get/set 모두를 할 수 있다.
  • LiveData : 값의 get()만을 할 수 있다.
    안드로이드 권장 아키텍쳐 관점에서, UI Controller는 event발생을 trigger하고, 수동적으로 UI를 업데이트하는 일만 해야하기 때문에 UI Controller에 노출되는 건 값을 수정할 수 없는 LiveData이다. 값을 수정하기 위해선 ViewModel에서 MutableLiveData를 사용해야한다.

✅setValue() / postValue()

👉 setValue()

  • 말그대로 값을 set하는 함수입니다. MutableLiveData객체를 setValue()를 통해 값을 변경시킨다면 메인 쓰레드에서 그 즉시 값이 반영됩니다.

  • 중요한 점은 메인 쓰레드에서 값을 dispatch시킨다는 점입니다. setValue()assertMainThread("setValue") 라는 함수가 존재합니다. 이 함수 내용은 아래와 같습니다.

  • 그 내부에는 메인 쓰레드가 아니라면 throw를 통해 예외 발생시키는 구문이 숨겨져있었습니다.
    그렇기 때문에 setValue()는 반드시 MainThread 환경에서 호출해야합니다.

👉 postValue()

  • 반면 postValue()mPendingData 객체에 임시로 value를 할당합니다. 이 함수는 setValue()와 달리 MainThread 환경에서 호출되지 않아도 예외처리 되는 부분은 존재하지 않습니다. 다만,
    ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable) 를 통해서 내부적으로 MainThread 로 바꿔서 실행합니다.

  • 매개변수로 들어갔던 mPostValueRunnable 객체 입니다. mPendingData의 값은 newValue라는 지역변수에 할당 되고 mPendingData는 다시 기초 값으로 초기화 됩니다.
  • postValue()백그라운드 thread인 상황에서 MutableLiveData객체를 set 하고 싶을 때 사용하는 메서드이기 때문에 호출 순서가 결과에 영향을 줄 수 있습니다. 공식문서를 읽어봅시다.
If you need set a value from a background thread, 
you can use postValue(Object) Posts a task to a main thread to set the given value. 
If you called this method multiple times before a main thread executed a posted task,
only the last value would be dispatched.
  • 따라서 postValue()를 한 다음 바로 다음 라인에서 LiveData의 getValue()를 호출한다면, 변경된 값을 받아오지 못할 가능성이 큽니다. 반면 setValue()로 값을 변경하면 메인쓰레드에서 변경하는 것이기 때문에 바로 다음 라인에서 getValue()로 변경된 값을 읽어올 수 있습니다
 liveData.postValue("a");
 liveData.setValue("b");
  • 우선 처음엔 “b”가 설정되고, 이후 메인스레드는 postValue(“a”)로 부터 값을 “a”로 변경시킵니다

setValue , postValue 차이
setValue -> MainThread에서 실행해야함 mData라는 멤버변수의 값을 할당함
postValue -> MainThread에서 실행하지 않아도 됨, 하지만 다시 MainThread환경으로 바꿔줌, mPendingData를 통해 값을 전달하고 mPendingData객체는 디폴트값으로 초기화 됨

✅앱 아키텍처 관점에서 LiveData

  • 아키텍쳐 관점에서 LiveData, ViewModel의 역할을 살펴봅시다.

  • LiveData는 수명 주기를 인식하여 액티비티와 프래그먼트 등 항목의 수명 주기를 따릅니다. LiveData를 사용하여 이러한 수명 주기 소유자와 ViewModel 객체 등 수명이 다른 객체 간에 통신할 수 있습니다.

  • ViewModel은 기본적으로 UI 관련 데이터를 로드하고 관리하는 역할을 하므로 LiveData 객체를 보유하는 데 적합합니다. LiveData 객체를 ViewModel에 만들고 이를 사용하여 UI 레이어에 상태를 노출합니다.

  • 액티비티와 프래그먼트는 상태 보유가 아닌 데이터를 표시하는 역할을 하므로 LiveData 인스턴스를 보유해서는 안 됩니다. 활동과 프래그먼트가 데이터를 보유하지 않도록 하면 단위 테스트를 작성하기도 쉬워집니다.

    LiveData는 UI Controller의 수명주기를 따르니까 UI 데이터를 hold하는 역할인 ViewModel에 보유해야 한다. 이를 통해 UI Controller는 UI Data를 "보유"하는게 아닌 "표시"하는 역할에 충실할 수 있다.

profile
'왜?'라는 물음을 해결하며 마지막 개념까지 공부합니다✍

0개의 댓글