C++, Java로 코드를 짤 때에 순차적으로 진행되는 코드를 Imperative Programming (명령형 프로그래밍)이라고 한다.
명령형 프로그래밍의 안드로이드를 예시로 들어보겠다. (극단적인 예시)
- 회원의 정보를 나타내는 TextView들이 있고,
회원의 정보를 불러온 후 이 정보를 TextView에 나타내기 위해서는
TextView가 회원의 정보를 일일이 나타내도록 TextView에 text를 설정해주어야 한다.
만일 회원의 정보를 불러온 후 바로 View들이 회원의 정보가 불러와진 것을 알고 Update될 수 있다면?
이를 위해 등장한 것이 Reactive Programming, 반응형 프로그래밍이다.
Reactive Programming (반응형 프로그래밍)
- 반응형 프로그래밍은 코드의 영향이 아닌,
환경에 영향을 받아서 작동하게 하는 프로그래밍 기법. - 동시성 문제를 해소해준다. 수많은 스레드를 생성해 비동기로 결과를 얻게 해준다.
- 콜백을 계속해서 호출하는 문제를 해결해준다.
- Observer 패턴을 주로 사용하는데,
Observer 패턴이란 Observable를 구독(Subscribe)하다가 Observable의 값이 바뀌면
Observer가 특정 작업을 하게 됨. - 데이터의 흐름, 즉 스트림을 단위로 사용한다.
스트림은 세 가지가 존재하는데,
next : 데이터의 다음 값
error : 에러발생
complete : 변화 종료. -> 이후에는 다음 값이 들어와도 update 되지 않음.(구독 종료)
Reactive Programming을 구현할 수 있도록 하는 것이 바로 RxJava이다.
러닝 커브가 높지만 그만큼 기능도 많이 제공되고 있다고 한다.
(특히 데이터 스트림에 관한 기능이 많다고 한다.)
Reactive Programming을 쉽게 사용할 수 있도록 Android Jetpack에는
DataBinding과 LiveData가 존재한다.
View(Observer)가 ViewModel(=LiveData, Observable)을 Subscribe하다가,
값이 바뀌면 View가 알아서 Update되는 방식도 Reactive Programming이다.
LiveData는 안드로이드 생명주기를 알고 있기 때문에
(이는 LiveData를 observe할 때에 viewLifecycleOwner를 설정하기 때문에 알게 된다.)
LiveData가 존재하는 View가 활성상태일 때에만 데이터의 변화를 Update한다.
그러면 왜 RxJava를 알아야 하는지?
- LiveData는 안드로이드 생명주기를 알고 있기 때문에 뷰모델 레이어에 존재하게 된다.
다르게 말하면 데이터 레이어, 레포지토리 레이어에 LiveData를 사용하는 것은 옳지 않다.
(Observer 패턴을 사용하기 위해 LiveData를 쓰는데 viewLifeCycleOwner를 지정할 수 없기 때문) - 나의 경우에는 Reactive Programming의 조상격인 RxJava를 정리하고 싶었음.
+ Flow를 공부하기 전, RxJava를 공부하고 싶었음.
ReactiveProgramming 예시 (RxJava, LiveData)
1. 유저 정보를 불러오고
2. 유저 정보를 수정할 수 있는
기능을 가진 프로젝트를 만들 것이다.
유저 정보를 불러올 때 Rx를 이용한 것과 LiveData를 이용해서
각각 Observer 패턴을 코드로 짜 보겠다.
(RxBinding, DataBinding은 사용하지 않겠다.)
앱 화면은 위처럼 만들었다.
우선 유저 정보를 불러오는 부분은 네트워크에서 불러오는 것이 아니라,
Application 클래스를 상속받은 클래스를 만들어서 manifest에 지정해주고
해당 클래스에서 companion object로 생성해 주었다.
전역 어플리케이션 코드
package com.example.rxjavaexampleproject.common
import android.app.Application
import com.example.rxjavaexampleproject.model.APIService
import com.example.rxjavaexampleproject.model.MyModel
class MyApplication: Application() {
companion object {
lateinit var apiService: APIService
}
override fun onCreate() {
super.onCreate()
apiService = APIService()
}
}
API 호출 코드
package com.example.rxjavaexampleproject.model
class APIService {
private val myModel = MyModel(0, "NAME")
fun getMyModel(): MyModel {
return myModel
}
fun updateMyModel(_idx: Int, _name: String) {
myModel.apply {
idx = _idx
name = _name
}
}
}
Model 데이터 클래스
package com.example.rxjavaexampleproject.model
data class MyModel(var idx: Int, var name: String)
Repository 클래스
package com.example.rxjavaexampleproject.repository
import com.example.rxjavaexampleproject.common.MyApplication
class MyRepository {
private val apiService = MyApplication.apiService
fun getMyModel() = apiService.getMyModel()
fun updateMyModel(_idx: Int, _name: String) = apiService.updateMyModel(_idx, _name)
}
이제 위 클래스들을 가지고 액티비티에서 데이터를 불러와서 화면에 보여줄 것이다.
MainActivity 클래스
package com.example.rxjavaexampleproject.view
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.widget.EditText
import android.widget.Toast
import androidx.core.widget.addTextChangedListener
import com.example.rxjavaexampleproject.databinding.ActivityMainBinding
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.observers.DisposableObserver
class MainActivity : AppCompatActivity() {
lateinit var viewBinding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(viewBinding.root)
setUpBtnListener()
}
private fun setUpBtnListener() {
viewBinding.liveDataBtn.setOnClickListener {
val intent = Intent(baseContext, LiveDataActivity::class.java)
startActivity(intent)
}
viewBinding.rxBtn.setOnClickListener {
val intent = Intent(baseContext, RxJavaActivity::class.java)
startActivity(intent)
}
}
}
메인 액티비티에서는 LiveData Activity / RxJava Activity로 이동하는 로직만 존재한다.
LiveData로 만들었을 때
LiveData를 효율적으로 사용하기 위해서 AAC ViewModel을 사용했다.
ViewModel 클래스
package com.example.rxjavaexampleproject.viewmodel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.example.rxjavaexampleproject.model.MyModel
import com.example.rxjavaexampleproject.repository.MyRepository
class MyViewModel(private val myRepository: MyRepository): ViewModel() {
private val _myModelLiveData = MutableLiveData<MyModel>()
val myModelLiveData: LiveData<MyModel>
get() = _myModelLiveData
private val myModel: MyModel?
get() = myModelLiveData.value
fun getMyModelInfo() {
val model = myRepository.getMyModel()
_myModelLiveData.postValue(model)
}
fun editMyModel(_name: String) = myModel?.idx?.let { _idx -> myRepository.updateMyModel(_idx + 1 , _name) }
}
AAC ViewModel에 매개변수를 넣어주기 위해 ViewModelFactory를 만들어야 한다.
ViewModelFactory 클래스
package com.example.rxjavaexampleproject.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.example.rxjavaexampleproject.repository.MyRepository
class MyViewModelFactory(private val myRepository: MyRepository): ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return modelClass.getConstructor(MyRepository::class.java).newInstance(myRepository)
}
}
이제 자주 사용하는 LiveData를 이용했을 때의 액티비티의 코드를 살펴보자면,
LiveData Activity 코드
package com.example.rxjavaexampleproject.view
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProvider
import com.example.rxjavaexampleproject.databinding.ActivityUserInfoBinding
import com.example.rxjavaexampleproject.repository.MyRepository
import com.example.rxjavaexampleproject.viewmodel.MyViewModel
import com.example.rxjavaexampleproject.viewmodel.MyViewModelFactory
/**
* LiveData를 통해 UI를 업데이트하는 Activity입니다.
* DataBinding이 아닌, Observer 패턴을 통해 UI를 Update했습니다.
*/
class LiveDataActivity: AppCompatActivity() {
lateinit var viewBinding: ActivityUserInfoBinding
private val myViewModel: MyViewModel by lazy {
ViewModelProvider(this, MyViewModelFactory(MyRepository())).get(MyViewModel::class.java)
}
private fun init() {
myViewModel.getMyModelInfo()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewBinding = ActivityUserInfoBinding.inflate(layoutInflater)
setContentView(viewBinding.root)
setUpObserver()
setUpBtnListener()
init()
}
//LiveData를 통해서 Observer 패턴을 이용해 View를 업데이트합니다.
private fun setUpObserver() {
myViewModel.myModelLiveData.observe(this) {
viewBinding.userIdxTv.text = it.idx.toString()
viewBinding.userNameTv.text = it.name
}
}
private fun setUpBtnListener() {
viewBinding.inputCompleteBtn.setOnClickListener {
val newName = viewBinding.userNameEditText.text.toString()
myViewModel.editMyModel(newName)
myViewModel.getMyModelInfo()
}
}
}
로직은 다음과 같다.
- 맨 처음 현재 유저의 정보를 가져와서 ViewModel의 LiveData에 기록해둔다.
- ViewModel의 myModelLiveData를 Observe한다.
이 때 해당 값이 Update되면 UI를 업데이트 한다. - '입력 완료' 버튼을 누르면 현재 유저의 정보가 수정되도록 한다.
LiveData를 이용하면 Observer 패턴을 굉장히 쉽게 구현할 수 있다.
심지어 LiveData가 LifeCycle을 알고있기 때문에 LifeCycle에 따라 구독을 알아서 중지해준다.
LiveData를 정리하는 글은 아니기 때문에 LiveData는 여기까지 보도록 하겠다.
RxJava로 만들었을 때
이제 RxJava를 사용해서 위와 같은 기능을 만들어 보겠다.
RxJava Activity 코드
package com.example.rxjavaexampleproject.view
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.example.rxjavaexampleproject.databinding.ActivityUserInfoBinding
import com.example.rxjavaexampleproject.model.MyModel
import com.example.rxjavaexampleproject.repository.MyRepository
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.core.ObservableOnSubscribe
import io.reactivex.rxjava3.core.Observer
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.observers.DisposableObserver
import io.reactivex.rxjava3.schedulers.Schedulers
class RxJavaActivity: AppCompatActivity() {
lateinit var viewBinding: ActivityUserInfoBinding
private var currentIdx: Int = 0
private val myRepository: MyRepository by lazy { MyRepository() }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewBinding = ActivityUserInfoBinding.inflate(layoutInflater)
setContentView(viewBinding.root)
init()
setUpBtnListener()
}
private fun getObservable() {
/* Create로 생성한 Observable 코드. Stream을 직접 설정할 수 있음. */
// Observable.create(ObservableOnSubscribe<MyModel> { emitter ->
// emitter.onNext(myRepository.getMyModel())
// emitter.onComplete()
// }).subscribe(object: DisposableObserver<MyModel>() {
// override fun onNext(t: MyModel) {
// currentIdx = t.idx
// viewBinding.userIdxTv.text = t.idx.toString()
// viewBinding.userNameTv.text = t.name
// }
//
// override fun onError(e: Throwable) {
// Toast.makeText(baseContext, "에러가 발생하였습니다.", Toast.LENGTH_SHORT).show()
// }
//
// override fun onComplete() {
// // onComplete() 호출시 자동으로 dispose() 메서드 호출됨.
// Log.d("RX::", "유저 정보 가져오기 성공.")
// }
// })
Observable
.just(myRepository.getMyModel())
//.subscribeOn(Schedulers.io()) 네트워크 작업일 경우 IO 스레드에서 작업.
.subscribe (
object: DisposableObserver<MyModel>() {
override fun onNext(t: MyModel) {
currentIdx = t.idx
viewBinding.userIdxTv.text = t.idx.toString()
viewBinding.userNameTv.text = t.name
}
override fun onError(e: Throwable) {
Toast.makeText(baseContext, "에러가 발생하였습니다.", Toast.LENGTH_SHORT).show()
}
override fun onComplete() {
// onComplete() 호출시 자동으로 dispose() 메서드 호출됨.
Log.d("RX::", "유저 정보 가져오기 성공.")
}
}
)
}
private fun init() {
getObservable()
}
private fun setUpBtnListener() {
viewBinding.inputCompleteBtn.setOnClickListener {
val newName = viewBinding.userNameEditText.text.toString()
myRepository.updateMyModel(currentIdx + 1, newName)
getObservable()
}
}
}
로직은 다음과 같다.
- Repository에서 데이터를 가져온다.
이를 Observable로 바꾸어서 익명 객체의 Observer를 생성해서
가져온 데이터에 따라 UI를 업데이트한다. - 입력완료 버튼을 누르면 모델을 업데이트하고 1의 로직을 실행해서 UI를 업데이트 한다.
사실 위 RxJava 예제 코드를 짜면서 든 의문점은 다음과 같다.
- 그래서 Rx의 장점이 무엇이지?
- Observer를 onNext(<T>)를 바로 호출하면 되는데,
왜 Observable을 사용해야 할까?
찾아보면서 각각의 답변을 해 보자면
- 그래서 Rx의 장점이 무엇이지?
-> 위 예제의 경우에는 Rx의 장점을 완벽히 사용한 것이 아니다.
RxJava는 ReactiveProgramming과 비동기 작업을 동시에 처리할 수 있도록 해준다.
내가 정리한 부분은 ReactiveProgramming 부분으로 RxJava의 기능의 극히 일부였다.
.map / .flatMap 등의 메서드를 사용해서 콜백지옥을 벗어날 수 있게 된다고 한다.
나는 안드로이드 개발을 시작할 때, Coroutine을 먼저 사용해서 배워서
너무 간단하게 비동기 처리를 할 수 있었다.
Coroutine, LiveData를 사용해서 먼저 반응형 프로그래밍 + 비동기 처리를 해서
"굳이 Rx를...?" 이라는 생각이 들지만
러닝 커브가 높은만큼 정말 많은 기능을 제공하는 것 같다. - 왜 Observable을 사용해야 할까?
-> 이 부분에 대해서는 내가 잘못 이해한 것인지 모르겠지만 자료를 찾을 수 없었다.
나만 궁금해 하는 내용인 것 같은데,,,
PublishSubject(Subject)를 사용해서 onNext()만 사용할 수 있는 것 같긴하다.
Observer만 생성해서 onNext()로만 사용하면 안되는걸까? - 또한 위 예시 코드는 메모리 누수 위험성이 있는 코드이다.
Observer가 View 관련된 로직을 참고하는데,
만일 액티비티가 비정상적으로 종료되면(화면 회전 등의 이유)
구독하는 상황이 사라지지 않기 때문이다.
Rx는 정말 다양한 기능을 제공하는 만큼 러닝커브가 확실히 높은 것 같다.
(그도 그럴게, 반응형 프로그래밍과 비동기성을 동시에 제공해주니,,,)
그러나 Rx를 경험해보지 않고 Flow와 LiveData, Coroutine을 사용하고 싶진 않았다.
찜찜하지만 이 정도로 경험해보고 이후에 필요에 따라 추가적인 기능을 정리해야 할 것 같다.
전체 프로젝트 코드는 아래에서 확인 가능하다.