Retrofit은 예제를 볼 때에는 그다지 어려워보이지 않는데 막상 만들 때에는 복잡하게 느껴진다. OkHttp와 함께 쓰려다보니 헷갈려서 정리 겸 쓰는 포스트.


Retrofit? OkHttp?

레트로핏은 안드로이드와 자바에서 쉽게 RESTful한 통신을 할 수 있도록 도와주는 라이브러리이다. 글을 쓰는 2020년 1월 기준으로 2.4 버전을 지원하고 있다. 호출이나 비동기 처리 등 이용이 손쉽고, 속도도 Volley 등 다른 라이브러리보다 빨라서 대세 라이브러리로 자리잡았다.

레트로핏을 더 편리하게 쓰기 위해 흔히 OkHttp를 함께 사용하곤 한다.

OkHttp는 Http를 더 간편하고 효율적으로 쓸 수 있게 도와주는 클라이언트이다. 이 예제에서는 입력값이나 서버 응답을 로그로 쉽게 확인하기 위해서, 또 헤더 값을 편하게 입력하기 위해서 OkHttp를 썼다. 예제로 사용하기 만만한 네이버 API를 이용해 Retrofit과 OkHttp 사용법을 알아보자.



일단 Retrofit 만 사용해보기

우선 Retrofit만을 사용해 예제를 만들어보았다.

네이버 API 사용 예제 - GET

검색어를 입력하면 관련 기사를 찾아주는 네이버 뉴스 API를 사용해보자. GET으로 입력하면 JSON을 출력하는 API이다. 우선 네이버 개발자센터에서 서비스 API - 뉴스에 쓰여있는 대로 출력되는 JSON 값에 대한 모델 클래스를 하나 만들었다.

// ResultGetSearchNews.kt

data class ResultGetSearchNews(
    var lastBuildDate: String = "",
    var total: Int = 0,
    var start: Int = 0,
    var display: Int = 0,
    var items: List<Items>
)

data class Items(
    var title: String = "",
    var originallink: String = "",
    var link: String = "",
    var description: String = "",
    var pubDate: String = ""
)

그리고 레트로핏을 쓰기 위한 api interface도 생성했다. 헤더에 네이버 개발자센터 id와 비밀번호를 넣어야 하는데, @Header를 이용해 간단히 처리할 수 있다. 쿼리 또한 @Query 어노테이션을 이용해 함수를 호출할 때 입력받도록 적어 두었다. 단, display와 start 속성은 필수가 아니라 nullable로 처리해두었다.

// NaverAPI.kt

interface NaverAPI {
    @GET("v1/search/news.json")
    fun getSearchNews(
        @Header("X-Naver-Client-Id") clientId: String,
        @Header("X-Naver-Client-Secret") clientSecret: String,
        @Query("query") query: String,
        @Query("display") display: Int? = null,
        @Query("start") start: Int? = null
    ): Call<ResultGetSearchNews>
}


그리고 액티비티에서 레트로핏을 하나 생성해준다. call.enqueue()를 통해 인터페이스로부터 함수를 호출할 수 있다. enqueue 할 때에는 콜백을 파라미터로 넣어 통신 성공 및 실패 시의 처리를 핸들링한다.

call.execute()함수는 요청을 현재 쓰레드에서 처리하기 때문에 메인 쓰레드에서 사용하면 크래쉬가 발생할 것이다. call.enqueue(callback) 함수를 사용하면 백그라운드 쓰레드에서 요청을 수행한 후에 콜백은 현재 쓰레드에서 처리한다.


// MainActivity.kt

val CLIENT_ID = "네이버_개발자센터_아이디"
val CLIENT_SECRET = "네이버_개발자센터_비밀번호"

val retrofit = Retrofit.Builder()
            .baseUrl(BASE_URL_NAVER_API)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
val api = retrofit.create(API::class.java)
val callGetSearchNews = api.getSearchNews(CLIENT_ID, CLIENT_SECRET, "테스트")

callGetSearchNews.enqueue(object : Callback<ResultGetSearchNews> {
    override fun onResponse(
        call: Call<ResultGetSearchNews>,
        response: Response<ResultGetSearchNews>
    ) {
        Log.d(TAG, "성공 : ${response.raw()}")
    }

    override fun onFailure(call: Call<ResultGetSearchNews>, t: Throwable) {
        Log.d(TAG, "실패 : $t")
    }
})


네이버 API 사용 예제 - POST

파파고 번역 API 를 이용해 POST도 테스트했다. 위에서 만들어 둔 API를 재활용하여 통신했는데, 다음과 같은 에러 메세지를 받았다.

{
    "errorMessage": "Internal Server Error",
    "errorCode": "XX99"
}


POST 방식인데, 요청 변수를 Body가 아니라 Query로 보냈기 때문에 발생한 문제였다. 하필 에러 메세지가 서버 에러라고 출력되어 헷갈렸는데, 쿼리 부분을 바꾸고 나니 해결되는 문제였다.

위에서 했던 것과 마찬가지로 결과값에 대한 데이터 모델을 우선 만들어주었다.

// ResultTransferPapago.kt
data class ResultTransferPapago (
    var message: Message
)

data class Message(
    var result: Result
)

data class Result (
    var srcLangType: String = "",
    var tarLangType: String = "",
    var translatedText: String = ""
)


POST 방식으로 사용할때에는 @FormUrlEncoded 어노테이션과 함께 @Field로 요청 변수를 넣어준다.

// NaverAPI.kt

@FormUrlEncoded
@POST("v1/papago/n2mt")
fun transferPapago(
    @Header("X-Naver-Client-Id") clientId: String,
    @Header("X-Naver-Client-Secret") clientSecret: String,
    @Field("source") source: String,
    @Field("target") target: String,
    @Field("text") text: String
): Call<ResultTransferPapago>


그리고 뷰에서 호출한다.

// MainActivity.kt

val callPostTransferPapago = api.transferPapago(CLIENT_ID, CLIENT_SECRET, 
        "ko", "en", "테스트입니다. 이거 번역해주세요.")

callPostTransferPapago.enqueue(object : Callback<ResultTransferPapago> {
    override fun onResponse(
        call: Call<ResultTransferPapago>,
        response: Response<ResultTransferPapago>
    ) {
        Log.d(TAG, "성공 : ${response.raw()}")
    }

    override fun onFailure(call: Call<ResultTransferPapago>, t: Throwable) {
        Log.d(TAG, "실패 : $t")
    }
})



Retrofit + OkHttp 함께 사용하기

Retrofit을 OkHttp와 함께 사용해보자. 위에서는 액티비티마다 레트로핏 객체를 생성하고, API의 각 함수마다 헤더 값을 달아주어야 했다. 이런 코드를 조금 더 정리해서 간결하게 사용해 볼 것이다.

create() 함수를 Activity에서 Interface로 옮겼다.

// NaverAPI.kt

companion object {
    private const val BASE_URL_NAVER_API = "https://openapi.naver.com/"
    private const val CLIENT_ID = "네이버_개발자센터_아이디"
    private const val CLIENT_SECRET = "네이버_개발자센터_비밀번호"

    fun create(): NaverAPI {
        return Retrofit.Builder()
            .baseUrl(BASE_URL_NAVER_API)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(NaverAPI::class.java)
    }
}


여기에, 로그 기록에 사용할 HttpLoggingInterceptor와 고정 헤더 값에 사용할 Interceptor을 만들어준다. 두 개의 Interceptor를 클라이언트에 추가한 후, 다시 이 클라이언트를 레트로핏을 빌드할 때 추가해준다. create 할 때 헤더를 고정으로 넣어주므로 호출할 때에 사용하던 헤더는 제거해준다.

// NaverAPI.kt

interface NaverAPI {
    @GET("v1/search/news.json")
    fun getSearchNews(
        @Query("query") query: String,
        @Query("display") display: Int? = null,
        @Query("start") start: Int? = null
    ): Call<ResultGetSearchNews>

    @FormUrlEncoded
    @POST("v1/papago/n2mt")
    fun transferPapago(
        @Field("source") source: String,
        @Field("target") target: String,
        @Field("text") text: String
    ): Call<ResultTransferPapago>

    companion object {
        private const val BASE_URL_NAVER_API = "https://openapi.naver.com/"
        private const val CLIENT_ID = "네이버_개발자센터_아이디"
        private const val CLIENT_SECRET = "네이버_개발자센터_비밀번호"

        fun create(): NaverAPI {
            val httpLoggingInterceptor = HttpLoggingInterceptor()
            httpLoggingInterceptor.level = HttpLoggingInterceptor.Level.BODY

            val headerInterceptor = Interceptor {
                val request = it.request()
                    .newBuilder()
                    .addHeader("X-Naver-Client-Id", CLIENT_ID)
                    .addHeader("X-Naver-Client-Secret", CLIENT_SECRET)
                    .build()
                return@Interceptor it.proceed(request)
            }

            val client = OkHttpClient.Builder()
                .addInterceptor(headerInterceptor)
                .addInterceptor(httpLoggingInterceptor)
                .build()

            return Retrofit.Builder()
                .baseUrl(BASE_URL_NAVER_API)
                .client(client)
                .addConverterFactory(GsonConverterFactory.create())
                .build()
                .create(NaverAPI::class.java)
        }
    }
}





액티비티에서는 객체 생성 및 사용이 더 간편해진다.

// MainActivity.kt

val api = NaverAPI.create()

api.getSearchNews("테스트").enqueue(object : Callback<ResultGetSearchNews> {
    override fun onResponse(
        call: Call<ResultGetSearchNews>,
        response: Response<ResultGetSearchNews>
    ) {
        // 성공
    }

    override fun onFailure(call: Call<ResultGetSearchNews>, t: Throwable) {
        // 실패
    }
})


References

  • https://github.com/DNights/RestAPISample
  • https://stackoverflow.com/questions/21398598/how-to-post-raw-whole-json-in-the-body-of-a-retrofit-request
  • https://stackoverflow.com/questions/48151124/what-is-the-difference-between-retrofit-synchronous-and-asynchronous-request-wh