[Android] Retrofit With Coroutine (In MVVM Architecture)
Android/Kotlin

[Android] Retrofit With Coroutine (In MVVM Architecture)


📌 Retrofit with Coroutine (In MVVM Architecture)


이 문서는 Coroutine 선행 공부가 되신 분들이 보기 쉽게 작성되었습니다

Coroutine에 대하여 좀 더 공부하고 싶다면 아래와 같은 페이지를 참조하세요!

https://zladnrms.tistory.com/116

 

[Kotlin] Coroutine 연습 문제

Coroutine을 공부하면서, 문서를 꼼꼼히 읽어봐도 실전에서는 분명 헷갈리는 부분이 적지 않았다. 그래서 다시 Unit Test를 통하여 연습하던 중에, 이를 문제 - 정답 형식의 문서로 작성해두면 좋을 것 같아 남기..

zladnrms.tistory.com

 

Coroutine을 학습하기 전 까지는 네트워크 비동기 처리를 위하여 Retrofit2을 사용하였고, Response Data의 Stream 처리를 위해서는 RxJava2를 사용해왔습니다.

 

그러나 Coroutine을 학습하며, 상대적으로 더 깔끔한 코드, 메소드 체이닝이 아닌 Scope별로 나누는 스레드 스위칭 방식. 그리고 RxJava의 초반 학습곡선 높음과 그에 따른 협업에 있어서의 유지보수의 어려움 등 여러 가지 이유로, 기존에 RxJava를 사용하였던 비동기 처리가 필요한 부분, 특히 서버와의 API 통신 부분을 Coroutine으로 교체하는 작업을 진행하였습니다.

 

그러나 Retrofit의 최신 버전에 맞춘 Coroutine과의 비동기 네트워크 처리 구현에 대한 정보가 부족하다고 생각하였고, 그래서 제가 구현한 방법에 대해 알려드리려 작성하게 되었습니다.

📝 구현

MVVM architecture 형태로 구현된 앱에서의 예시입니다.

 

📍가게 목록 API를 GET method로 가져오는 상황을 연출하기

 

API 호출을 위한 Retrofit 인터페이스를 구현

//RetrofitInterface.kt
@GET("/xxx/xxx")
suspend fun getStoreList(@Header("Authorization") token: String?) : StoreListRepsonse

 

Hot Ez Ex) 위 코드와 동일하게 작성 시 Retrofit 2.6.0 이상에서 진행하지 않으면 에러가 납니다!!! 반드시 최신 버전을 유지해주셔야 합니다.

 

2.6.0 버전 이전에는 API 요청에 대한 응답을 반환받을 때 Response<T>가 필수였습니다.

하지만 Coroutine의 suspend modifier 업데이트 이후 retrofit2에서도 변경점이 있었습니다.

suspend 식별자를 붙이면 Response가 필수가 아니게 된 것이 그것입니다.

이로써 API 호출 후 데이터 클래스로 바로 반환받는 것이 가능하게 되었고, 이는 API 호출 결과 처리에 있어서 기존의 상용구코드, Boiler Plate들을 줄여준다는 것에 있어서 의미가 큽니다.

(Retrofit2 ChangeLog : Ref)

 

물론 Response을 사용하여 조금 더 편리하게 Response MetaData에 대한 처리를 해줄 수 있습니다.

하지만 직접 Http Request Code나 성공 / 실패에 따른 예외처리를 한다면 굳이 필요하지는 않습니다.

결론적으로, 기존에 비해 매우 간결해진 Network 비동기 처리가 가능해졌습니다.

suspend 수식어에 대해서는 아래 주소를 참고하면 도움이 됩니다. Ref)

 

대략 요약하자면, Coroutine은 thread위에서 동작하는데, suspend 수식어가 붙은 함수가 실행되면 그 직후부터 그 함수가 끝나거나 값을 반환할때까지, thread를 block시키지 않고 suspend(지연)시키는 것 입니다. 즉,  그 함수가 호출되면 해당 함수로 Context Switching이 즉시 실행되는 것이 아니라, 우선 지연되어 특정한 시점에 호출되어 처리하는 것 입니다.

이러한 방법은 block에 비해 cost가 상대적으로 free하다고 합니다.

 

 

Retrofit Interface에 대해서 잠시 알아보았습니다.다음으로 볼 것은 API를 호출하여 처리할 ViewModel에서의 로직입니다.

//StoreViewModel.kt
class StoreViewModel(private val repository: UserRepository, private val api: NetworkInterface) : DisposableViewModel() {

    private val user by lazy {
        viewModelScope.async(Dispatchers.IO) {
            repository.getUserInfo()
        }
    }

    // recyclerview list
    val storeList = MutableLiveData<ArrayList<Store>>().apply { value = arrayListOf() }
    // empty list
    val emptyList : LiveData<Boolean> get() = Transformations.map(storeList) {
        it.isEmpty()
    }
    // is True = ProgressView VISIBLE , is False = ProgressView GONE
    private val _dataLoading = MutableLiveData<Boolean>()
    val dataLoading: LiveData<Boolean> get() = _dataLoading

    // ViewModel의 생성과 함께 API 호출
    init {
        getStoreList()
    }
    
    // 가게 목록을 가져오는 함수
    fun getStoreList() {
        // Progress View VISIBLE
        _dataLoading.value = true
        // StoreList 초기화
        storeList.value?.clear()

        // 비동기 처리 Scope 선언
        viewModelScope.launch(Dispatchers.IO) {
            try {
                //user.key = token XXXXXXXXXXXXXXXX
                api.getMyStoreList(user.await().key.tokenize()).apply {
                    if (this.results.size > 0) {
                        storeList.value?.addAll(this.results)
                    }

                    withContext(Dispatchers.Main) {
                        storeList.value = storeList.value
                        // Progress View GONE
                        _dataLoading.value = false
                    }
                }
            } catch (e: Throwable) {
                ...
            }
        }
    }

 

 

 

Hot Ez Ex) viewModelScope의 경우 undefined references 표시가 날 수 있는데, 이는 dependency를 추가하지 않았기 때문입니다. 이 경우 아래의 Dependency를 추가해주시면 됩니다.. (최신 버전은 주소 참조)

//in build.gradle(Module: app)
dependencies {
    androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0-beta01
}​

 

https://developer.android.com/topic/libraries/architecture/coroutines#dependencies 참조

 

위의 처리 로직에 등장하는 변수들에 대하여 간략히 설명하자면 아래와 같습니다.

  • DisposableViewModel : ViewModel을 상속하며 LifeCycle에 맞추어 RxJava의 Observable 구독 해제를 담당하는 역할이 추가됨
  • repository: UserRepository : 회원 정보를 담고 있는 Local DB
  • api: NetworkInterface : API 요청을 위한 Retrofit Interface
  • storeList : Store data class를 List로 가지고 있는 객체를 가지고 있는 LiveData 클래스.
  • RecyclerView의 List로써 쓰인다.
  • fun getStoreList() : 서버로 부터 Store의 목록을 받아오는 비동기 네트워크 처리 함수.
  • dataLoading: 비동기 처리 중 화면에 Loading Progress를 표시할 View의 VISIBLE 처리를 위한 LiveData객체.
  • viewModelScope : ViewModel 내에서 Coroutine을 처리하기 위한 Scope 선언
  • repository.getUserInfo() : Local DB에 저장된 유저의 데이터를 사용하기 위한 함수. 여기서는 회원의 Token 값을 사용하기 위해 호출함.
  • api.getMyStoreList() : Remote DB와 연동하기 위한 함수. 서버에서 유저의 Store 목록을 가져온다

 

ViewModel 내에서 RxJava를 사용하셨다면, 메모리 누수 방지를 위해 LifeCycle Destroyed에 맞추어 구독을 해지해주어야 합니다.

이를 ViewModel을 상속하여 구현한 것이 DisposableViewModel입니다.

 

open class DisposableViewModel: ViewModel() {

    /**
     * Observable의 Disposable 객체를 모아두는 클래스.
     * ViewModel이 clear될 때, 한 번에 구독해지하는 역할을 담당함.
     */
    private val compositeDisposable = CompositeDisposable()

    fun addDisposable(disposable: Disposable) {
        compositeDisposable.add(disposable)
    }

    override fun onCleared() {
        compositeDisposable.clear() // or dispose()
        super.onCleared()
    }
}​

 

 

또한, 아까 전의 예시인 StoreViewModel의 API 요청 소스에서는 예외 처리를 위하여 try{} catch{}문으로 처리하고 있습니다.

그러나 ViewModel 내에서 수많은 비동기 처리와 그에 따른 예외 처리를 해줄 때마다, try ~ catch로 작성한다면,

이는 분명 Boiler plate입니다.

 

Coroutine은 CoroutineScope 내부의 예외처리 Handler을 제공하고 있습니다.

CoroutineExceptionHandler이라는 녀석입니다.

 

이에 대해서는, 다른 글에서 다루는 것으로 하고, 우선은 이 CoroutineExceptionHandler를 사용함으로써 코드가 어떻게 간결해지는 지 확인해보도록 하겠습니다.

 

 

open class DisposableViewModel: ViewModel() {

    /**
     * CoroutineScope 내부 Exception 처리 Handler
     */
    protected val coroutineExceptionHanlder = CoroutineExceptionHandler { coroutineContext, throwable ->
        throwable.printStackTrace()
    }

    /**
     * Dispatchers 선언 (Normal Dispatchers + CoroutineExceptionHandler)
     */
    protected val ioDispatchers = Dispatchers.IO + coroutineExceptionHanlder
    protected val uiDispatchers = Dispatchers.Main + coroutineExceptionHanlder

    /**
     * Clear Rx when called onCleared
     */
    private val compositeDisposable = CompositeDisposable()

    fun addDisposable(disposable: Disposable) {
        compositeDisposable.add(disposable)
    }

    override fun onCleared() {
        compositeDisposable.clear()
        super.onCleared()
    }
}​

 

위에서 보여드렸던 DisposableViewModel에 CoroutineExceptionHandler을 추가한 모습입니다.

CoroutineScope 내에서 발생한 Exception을 Catch 해주어 coroutineContext와 Throwable의 형태로 반환해줍니다.

 

또한 Dispatchers.IO와 Dispatchers.Main 등 사용할 쓰레드에 위의 핸들러를 추가하여 따로 변수를 만들어주었습니다.

 

그렇다면 이제 위의 DisposableViewModel을 상속한 ViewModel에서의 처리를 보도록 하겠습니다.

 

//StoreViewModel.kt
class StoreViewModel(private val repository: UserRepository, private val api: NetworkInterface) : DisposableViewModel() {

    private val user by lazy {
        viewModelScope.async(ioDispatchers) {
            repository.getUserInfo()
        }
    }

    // recyclerview list
    val storeList = MutableLiveData<ArrayList<Store>>().apply { value = arrayListOf() }
    // empty list
    val emptyList : LiveData<Boolean> get() = Transformations.map(storeList) {
        it.isEmpty()
    }
    // is True = ProgressView VISIBLE , is False = ProgressView GONE
    private val _dataLoading = MutableLiveData<Boolean>()
    val dataLoading: LiveData<Boolean> get() = _dataLoading

    // ViewModel의 생성과 함께 API 호출
    init {
        getStoreList()
    }
    
    // 가게 목록을 가져오는 함수
    fun getStoreList() {
        // Progress View VISIBLE
        _dataLoading.value = true
        // StoreList 초기화
        storeList.value?.clear()

        // 비동기 처리 Scope 선언
        viewModelScope.launch(ioDispatchers) {
            api.getMyStoreList(user.await().key.tokenize()).apply {
                if (this.results.size > 0) {
                    storeList.value?.addAll(this.results)
                }

                withContext(uiDispatchers) {
                    storeList.value = storeList.value
                    // Progress View GONE
                    _dataLoading.value = false
                }
            }
        }
    }
}​

 

CoroutineScope 내의 모습이 한결 보기 편해진 모습입니다. 

dddq#안드로이드 #redqwtrofit with coroutine #비동기 네트워크 처리 #async network #coroutine #retrofit2qwdds

'Android > Kotlin' 카테고리의 다른 글

[Kotlin] 함수형 프로그래밍  (0) 2020.01.06
[Kotlin] Coroutine 연습 문제  (0) 2020.01.03
[Android/kotlin] Layout Margin dp 단위 변경하기  (1) 2019.11.25
[Android/kotlin] ktlint 설치  (0) 2019.11.18
Android Coroutine 정리  (0) 2019.08.22