티스토리 뷰

안드로이드 프로젝트를 하면서 API 통신이 필요할 때 일반적으로 Retrofit 라이브러리를 사용하는데, 그 이유는 Retrofit의 응답 속도가 빠르고 구현 방법이 간단하기 때문이다. 이번 포스팅에서는 이렇게 많이 사용되는 Retrofit의 장점과 사용법에 대해 알아보려고 한다.

 


 

Retrofit2 특징

Retrofit2는 Square사에서 만든 네트워킹 라이브러리로, 오래 전에 Retrofit이라는 라이브러리를 출시한 뒤 기존 라이브러리의 문제점을 개선해 현재의 Retrofit2가 만들어졌다. Retrofit2는 Retrofit의 업그레이드 된 버전이기 때문에 일반적으로 1과 2를 구분짓지 않고 Retrofit이라고 통용된다. 그렇기 때문에 포스팅 중에 Retrofit이라고 하는 것은 모두 Retrofit2를 의미하는 것이므로 혼동이 없길 바란다.

Retrofit2는 네트워크 요청을 처리하기 위해서 OkHttp를 사용하고 enqueue와 excute를 사용하여 동기, 비동기 처리를 지원한다. 또한 JSON에서 Java 객체로 변환 해 줄 JSON 변환기가 내장되어있지 않기 때문에 JSON 변환기 라이브러리에 대한 지원을 제공하고 있다.

 

Retrofit의 장점은 속도편의성가독성이 있다.

  • OkHttp에서는 사용 시 대개 Asynctask를 통해 비동기로 실행하는데, Asynctask가 성능상 느리다는 이슈가 있었다. Retrofit에서는 Asynctask를 사용하지 않고 자체적인 비동기 실행과 스레드 관리를 통해 속도를 훨씬 빠르게 끌어올렸다.

  • OkHttp에서도 쿼리스트링, request, response 설정 등 반복적인 작업이 필요한데, Retrofit에서는 이런 과정을 모두 라이브러리에 넘겨서 처리하도록 하였다. 따라서 사용자는 함수 호출시에 파라미터만 넘기면 되기 때문에 작업량이 훨씬 줄어들고 사용하기 편리하다.

  • 인터페이스 내에 annotation을 사용하여 호출할 함수를 파라미터와 함께 정의해놓고, 네트워크 통신이 필요한 순간에 구현없이 해당 함수를 호출하기만 하면 통신이 이루어지기에 코드를 읽기가 매우 편하다. Asynctask를 쓰지 않기에 불필요하게 코드가 길어질 필요도 없으며, 콜백 함수를 통해 결과가 넘어오도록 되어있어 매우 직관적인 설계가 가능하다.

 

Retrofit2 사용법

이제 Retrofit2를 사용하는 방법을 알아보자.

설명을 위해 실제로 이용했던 Naver API인 Reverse Geocoding을 예시로 사용하겠다.

(실제로 해당 라이브러리를 사용하기 위해서는 애플리케이션 등록을 통해 Client ID와 Secret Key를 발급 받는 등 추가적인 작업이 필요하지만 본 포스팅의 주제와 벗어나기 때문에 다루지 않겠다.)

 

 

1. 라이브러리 추가 & INTERNET 권한 추가

build.gradle (:app) 파일에 라이브러리를 추가한다.

dependencies {
    ...
    
    // Retrofit, Gson, RxAdapter
    implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
    implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
    implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofit_version"
    
    // okhttp
    implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
    implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
}

 

아래 라이브러리들 중에 필요한 라이브러리를 선택적으로 추가하면 된다.

  • retrofit: Retrofit 라이브러리

  • converter-gson: Json데이터를 사용자가 정의한 Java 객채로 변환해주는 라이브러리

  • adapter-rxjava2: Retrofit을 Rx형태로 사용하도록 설정해주는 어댑터 라이브러리

  • okhttp, logging-intercepter: Retrofit을 사용해 받는 HTTP데이터들을 로그상으로 확인하기 위해 사용하는 라이브러리

 

AndroidManifest.xml 파일에 INTERNET 권한을 추가한다.

<uses-permission android:name="android.permission.INTERNET" />

 

 

2. 응답 데이터 정의

API 문서를 확인해 응답 데이터 클래스를 정의한다.

응답 JSON 데이터가 다음과 같을 때 응답 데이터 클래스를 만들어보자.

{
  "status":{
    "code":0,
    "name":"ok",
    "message":"done"
  },
  "results":[
    {
      "name":"legalcode",
      "code":{
        "id":"4113510300",
        "type":"L",
        "mappingId":"02135103"
      },
      "region":{
        ...
      }
    },
    {
      "name":"admcode",
      "code":{
        "id":"4113555000",
        "type":"A",
        "mappingId":"02135550"
      },
      "region":{
        ...
      }
    }
  ]
}

 

아래 코드는 응답 데이터 클래스로, JSON 응답 데이터에서 모든 정보를 필드로 정의할 필요 없이 필요한 정보만 정의해 사용할 수 있다.

data class GeoResponse (
    @SerializedName("results")
    val results: List<ResultResponse>
)

data class ResultResponse (
    @SerializedName("name")
    val convertName: String,
    
    @SerializedName("region")
    val region: RegionResponse
)

 

Gson을 사용할 때 클래스 필드에 @SerializedName 어노테이션을 붙여야 한다.

 

이 어노테이션의 역할은 Gson이 JSON 키를 Java 필드에 매핑하는 것을 도와주는 것인데, 이를 사용하지 않으면 클래스 멤버 변수와 JSON 키의 이름이 동일해야 매핑이 된다. 하지만 @SerializedName를 사용하면 JSON 키와 다른 이름의 변수명을 사용할 수 있다. 예를 들어, 위 예제에서처럼 @SerializedName을 사용해 JSON 키 "name"이 Java 필드 "convertName"에 매핑되어야 한다고 Gson에게 알려주어 "name"으로 들어왔지만 "convertName"으로 사용할 수 있는 것이다.

또한 JSON 키는 일반적으로 snake_case 규칙을 따르기 때문에 @SerializedName 어노테이션을 사용해 변수명에 대한 camelCase 규칙을 일관적으로 유지할 수 있다. 

 

하지만 이름의 변경이 없는 경우에도 @SerializedName 어노테이션을 붙이는 것이 좋다. 그 이유는 애플리케이션을 Release 할 때 소스 코드가 난독화 되는 과정에서 Java 필드가 변환되고, 이로 인해 Gson 매핑에 오작동이 일어날 수 있기 때문에 @SerializedName는 필수로 사용하는 것이 좋다고 한다.

 

 

3. API 인터페이스 정의

필요한 Data Class를 만들었다면 이제 API 통신을 위한 인터페이스를 정의해야 한다.

interface GeoService {

    @GET("gc")
    fun getAddress(
        @Query("coord") coord: String,
        @Query("output") output: String = "json"
    ): Call<GeoResponse>

}

 

Retrofit에서는 annotation으로 GET, POST, PUT, PATCH, DELETE와 같은 HTTP Request를 처리하는데 사용 할 메서드를 포함하는 인터페이스를 만들면 통신을 위한 메서드 내부가 구현된 클래스를 만들어서 통신한다. 위 예제는 GET 방식으로 통신하는 코드이다. GET안에 파라미터는 Base Url 뒤에 오는 주소를 넣어주면 "Base Url + 파라미터 주소"로 요청 url이 만들어진다. 이는 다른 Method Annotation에도 마찬가지로 적용된다. 문법에 대한 자세한 설명은 공식 문서에 잘 나와있다.

 

여기서 API 토큰 인증을 위해  Header에 Client ID와 Secret Key를 함께 보내야 하는 경우에는 다음과 같이 @Header 어노테이션으로 추가할 수 있다. 하지만 이처럼 코드에 이러한 key를 노출시키는 것은 좋지 않다. 이를 위해 [안드로이드] Properties 값 Java/xml 파일에서 읽어오기를 참고해 개선할 수 있다.

interface GeoService {

    @Headers(
        "X-NCP-APIGW-API-KEY-ID: (발급 받은 Client ID)",
        "X-NCP-APIGW-API-KEY: (발급 받은 Secret Key)"
    )
    @GET("gc")
    fun getAddress(
        @Query("coord") coord: String,
        @Query("output") output: String = "json"
    ): Call<GeoResponse>

}

 

 

4. Retrofit Client 생성

이제 통신을 위한 Retrofit Client를 만들어 보자. 

object RetrofitClient {
    val service: GeoService = initService()

    private fun initService(): GeoService =
        Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(GeoService::class.java)
    }
}

 

여기서 Retrofit Builder의 create의 인자로 API 인터페이스인 GeoService를 넣어주면 해당 인터페이스를 구현한 클래스가 생성되어 그 클래스로 통신이 이루어진다.

 

 

5. Request 전송

이제 통신을 위한 모든 준비가 끝났으니, 요청을 보내보자.

fun requestAddr(
    x: Double, y: Double,
    success:(response: GeoResponse?) -> Unit,
    failure:(String, String) -> Unit
) {
    RetrofitClient.service.getAddress("$x,$y")
        .enqueue(object: Callback<GeoResponse> {
            override fun onResponse(call: Call<GeoResponse>, response: Response<GeoResponse>) {
                if (response.isSuccessful)
                    success(response.body())
                else
                    failure(TAG, "request failure: ${response.message()}")
            }

            override fun onFailure(call: Call<GeoResponse>, t: Throwable) {
                failure(TAG, "throwable: ${t.message}")
            }
        })
}

 

위 함수는 x, y 좌표를 받아 GeoService의 메서드인 getAddress()를 호출해 Retrofit으로 통신하고 응답을 받는 함수이다.

Retrofit은 enqueue()를 이용해 비동기로 통신을 할 수 있고, execute()로 동기적으로도 통신을 할 수 있다.

비동기로 통신을 할 경우에는 위와 같이 enqueue()의 인자로 응답이 왔을 때 동작하는 콜백 함수를 지정해 주면 되고, 동기로 통신을 할 경우에는 execute()의 반환 값으로 응답 데이터가 반환된다. 위 예제의 경우는 Response<GeoResponse>가 반환된다.

 

 

이렇게 Retrofit으로 통신하는 기본적인 방법은 끝이 났다. 여기까지만 알아도 Retrofit을 사용하는 데 문제 없지만, 이 포스팅에서는 모든 API 요청에 인증 토큰을 함께 보내야 하는 경우 이용하는 OkHttp Interceptor에 대해 추가로 다룰 것이다.

 

앞의 예제처럼 요청이 적을 때는 @Header 어노테이션을 이용하여 직접 추가할 수 있지만, Http 요청이 많아지고 요청마다 토큰 인증이 필요하다면 요청을 추가 할때마다 지속적으로 매개변수를 추가해주어야 하는 문제가 발생한다. 그렇기 때문에 모든 Http요청에 헤더를 추가하기 위해서는 OkHttp Interceptor를 이용해야 한다.

 

먼저 HeaderInterceptor를 만들어 아래와 같이 intercept 메서드를 오버라이드 한다. 여기서 기존 Request에서 addHeader(key, value)로 요청에 필요한 Header를 추가한 것을 알 수 있다.

class HeaderInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val origin = chain.request()
        val request = origin.newBuilder()
            .addHeader("X-NCP-APIGW-API-KEY-ID", "발급 받은 Client ID")
            .addHeader("X-NCP-APIGW-API-KEY", "발급 받은 Secret Key")
            .build()

        return chain.proceed(request)
    }
}

 

RetrofitClient에서 service를 만드는 부분에서 이 HeaderInterceptor 클래스를 아래와 같이 설정해 줘야 한다.

object RetrofitClient {
    val service: GeoService = initService()

    private fun initService(): GeoService {
        val okHttpClient = OkHttpClient.Builder()
            .addInterceptor(HeaderInterceptor())
            .build()

        Retrofit.Builder()
            .baseUrl(BASE_URL)
            .client(okHttpClient)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(GeoService::class.java)
    }
}

 

이렇게 했다면 API Interface의 메서드에 @Header 어노테이션이 없더라도 HeaderInterceptor에 지정한 Header가 요청과 함께 넘어가게 된다.

 

 

 

 

 

[참고한 사이트]

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함