[연재] 코틀린 프로젝트 - 02 - 7단계: 코루틴의 사용

이번에는 기존에 만들어진 RxJava 대신에 코루틴을 사용해 바꿔 봅시다. 코루틴을 사용하면 RxJava보다 가볍고 비동기 처리루틴을 사용하기가 더 쉽습니다. RxJava에서 사용되었던 구독자와 생산자로 지정되었던 부분을 수정할 것입니다.

코루틴을 사용하기 위한 추가 라이브러리 지정

1. 코루틴을 사용하기 위해서 기본 핵심 라이브러리와 Retrofit 확장 라이브러리를 추가하겠습니다.

모듈의 빌드 스크립트에 추가 build.gradle
...
dependencies {
...
    // Coroutines
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core-common:1.0.0"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.0"

    // Coroutines - Retrofit extention
    implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2'
    implementation 'ru.gildor.coroutines:kotlin-coroutines-retrofit:0.13.0-eap13'

2. 코루틴에서 백그라운드 작업에 사용할 변수를 프로그먼트 기본 클래스인 RxBaseFragment 클래스에 선언해 둡니다. 
코딩하기!     

Job의 객체 변수 선언과 생명 주기에 따른 동작 ui/RxBaseFragment.kt
...
open class RxBaseFragment : Fragment() {

    protected var subscriptions = CompositeDisposable()
    protected var job: Job? = null // (1) Job 변수 선언

    override fun onResume() {
        super.onResume()
        job = null // (2) 재개 될 때 코루틴 제거
    }

    override fun onPause() {
        super.onPause()
        job?.cancel() // (3) 코루틴의 취소 및 제거
        job = null
    }
}

(1)번과 같이 코루틴 작업을 위한 변수를 선언하였습니다. 앱이 중단되거나 재개될 때 기존의 작업은 제거하기 위해 null을 지정합니다. 특히 앱이 가려지거나 하게되면 onPause()가 호출되는데 이때 (3)번과 같이 코루틴 작업을 취소합니다. 

코루틴을 사용하는 launch()로 변경

3. 이제 앞서 만들어진 RxJava를 사용하는 구독 루틴인 requestMovie()를 다음과 같이 변경합니다.

코루틴을 사용하는 비동기 루틴 ui/MovieFragment.kt
...
class MovieFragment : RxBaseFragment(), MovieItemAdapter.ViewSelectedListener {

    private val movieManager by lazy { MovieManager() }
    private var theMovieList: MovieList? = null
    private val movieAdapter by androidLazy { MovieAdapter(this) }
...
    private fun requestMovie() {

        job = GlobalScope.launch(Dispatchers.Main) { // (1) 코루틴의 launch 빌더 사용
            try {
                val param = mapOf(
                    "page" to (theMovieList?.page).toString(),
                    "api_key" to API_KEY,
                    "sort_by" to "popularity.desc",
                    "language" to "ko"
                )
                val retrievedMovie = movieManager.getMovieList(param) // (2)
                retrievedMovie.page = retrievedMovie.page?.plus(1)

                theMovieList = retrievedMovie
                movieAdapter.addMovieList(retrievedMovie.results)
            } catch (e: Throwable) { // (3)
                if (isVisible) {
                    rv_movie_list.snackbar(e.message.orEmpty(), "RETRY") {
                        requestMovie()
                    }
                }
            }
        }
    }
}

(1)번에서 getMovie()에는 코루틴을 사용하기 위해 GlobalScope.launch()가 사용되었고 안드로이드의 UI스레드인 메인스레드에서 동작하기 위해 Dispatchers.Main을 인자로 지정했습니다. GET 요청에 보낼 변수들을 param으로 정의하고 (2)번에서 인자로 사용했습니다. 이렇게 가져온 목록은 results만 따로 읽어 어댑터에 추가합니다. 에러처리를 위해 (3)번과 같이 예외 처리를 할 수 있도록 try~catch 블록을 구성하고 원인 메시지 스낵바로 표시합니다. 만일 RETRY를 누르면 requestMovie()를 다시 시도할 수 있게 합니다. 

4. 전달된 param을 코루틴에서 처리할 수 있도록 getMovieList()를 지연 함수로 변경합니다. 

getMovieList의 매개변수 변경 ui/MovieManager.kt
...
class MovieManager(private val api: RestApi = RestApi()) {
    suspend fun getMovieList(param: Map<String, String>): Observable<MovieList> { // (1) 매개변수 변경
        val result = api.getMovieListCo(param) // (2) REST API의 코루틴 버전
        return process(result)
    }

    private fun process(response: MovieListResponse): MovieList {
        val list = response.results.map {
            MovieItem(
                it.vote_count,
                it.vote_average,
                it.title,
                it.release_date,
                it.poster_path,
                it.overview
            )
        }
        return MovieList(response.page, list)
    }
}

param은 Map으로 GET 요청이 들어있는 변수를 전달하게 됩니다. 이것을 받아서 다시 REST API를 처리할 getMovieListCo(param)에 전달합니다. 이 때 최종적으로 받아온 결과로 page와 results를 반환 하게 됩니다. 

Retrofit과 코루틴 함께 사용

5. 코루틴을 사용하면서 Retrofit에서도 HTTP의 GET 요청시에도 지연된 작업을 할 수 있게 Call<T> 대신에 Deferred<T> 형태를 사용할 수 있게 추가합니다.

코루틴을 사용하는 Deferred<T>의 추가 data/TheMovieService.kt
...
/**
 * Retrofit의 @GET 어노테이션으로 HTTP의 GET요청으로 JSON을 읽어온다
 */
interface  TheMovieService {
    @GET("discover/movie")
    /**
     * REST 요청을 처리하기 위한 메서드
     * @param par QueryMap을 통해 질의한 쿼리문을 Map으로 부터 받는다.
     * @return Call<T> 콜백 인터페이스 반환, T는 주고 받을 데이터 구조
     * @QueryMap 어노테이션은 위치가 바뀌어도 동적으로 값을 받아올 수 있게 한다.
     */
    fun getTop(@QueryMap par: Map<String, String>): Call<MovieListResponse>

    /**
     * 코틀린을 사용하는 버전의 요청 메서드 추가 부분
     * @return Deferred<T> 코루틴의 지연된 콜백 인터페이스 반환, T는 주고 받을 데이터 구조
     */
    @GET("discover/movie")
    fun getDeferredTop(@QueryMap par: Map<String, String>): Deferred<MovieListResponse>
    
}

6. 이것을 다시 REST API에서 사용할 수 있도록 RestApi에 코루틴을 위한 어댑터와 추가 메서드를 만들어 줍니다.

코루틴을 위한 지연 메서드의 추가 data/RestApi.kt
...
class RestApi {
    private val theMovieService: TheMovieService

    init {
        val retrofit = Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(MoshiConverterFactory.create())
            .addCallAdapterFactory(CoroutineCallAdapterFactory()) // (1) 코루틴을 사용하는 경우
            .build()
        theMovieService = retrofit.create(TheMovieService::class.java)
    }

    fun getMovieListRetrofit(param: Map<String, String>): Call<MovieListResponse> {
        return theMovieService.getTop(param)
    }

    suspend fun getMovieListCo(param: Map<String, String>): MovieListResponse { // (2)
        return theMovieService.getDeferredTop(param).await()
    }
}

(1)번에서 Retrofit의 코틀린 확장을 통해 코루틴을 위한 어댑터 설정을 추가합니다. (2)번에서는 지연 함수를 통해 코루틴에서 사용할 수 있게 합니다. 이때 await()을 사용해 분리된 루틴에서 결과를 기다릴 수 있게 되었습니다. 

이제 앱을 실행하고 정상적으로 구동되는지 확인합니다. 결과는 동일할 것입니다.

요청 처리부분이 RxJava에 비해서 더 간소화되었으며 suspend와 launch(), await() 등을 이용해 기존의 구독자 모델을 대체할 수 있게 되었습니다. 

 

youngdeok's picture

Language

Get in touch with us

"If you would thoroughly know anything, teach it to other."
- Tryon Edwards -

Contact us