Retrofit 파헤치기

안석주·2021년 9월 15일
3

Retrofit이란?

Retrofit의 공식 홈페이지를 보면 이렇게 설명되어 있습니다.

Retrofit은 Android 와 Java를 위한 type-safety한 HTTP Client입니다.
Retrofit을 이용해 HTTP API를 Java Interface로 변환할 수 있습니다(물론 컨버터도 필요합니다).

Retrofit을 이용한 GitHub Api 이용하기

public interface GitHubService {
  @GET("users/{user}/repos")
  Call<List<Repo>> listRepos(@Path("user") String user);
}

Retrofit class는 Github Interface의 구현을 생성(create)해줍니다.

Retrofit retrofit = new Retrofit.Builder()
	.baseUrl("https://api.github.com/")
    	.build();
        
GitHubService service = retrofit.create(GitHubService.class);

위의 예제는 Github의 api를 이용하는 예제입니다.

생성된 GitHubService에서 각각의 호출(Call)은 원격 웹 서버에 동기 또는 비동기로 HTTP Request를 할 수 있습니다. 여기서 HTTP Request의 종류는 여러가지가 있지만, 대표적으로 가장 많이 사용되는 CRUD(Create, Read, Update, Delete)는 POST, GET, PUT, DELETE가 있습니다.

Call<List<Repo>> repos = service.listRepos("octocat");

위에 GitHubService에서 만든 listRepos라는 함수를 통해 octocat이라는 user의 정보를 담아 repos의 정보를 받아옵니다. listRepos의 인자인 "octocat"은 @GET에서 {user}부분에 들어가 데이터를 요청하게 됩니다. 즉, https://api.github.com/users/octocat/repos에 GET 요청을 통해 값을 받아옵니다.
이처럼 HTTP Request는 어노테이션(@)을 사용합니다!

Retrofit은 위에 말했던 것처럼 URL 매개변수 교체 및 쿼리 매개변수를 지원합니다.
또한 객체를 Request body형식으로 변환해줍니다! (JSON, Protocol buffers)

여기까지 Retrofit 객체를 만들어 사용하는 방법을 간단하게 알아봤습니다.
그렇다면 이번에는 위의 Interface에 들어가는 어노테이션들에 대해서 알아보겠습니다!

Interface Annotations

Retrofit을 이용하기 위해 만든 Interface(위의 예제에서는 GitHubService)에서 메소드와 어노테이션을 통해서 요청이 처리되는 방법을 나타낼 수 있습니다. HTTP Request에는 HTTP, GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD 총 8개가 있습니다. 또한 URL에 쿼리 매개변수를 지정할 수도 있습니다.

@GET("users/list")
@GET("users/list?sort=desc")
  • GET : GET은 해당 리소스를 조회합니다. 리소스를 조회한 뒤 해당 도큐먼트에 대한 자세한 정보를 가져옵니다.
  • POST : POST를 통해 해당 URI를 요청하면, 리소스를 생성합니다. BODY에 전송할 데이터를 담을 수 있습니다!
  • PUT : PUT은 해당 리소스를 전체 수정합니다. BODY에 전송할 데이터를 담을 수 있습니다!
  • PATCH : PATCH 또한 리소스 수정이지만, PATCH는 일부만 변경합니다!
  • DELETE : DELETE는 리소스를 삭제합니다.
  • HEAD : 헤더 정보만 요청합니다.
  • OPTIONS : 특정 URL에 대해 지원되는 요청 메소드의 목록을 리턴, 요청 URL이 *인 경우 서버 전체를 의미합니다!

이제 각각의 사용법을 알아보겠습니다!

GET

GET은 URL에 데이터를 모두 담아 전송하며, 길이 제한이 있고 동일한 요청에 동일한 데이터를 응답하는, 단순 조회용입니다.
요청 URL은 {}블록과 그 안의 매개변수를 이용해 URL을 동적으로 업데이트 할 수 있습니다. @Path를 통해 사용 가능합니다.

@GET("group/{id}/users")
Call<List<User>> groupList(@Path("id") int groupId);

@Query 파라미터도 추가할 수 있습니다.

@GET("grout/{id}/users")
Call<List<User>> groupList(@Path("id") int groupId, @Query("sort") String sort);

쿼리를 이용하면, 특정 데이터만 가져올 수 있습니다. 쿼리문을 이용하면 ?와 함께 조건문이 생깁니다.
예를들어 이름이 harry인 데이터만 가져오고 싶다면, Request가 이런식으로 구성됩니다.
/students?name=harry. 이런식으로 특정 조건이 필요하다면 @Qurey를 이용합니다.

또한 복잡한 쿼리 매개변수의 조합을 할 경우 맵을 사용할 수 있습니다(userId = 10, groupId = 20인 다중 쿼리를 만들고 싶을 때 사용)

@GET("grout/{id}/users")
Call<List<User>> groupList(@Path("id") int groupId,
			   @QueryMap Map<Int, Int> options);

이처럼 GET을 통해 리소스를 조회하고, 가져올 수 있습니다.
QueryMap을 실제 사용하는 것은 영상을 참고해보세요!

POST

POST는 Body에 전송할 데이터를 담아 서버에 생성합니다.
@Body를 이용해 HTTP 요청의 본문으로 사용할 객체를 지정해줄 수 있습니다!

@POST("users/new")
Call<User> createUser(@Body User user);

또한 객체는 Retrofit 인스턴스에 저장된 컨버터를 통해 변환되며, 컨버터를 따로 추가하지 않으면 RequestBody만 사용할 수 있습니다.

Multipart data and Form-encoded

multipart란 하나의 Body에 두 종류 이상의 데이터를 넣어주어야 해서 생긴 것인데, 예를 들어 post를 이용해 파일을 업로드할 때, 사진의 jpeg input과 사진 설명의 input이 있을 때, 다른 종류의 input을 하나의 HTTP Request Body에 넣어주면서, 구분해줄 수 있는 방법이 필요해졌습니다. 그래서 나온 것이 @Multipart입니다.

@Multipart
@Post("user/edit")
Call<User> updateUser(@Field("first_name") String first, @Field("last_name") String last);

Multipart의 parts는 Retrofit의 컨버터를 이용하거나, RequestBody를 구현하여 자체 직렬화를 처리할 수 있습니다.

Form-encoded는 @FormUrlEncoded를 이용해 사용이 가능합니다. 각각의 key-value 쌍은 @Field를 이용해 지정이 가능합니다.

@FormUrlEncoded
@POST("user/edit")
Call<User> updateUser(@Field("first_name") String first, @Field("last_name") String last);

Field와 Body의 차이점은?

Field는 인자를 form-urlencoded로 보낼 때 사용합니다. form-urlencoded는 key=value&key=value 형식으로 키 밸류형식으로 이용됩니다.

Body는 서버에서는 유일한 매개변수로 받고, 클라이언트에서 Java Object를 통째로 직렬화 해서 보낼 때 사용합니다. 즉 내가 만든 객체를 Json형식으로 서버로 보낼 때 이용 가능하며, PUT 이외에도 사용이 가능합니다.

@Headers를 이용해 메소드에 대한 정적 헤더를 설정해줄 수 있습니다.

@Headers("Cache-Control: max-age=640000")
@GET("widget/list")
Call<List<Widget>> widgetList();
@Headers({
    "Accept: application/vnd.github.v3.full+json",
    "User-Agent: Retrofit-Sample-App"
})
@GET("users/{username}")
Call<User> getUser(@Path("username") String username);

@Headers를 헤더를 설정해줄 수 있습니다.

또한 @Header를 이용하여 요청 헤더를 동적으로 업데이트할 수 있습니다.

Call<User> getUser(@Header("Authorization") String authorization)

보통은 이 기능을 이용해 헤더에 인증 ID와 SECRET_VALUE를 넣어줍니다.

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>
}

쿼리와 비슷하게 Map도 이용가능합니다!

Call<User> getUser(@HeaderMap Map<String, String> headers)

그런데 이런 식으로 모든 요청에 ID나 SECRET을 추가해야 하는 경우 매번 파라미터에 명시할 필요 없이 interceptor를 이용하면 더 편리하게 구현이 가능합니다.

PUT

@PUT은 데이터를 수정하는 역할로, 변경/수정을 의미합니다.
POST와 동일하게 @Body, @Field, @FieldMap으로 데이터 전송합니다.
POST와는 다른 점은 변경할 데이터를 선택해야합니다.

@Multipart
@PUT("user/photo")
Call<User> updateUser(@Part("photo") RequestBody photo, 
		      @Part("description") RequestBody description);

PUT에서 Multipart를 사용할 때 각 파트는 @Part로 이용합니다.

DELETE

@DELETE는 데이터를 삭제할 때 이용합니다. 반환 데이터는 없고 성공시에 응답코드 200으로 응답이 옵니다.

@DELETE("posts/{id}")
Call<Void> deletePost(@Path("id") int id);

ConverterAdapter

기본적으로 Retrofit은 HTTP 요청 본문을 Okhttp의 ResponseBody 형식과 @Body에 이용하는 RequestBody 타입만 역직렬화할 수 있습니다. 컨터버를 통해 이외의 형식들을 변환해줄 수 있습니다. 그 종류에는 Gson, Jackson, Moshi, Protobuf 등등이 있습니다.

Converter 또한 ConverterAdapter를 통해 사용자 지정 형식을 만들 수 있습니다.

val gson = GsonBuilder().serializeNulls().setDateFormat(DateFormat.LONG).create()
    
private val retrofit = Retrofit.Builder()
        .baseUrl(BASE_URL)
        .addConverterFactory(GsonConverterFactory.create(gson))
        .build()

위와 같이 작성해주면 Gson은 Date 형식의 데이터를 Long으로 바꿔주어 값을 반환해주게 됩니다.

Interceptor

먼저 알아두어야할 것이 있습니다! Retrofit은 내부적으로 OkHttp라는 Http 통신 라이브러리를 사용하여 동작합니다!

아래 사진을 보면 Okhttp에서 Interceptor가 어떤 것인지 보여줍니다! 윗부분이 우리가 사용하는 어플리케이션, 아랫부분이 서버라고 볼 수 있습니다. 일반 Interceptor는 어플리케이션 내에서 Okhttp 코어 사이의 요청/응답을 가로채는 역할을 하고, NetworkInterceptor는 실제 통신에서 서버와 OkHttp 코어 사이에서 요청/응답을 가로챕니다!

이처럼 Interceptor를 이용하면 서버의 JSON 응답을 별도의 파싱 없이 바로 사용할 수 있도록 커스텀할 수 있습니다.

private val client = OkHttpClient.Builder()
    .addNetworkInterceptor(commonNetworkInterceptor)
    .build()

private val retrofit = Retrofit.Builder()
    .baseUrl(BASE_URL)
    .client(client)
    .addConverterFactory(GsonConverterFactory.create())
    .build()

CallAdapter

위에서도 말했지만, Retrofit은 OkHttp를 이용하여 네트워크 요청을 실행합니다. 이 실행은 백그라운드 쓰레드에서 발생하며, OkHttp 클라이언트가 서버로부터 응답을 받으면 응답을 Retrofit으로 다시 전달해줍니다. 그런다음 Converter를 이용해 의미있는, 이용 가능한 Java 객체를 사용하여 사용 가능한 Response로 래핑합니다. 이는 모두 백그라운드 쓰레드에서 수행되며, 마지막으로 모든 것이 준비되면 Retrofit은 Android앱의 UI 쓰레드에 결과를 반환해야합니다. 이때 기본적으로 Call<TypedResponseClass>유형으로 수행됩니다. 결과를 받아 준비하는 백그라운드 쓰레드에서 안드로이드 UI 쓰레드로 돌아가는 동작이 CallAdapter 입니다.
CallAdapter는 제공되는 CallAdapter와 CallAdapter를 직접 커스텀하여 사용할 수도 있습니다. CallAdapter를 제대로 커스텀한다면 Retrofit의 로직을 완전히 변경할 수도 있습니다!
Retrofit에서 제공하는 기본적인 다중 호출 어댑터도 있습니다.

dependencies {  
    // Retrofit
    compile 'com.squareup.retrofit2:retrofit:2.5.0'

    // For example, add call adapter for RxJava 2
    compile 'com.squareup.retrofit2:adapter-rxjava2:2.5.0'

    // or alternatively:
    compile 'com.squareup.retrofit2:adapter-rxjava:2.5.0'
    compile 'com.squareup.retrofit2:adapter-guava:2.5.0'
    compile 'com.squareup.retrofit2:adapter-java8:2.5.0'
}

만약 RxJava2를 이용하는 CallAdapter를 쓴다고 가정해보겠습니다.

Retrofit retrofit =  
  new Retrofit.Builder()
      .baseUrl(apiBaseUrl)
      .addConverterFactory(GsonConverterFactory.create())
      .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
    .build()

Retrofit 객체를 생성시에 addCallAdapterFactory에 해당 CallAdapter에 맞는 메소드를 create해줍니다. 이를 통해 이 Retrofit 인스턴스는 이제 RxJava 유형의 응답 또한 래핑할 수 있습니다.

public interface GistService {  
    // OLD standard call adapter type
    @GET("gists")
    Call<List<Gist>> getGists();

    // NEW RxJava call adapter type
    @GET("gists")
    Observable<List<Gist>> getGists();
}

이전에 Call<List<Gist>>를 이용해 리턴 값을 정해놨다면, CallAdapter를 이용해 Observable<List<Gist>>를 리턴 값으로 받을 수 있게 되었습니다.
또한 한가지 CallAdapter만 이용할 수 있는 것도 아닙니다.

public interface GistService {  
    // access Gists with default call adapter
    @GET("gists") 
    Call<List<Gist>> getGists();

    // create new Gist with RxJava call adapter
    @POST("gists")
    Observable<ResponseBody> createGist(@Body Gist gist);
}

기본 CallAdapter, 그리고 동시에 RxJava를 지원하는 CallAdapter도 동시에 이용 가능합니다. 더 많이 필요하다면 addCallAdapterFactory()를 더 이용하면 됩니다!

커스텀 CallAdapter를 이용한다면, 자신만의 onResponse, onFailure를 지정할 수도 있습니다!

이후에 RxJava와 같이 사용하는 Retrofit을 통해 자세한 사용법을 볼 수 있습니다!

출처 및 참고

https://square.github.io/retrofit/
https://jaejong.tistory.com/38
https://junghyun100.github.io/Multipart_form-data/
https://medium.com/mj-studio/%EC%84%9C%EB%B2%84-%EC%9D%91%EB%8B%B5-cherry-pick-okhttp-interceptor-eaafa476dc4d
https://heepie.me/282
https://youngest-programming.tistory.com/135
https://futurestud.io/tutorials/retrofit-2-introduction-to-call-adapters

profile
뜻을 알고 코딩하기

0개의 댓글