본문 바로가기

Android

Retrofit2, OkHttpInterceptor, Koin, RxKotlin, Coroutine, Sandwich 등을 이용해 내가 작성한 네트워크 통신 방법

728x90

Retrofit2을 이용해 여러 네트워크 통신 방법을 구현한 코드들을 정리했습니다.

 

Retrofit2 인터페이스

    @GET(API.SEARCH_WEATHER)
    fun search_weather2(
            @Query("q") location : String,
            @Query("lang") lang : String) : Single<JsonElement>
            
    @GET(API.SEARCH_WEATHER)
    suspend fun search_weather(
        @Query("q") location : String,
        @Query("lang") lang : String) : ApiResponse<JsonElement>
        
   @GET(API.SEARCH_WEATHER)
   fun search_weather3(
        @Query("q") location : String,
        @Query("lang") lang : String) : Call<JsonElement>

 

OkHttp Interceptor를 작성

class RetrofitClient_Weather : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        Log.d(
            Constants.TAG, "${this@RetrofitClient_Weather::class.java.simpleName} " +
                    "intercept() / chain : $chain")

        /**
         * 1) 공통 파라미터 장착
         */
        val originalRequest = chain.request()

        val addClientIdUrl = originalRequest .url.newBuilder()
            .addQueryParameter("key", API.API_ID_WEATHER)
            .addQueryParameter("days", "3")
            .build()

        Log.d(
            Constants.TAG, "${this@RetrofitClient_Weather::class.java.simpleName} " +
                    "intercept() / addClicentUrl : $addClientIdUrl")

        val finalRequest = originalRequest.newBuilder()
            .url(addClientIdUrl)
            .method(originalRequest.method, originalRequest.body)
            .build()

        Log.d(
            Constants.TAG, "${this@RetrofitClient_Weather::class.java.simpleName} " +
                    "intercept() / finalRequest : ${finalRequest}")

        /**
         * 2) 서버로 부터 응답받기
         */
        val response = chain.proceed(finalRequest) //오리지날 요청으로 변경하면 401에러 문제없이 출력됨

        Log.d(
            Constants.TAG, "${this@RetrofitClient_Weather::class.java.simpleName} " +
                    "intercept() / responseCode : ${response.code}")

        return response
    }
}

 

OkHttp Client에 Interceptor를 추가하고, 이를 이용해 Retrofit2 Client 생성하는 코드

fun retrofitModule(wthrbaseUrl : String) = module {
    single<OkHttpClient> {
        OkHttpClient.Builder().apply {
            addInterceptor(get<RetrofitClient_Weather>())
            connectTimeout(3000, TimeUnit.MILLISECONDS)     //커넥션 작업의 타임아웃
            readTimeout(3000, TimeUnit.MILLISECONDS)       //읽기 작업의 타임아웃
            writeTimeout(3000, TimeUnit.MILLISECONDS)       //쓰기 작업의 타임아웃
            retryOnConnectionFailure(false)     //실패할 경우 다시 시도할 것인가?
        }.build()
    }

    single<Retrofit_Service> {
        Retrofit.Builder()
            .addConverterFactory(GsonConverterFactory.create())
            
            
            //Retrofit 반환타입이 AirResponse<T>인 경우
            .addCallAdapterFactory(CoroutinesResponseCallAdapterFactory())
            
            //RxJava를 이용하는 경우
            addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            
            
            .baseUrl(wthrbaseUrl)
            // 위에서 설정한 client로 retrofit client를 설정한다.
            .client(get<OkHttpClient>())
            .build()
            .create(Retrofit_Service::class.java)
    }

}

 


호출 코드 RxJava

val disposable =   weatherRepository.search_weather3(locationFormat, lang)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .filter { true }
            .map { it.asJsonObject }
            .subscribeBy(
                onError = { e ->
                    println(e.toString())
                },
                onSuccess = { response ->
                    Log.d(Constants.TAG, "${this::class.simpleName} " +
                            "search()-- 출력")

                    parsingWeatherJsonData(response).apply {
                        loadingLiveData.value = false
                    }
                }
            )

        Thread{
            try {
                Thread.sleep(3000)
                Log.d(Constants.TAG, "${this::class.simpleName} " +
                        "search()-- 진행 중")

            }catch (e: Exception){
                e.printStackTrace()
            }

            Log.d(Constants.TAG, "${this::class.simpleName} " +
                    "search()-- 종료")

            disposable.dispose()
        }.start()

 

호출 코드 Sandwich (enqueue() 대신 request를 이용해 같은 기능을 구현할 수 있는 것으로 판단됨)

  • Interface의 반환타입이 Call<타입>일때 사용할 수 있습니다.
        weatherRepository.search_weather(locationFormat, lang).request {  response ->
                when (response) {
                    // handles the success case when the API request gets a successful response.
                    is ApiResponse.Success -> {

                    }
                    // handles error cases when the API request gets an error response.
                    // e.g., internal server error.
                    is ApiResponse.Failure.Error -> {
                        // stub error case
                        Log.d(Constants.TAG, "${this::class.simpleName} " +
                                "Error")

                        // handles error cases depending on the status code.
                        when (response.statusCode) {
                            StatusCode.InternalServerError ->
                                Log.d(Constants.TAG, "${this::class.simpleName} " +
                                        "${response.statusCode}")
                            StatusCode.BadGateway ->
                                Log.d(Constants.TAG, "${this::class.simpleName} " +
                                        "${response.statusCode}")
                            else ->
                                Log.d(Constants.TAG, "${this::class.simpleName} " +
                                        "${response.statusCode}")
                        }
                    }
                    // handles exceptional cases when the API request gets an exception response.
                    // e.g., network connection error, timeout.
                    is ApiResponse.Failure.Exception -> {
                        // stub exception case
                    }
                }
            }

 

고차함수를 반환하는 Genetic 확장 함수를 이용한 코드

fun <T> Call<T>.enqueue(callback : (result : Response<T>?, msg : String) -> Unit) {
    enqueue(object : Callback<T> {
        override fun onResponse(call: Call<T>, response: Response<T>) {
            Log.d(
                Constants.TAG, this@enqueue::class.java.simpleName +
                    "1 onResponse() called : ${response}")

            when(response.code()){
                200 ->{
                    callback.invoke(response, response.message())
                }


                else ->
                    callback.invoke(null, response.errorBody()?.string() ?: response.message())
            }
        }

        override fun onFailure(call: Call<T>, t: Throwable) {
            Log.d(
                Constants.TAG, this@enqueue::class.java.simpleName +
                    "1 onFailure() called : ${t}"
            )

            callback.invoke(null, t.message ?: "error!")
        }
    })
}

//use
weatherRepository.search_weather3(locationFormat, lang).enqueue { result, msg ->
            
        ...
            
    }

 

Retrofit2 Interface의 반환 타입을 일시 중지 함수의 반환 유형인 ApiResponse<T>로 선택하여 Sandwich와 Coroutine을 이용한 코드

  • Interface의 반환 타입을 ApiResponse<T>로 설정하려면 suspend 키워드가 꼭 필요하며, Retrofit2 Http Client를 생성하는 코드에 .addCallAdapterFactory(CoroutinesResponseCallAdapterFactory())를 추가해야합니다.
  • 해당 코드가 사용되는 함수는 필수적으로 suspend 한정자가 추가되거나, 비동기 환경에서 사용되어야 합니다.
  val response : ApiResponse<JsonElement> = weatherRepository.search_weather(locationFormat, lang)
            response.onSuccess {
                Log.d(Constants.TAG, "${this::class.simpleName} " +
                        "onSuccess ${data?.asJsonObject}")

                data?.asJsonObject?.let { parsingWeatherJsonData(it) }
                }.onError {
                // stub error case
                Log.d(Constants.TAG, "${this::class.simpleName} " +
                        "Error")

                // handles error cases depending on the status code.
                when (statusCode) {
                    StatusCode.InternalServerError ->
                        Log.d(Constants.TAG, "${this::class.simpleName} " +
                                "$statusCode")
                    StatusCode.BadGateway ->
                        Log.d(Constants.TAG, "${this::class.simpleName} " +
                                "$statusCode")
                    else ->
                        Log.d(Constants.TAG, "${this::class.simpleName} " +
                                "지정된 예시가 없는 에러 코드 : $statusCode")
                }
            }.onException {
                Log.d(Constants.TAG, "${this::class.simpleName} " +
                        "exception : $exception / message : $message")
            }

        loadingLiveData.postValue(false)

 

해당 코드를 선택한 이유는 Koin을 이용해 모듈을 생성하고 의존성을 주입받고, Coroutine을 이용해 비동기 환경에서 작동할 수 있도록 처리하며, Sandwich 라이브러리로 응답에 대한 처리를 깔끔하게 정리할 수 있어 현재로선 해당 방법이 가독성이나, 구현하고자 하는 프로그램 코드를 기능별로 적절히 나눠 모듈(크게는 하나의 파일, 작게는 하나의 함수)화 시킬 수 있고 이로인한 수월한 관리가 이루어질 수 있어 채택!