본문 바로가기

Android

Retrofit2, OkHttp를 함께 사용하는 이유와 예제

728x90

이번 포스팅을 보시기전에 Retrofit2, OkHttp와 HTTP, REST API에 대해 알고싶다면 보시면 좋습니다.

 

 

Retrofit2

Retrofit에 대해 알아보기 전 HTTP, HTTPS, REST API를 간단하게 알아보겠습니다. HTTP 인터넷 상에서 HTML 문서와 같은 데이터를 주고받기 위한 프로토콜(통신 규약)으로, 서버-클라이언트 모델을 따라는

goni95.tistory.com

 

실제로 앱에서 통신할 때 Retrofit과 OkHttp를 함께 사용하는 경우가 많습니다.

두 라이브러리의 개념과 차이점과 장점 대해 자세히 몰랐기 때문에 Retrofit과 OkHttp을 공부하던 시점에서 왜 둘 다 사용하지.. Retrofit 라이브러리 하나만 사용하면 되는 것이 아닌가? 라는 생각이 들었습니다.

저뿐만 아니라 여러 사람들이 이렇게 느낄거라고 생각하고 정리해보려고 합니다.

Retrofit과 OkHttp 모두 HTTP 클라이언트 라이브러리인데?

Retrofit은 내부적으로 OkHttp를 사용하고 있으며 Retrofit이 Http 통신을 할 때 OkHttp에 의존하고 있고, Rest API를 사용할 때 더 편리하게 Http 통신을 할 수 있도록 도와주는 라이브러리라고 생각하면 됩니다.

Retrofit2와 OkHttp3를 함께 사용하는가!

두 라이브러리의 이점을 사용하기 위해서 입니다.

OkHttp Client의 옵션에서 여러 이점이 있는데, 예를들어 OkHttp Client에 네트워크 Intercepter를 통해 API가 통신되는 모든 활동을 모니터링, 커넥션 타임아웃으로 API 호출이 경과 시간에 따라 중단, 공통 파리미터 또는 헤더 추가 등의 이점이 있습니다.

Retrofit2의 Parameter, Query, Header 등의 매핑작업과 결과 처리작업 등의 반복되는 작업들을 편리하게 처리할 수 있어 둘의 장점을 이용하여 통신을 하기위해 함께 사용합니다.


Retrofit2의 이점

interface Retrofit_Service {
     @GET(API.GET_RLTMAIRPOLLUTION)
    fun get_rltmAirPollution(
        @Query("stationName") sttN: String,
        @Query("dataTerm") dataTerm: String,
        @Query("pageNo") pageNo: Int,
        @Query("numOfRows") numOfRows: Int,
        @Query("ver") ver: Double
    ) : Call<JsonElement>
}

OkHttp3의 이점

object RetrofitClient_Airkorea {
    fun getApiClient(baseUrl: String) : Retrofit {
        return Retrofit.Builder()
            .addConverterFactory(GsonConverterFactory.create())
            .baseUrl(baseUrl)
            // 밑에서 설정한 client로 retrofit client를 설정
            .client(getOkHttpClient())
            .build()
    }

    fun getOkHttpClient() : OkHttpClient {
        Log.d(Constants.TAG, "${this@RetrofitClient_Airkorea::class.java.simpleName} " +
                    "getOkHttpClient() called")

            //OkHttp Client 생성
        val client = OkHttpClient.Builder()

            //Interceptor 인터페이스를 구현하여 intercept() 메서드를 오버라이딩
        val baseParmsInterceptor = object : Interceptor {
            override fun intercept(chain: Interceptor.Chain): Response {

                /** 1) 공통 파라미터 추가*/

                    //inctercept()의 응답으로 온 chain 객체를 이용해 공통 파라미터 추가 가능
                val originalRequest = chain.request()

                    //파라미터 추가
                val addClientIdUrl = originalRequest.url.newBuilder()
                    .addQueryParameter("ServiceKey", API.API_ID_AIRKOREA)
                    .addQueryParameter("_returnType", API.RETURN_TYPE)
                    .build()

                   /** 2) 공통 헤더 추가*/

                    //헤더 추가
                val finalRequest = originalRequest.newBuilder()
                    .addHeader("Authorization", "API.API_ID")
                    .url(addClientIdUrl)
                    .method(originalRequest.method, originalRequest.body)
                    .build()

                /** 2) 서버로 부터 응답받기 */

                    //proceed() 메서드를 이용해 서버와 통신 후 응답받기 가능
                val response = chain.proceed(finalRequest)

                if(response.code != 200){
                    Handler(Looper.getMainLooper()).post {
                        //Handler() - android.os, 백그라운드 스레드에서 작업 중, ui 스레드에서 실행하도록 변경
                        Toast.makeText(App.instance, "${response.code} 에러 입니다.", Toast.LENGTH_SHORT).show()
                    }
                }

                return response
            }
        }

            // 위에서 생성한 기본 파라미터 인터셉터를 okhttp clent에 추가
        client.addInterceptor(baseParmsInterceptor)

               // 커넥션 타임아웃
        client.connectTimeout(2000, TimeUnit.MILLISECONDS)     //커넥션 작업의 타임아웃
        client.readTimeout(2000, TimeUnit.MILLISECONDS)       //읽기 작업의 타임아웃
        client.writeTimeout(2000, TimeUnit.MILLISECONDS)       //쓰기 작업의 타임아웃
        client.retryOnConnectionFailure(false)     //실패할 경우 다시 시도할 것인가?

        return client.build()
    }
}

 

Retrofit Interface 구현체

class RetrofitManager_Airkorea {
    companion object{
        val instance = RetrofitManager_Airkorea()
    }

    //Retrofit 객체인 RetrofitClient_Airkorea의 create() 메서드와 Interface인 Retrofit_Service 이용해 Client 객체 생성
    private val retrofitService : Retrofit_Service =
        RetrofitClient_Airkorea.getApiClient(API.BASE_URL_AIRKOREA).create(Retrofit_Service::class.java)

    //고차함수를 이용해 함수를 결과로 반환할 수 있도록 작성
    fun getRltmAirPollution(stationName : String, completion : (RESPONSE_STATE, JsonElement?) -> Unit) {
        Log.d(Constants.TAG, "${this::class.simpleName} " +
                "searchGeocoding() called")

        val sttN = stationName.let { it } ?: ""

    //Client 객체가 제공하는 enqueue() 메서드를 이용해 요청을 비동기로 처리하고, Callback Interface를 구현
        retrofitService.get_rltmAirPollution(sttN, "daily", 1, 1, 1.3).enqueue(object : Callback<JsonElement> {
            override fun onResponse(call: Call<JsonElement>, response: Response<JsonElement>) {
                Log.d(Constants.TAG, "${this@RetrofitManager_Airkorea::class.simpleName} " +
                        "onResponse() called : response.raw(): ${response.raw()}")
                Log.d(Constants.TAG, "${this@RetrofitManager_Airkorea::class.simpleName} " +
                        "onResponse() called : response.body(): ${response.body()}")
                Log.d(Constants.TAG, "${this@RetrofitManager_Airkorea::class.simpleName} " +
                        "onResponse() called : response.code(): ${response.code()}")

                when (response.code()) {
                    200 -> {
                        completion(RESPONSE_STATE.OK, response.body())
                    }
                }
            }

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

                completion(RESPONSE_STATE.FAIL, null)
            }
        })
    }
}

 

ViewModel

class AirPollutionViewModel(application: Application) : AndroidViewModel(application) {
    private lateinit var tmLocation : Pair<Double, Double>

    private var rltmAirPollutionData = RltmAirPollutionData()
    var rltmArPlltnLiveData = MutableLiveData<RltmAirPollutionData>()

    private val job = Job()
    private val uiScope = CoroutineScope(Dispatchers.Main + job)

    init {
        Log.d(Constants.TAG, "${this::class.simpleName} called")
    }

    // 실시간 대기오염도
    fun callRltmAirPollutionApi(sttN : String){

        //고차함수인 getRltmAirPollution(sttN)을 호출하고, 함수를 인자로 받는다
        RetrofitManager_Airkorea.instance.getRltmAirPollution(sttN) { responseState, responseBody ->
            when (responseState) {
                RESPONSE_STATE.OK -> {
                    Log.d(Constants.TAG, "${this::class.simpleName} " +
                            "실시간 대기오염도 API 호출 성공")

                    if (responseBody != null) parsingRltmAirPollution(responseBody)
                }
                RESPONSE_STATE.FAIL -> {

                    Log.d(Constants.TAG, "${this::class.simpleName} " +
                            "실시간 대기오염도 API 호출 실패")
                }
            }
        }
    }

    // 실시간 대기오염도 Parsing
    fun parsingRltmAirPollution(responseBody: JsonElement){
        uiScope.launch {
            val getRltmAirPollutionTask = launch(Dispatchers.Default, CoroutineStart.LAZY) {

                val attribute = arrayOf("so2Grade", "coGrade", "o3Grade", "no2Grade",
                    "pm10Grade1h", "pm25Grade1h", "khaiGrade")

                val body = responseBody.asJsonObject
                val list = body.getAsJsonArray("list")

                // list에서 jsonobject를 하나씩 받아 attribute가 있는지 확인 후 data class 객체에 저장
                list.forEach {
                    val item = it.asJsonObject

					/*attribute의 크기만큼 반복하여, data class인 RltmAirPollutionData 객체에 
                    정의한 set() 함수를 이용해 JsonObject에 같은 attribute가 있다면 추가*/
                    for (i in 0 until attribute.size){
                        if(item.has(attribute[i]))
                            rltmAirPollutionData.set(i, item.get(attribute[i]).asInt)
                        else
                            rltmAirPollutionData.set(i, 0)
                    }
                }

                rltmArPlltnLiveData.postValue(rltmAirPollutionData)
            }
            getRltmAirPollutionTask.join()
        }
    }

    override fun onCleared() {
        super.onCleared()
        job.cancel()
    }
}

'Android' 카테고리의 다른 글

Extension functions(확장 함수)  (0) 2021.03.28
StateBar (상태바) : 색상 변경  (0) 2021.03.28
Retrofit2 예제 (feat. HTTP, REST API, OkHttp)  (0) 2021.03.21
RecyclerView : 리사이클러  (0) 2021.03.15
Permission Check : 권한 체크  (0) 2021.03.14