[안드로이드] HTTP 통신을 위한 Retrofit 라이브러리

hee09·2021년 11월 25일
0
post-thumbnail

Retrofit

초창기 안드로이드는 HTTP 통신을 위해서 HTTPURLConnection 또는 Apache HTTP Client API를 사용하였습니다. 하지만 이 두 방법은 모두 단점이 많아서 Deprecated 되었고 OkHttp, Volley API, Retrofit, Ktor 등의 Third-Party 라이브러리가 나왔습니다.

Third-Party 라이브러리 중에서도 Sqaure에서 만든 Retrofit이 많이 사용되는 것 같습니다. 그래서 이번 글에서는 Retrofit 라이브러리의 사용법을 알아보겠습니다.

Retrofit 이용 준비 및 구조

Retrofit을 이용해 서버 연동을 하려면 Call 객체가 필요합니다. Call 객체는 실제 서버 연동을 실행하는 객체로 생각하면 되는데 이 Call 객체를 개발자가 직접 만들지 않고 Retrofit이 자동으로 만들어 준다는 개념입니다.

Retrofit의 기본 구조

위의 그림은 Retrofit의 동작 흐름입니다. 개발자가 네트워크를 위해 직접 작성해야 하는 것은 인터페이스뿐입니다.

  1. 인터페이스에 네트워킹 시 호출해야 할 함수들을 등록합니다. 단지 추상함수만 선언하면 되는 것으로 실제 네트워킹을 위한 코드는 작성되어 있지 않습니다.
  2. 이렇게 작성한 인터페이스를 Retrofit에 전달하면 Retrofit에서 인터페이스의 함수를 구현해 실제 네트워킹을 진행해 주는 Call 객체를 반환하는 Service 객체를 반환해줍니다.
    (여기서 서비스는 안드로이드 서비스에 해당 하지않고 Retrofit에서 용어로 서비스를 사용)
  3. Service 객체를 획득하여 실제 네트워킹이 필요한 순간 Service의 함수를 호출하여 Call 객체를 받고 Call 객체에게 일만 시키면 됩니다.

결국, 인터페이스를 이용하여 Service 객체를 획득한 후 네트워킹이 필요할 때 Call 객체만 획득해 이용하면 되는 구조입니다.

의존성 설정

Retrofit을 이용하기 위해서는 build.gradle 파일에 의존성 설정이 필요합니다. Retrofit만을 설정해도 되지만, 대부분 서버 연동 시 주고받는 데이터는 JSON, XML입니다. 이를 자동으로 파싱해 개발자가 만든 객체로 변환해주는 기능이 있는데 이를 컨버터(Convertor)라고 부릅니다. 이는 Retrofit에 내장되어 있지 않지만 Retrofit을 이용하면서 JSON, XML 데이터를 객체로 변환해주는 외부 라이브러리 연동을 지원하므로 이를 이용하면 됩니다.

Retrofit에서 사용할 수 있는 컨버터는 다양한데 그중에서도 많이 사용하는 것은 Gson과 Moshi입니다. 현재 글에서는 Gson을 사용해보도록 하겠습니다.

modlue수준의 build.gradle

// 작성일 기준 최신 버전
// Retrofit
implementation "com.squareup.retrofit2:retrofit:2.9.0"
// Gson Converter
implementation "com.squareup.retrofit2:converter-gson:2.9.0"

권한 설정

Retrofit도 서버 연동을 목적으로 하므로 AndroidManifest.xml 파일에 INTERNET 권한을 설정해야 합니다.

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

Retrofit을 이용한 HTTP 통신

Model 정의

Model은 서버 연동을 위한 데이터 추상화 클래스입니다. 위해서 말했듯이 JSON이나 XML 데이터를 개발자가 직접 파싱하지 않고 컨버터가 자동으로 객체를 생성한 후 변수에 파싱된 데이터를 담아 줍니다. 그러면 개발자는 이 객체에 담긴 데이터를 사용하면 되는 것입니다. Model을 만들어 보겠습니다.

예시 json 데이터

{
  "name":"test_json",
  "description": "테스트용",
  "id": 10,
  "user_date": "2021-11-14"
}

예시 모델 클래스

// data 클래스를 사용
data class Example(
    val name: String,
    val description: String,
    val id: Int,
    // 키값과 변수명이 다를 때 사용하는 어노테이션
    // 키가 user_date인 데이터의 값을 date 변수에 저장
    @SerializedName("user_date")
    val date: String
)

만약 위와 같은 json 있을 때 모델 클래스를 선언하면 컨버터는 변수명과 파싱된 데이터의 키값을 매핑해서 데이터를 저장합니다(name 키의 값인 test_json은 name 변수에 저장되는 방식). 만약 Json 데이터의 키값과 모델 클래스의 변수명이 다를 경우 @SerializedName이라는 어노테이션을 추가해서 다음처럼 변수에 할당할 데이터의 키값을 명시해줄 수 있습니다(예시에서는 json 데이터의 user_date 키와 model 클래스의 date 변수를 연결)

이런 Model 클래스는 직접 만들어도 되지만 자동으로 만들어주는 사이트가 존재합니다. json 데이터를 넣고 원하는 조건을 명시한 후 변경하면 자동으로 pojo(Plain Old Java Objects)로 만들어줍니다.

Retrofit 객체 생성

다음은 Retrofit 객체를 생성하는 과정입니다. Retrofit은 Builder 패턴으로 되어있어서Builder에 setter 함수를 사용하여 각종 정보를 설정하고 객체를 생성하면 됩니다.

싱글톤으로 Retrofit 객체 생성

object RetrofitFactory {
    // 기본적으로 적용되는 서버 URL
    private const val BASE_URL = "https://newsapi.org/"
    private var instance: Retrofit? = null

    fun getInstance(): Retrofit {
        if(instance == null) {
            instance = Retrofit.Builder() // Builder 패턴
                .baseUrl(BASE_URL)
                // 컨버터 지정(Gson Converter 사용)
                .addConverterFactory(GsonConverterFactory.create())
                .build()
        }

        return instance!!
    }
}

baseUrl() 메서드로 전달한 인자는 Retrofit을 이용할 때 기본으로 적용되는 서버 URL입니다. 이곳에 URL을 지정하면 실제 네트워킹 때는 이 URL 뒤에 이어지는 경로나 질의 문자열 등만 지정하면 됩니다. 참고로 baseUrl이 지정되어 있어도 실제 네트워킹 때는 @url 어노테이션을 사용해 다른 URL을 지정할 수 있습니다.

예를 들어서 아래와 같은 주소에 HTTP 통신을 한다고 가정하면

https://google.com/api/user
https://google.com/api/search
https://google.com/api/address

뒤에 user, search, address만 달라지므로 baseUrl의 인자는 https://google.com/이 되는 방식입니다.

addConverterFactory() 메서드는 서버와 통신할 데이터 타입에 맞는 컨버터를 지정하는 역할을 합니다. 위의 코드는 GsonConverFactory를 지정하여서 JSON 데이터를 Gson 라이브러리로 파싱하고 그 데이터를 Model에 자동으로 담아줍니다.


Service 인터페이스

Retrofit에서 가장 중요한 부분은 Service 인터페이스 선언이라고 할 수 있습니다. Retrofit의 핵심은 서버 네트워킹을 위한 함수를 인터페이스의 추상 함수로 만들고 그 함수에 어노테이션으로 GET/POST 등의 HTTP Method를 지정, 서버 전송 질의 지정 등을 하면 그 정보에 맞게 서버를 연동할 수 있는 Call 객체를 자동으로 만들어주는 구조입니다.

Service 인터페이스 선언

// Retrofit 인터페이스 
interface RetrofitService {
    // HTTP method는 GET 방식
    // baseUrl 뒤에 v2/everything를 붙인다
    @GET("v2/everything")
    // 추상 메서드 정의
    // @Query 어노테이션을 사용해 질의 문자열 지정
    // 반환값은 Call 객체(이 객체에 네트워킹을 시키는 구조)
    fun getList(@Query("q") q: String
                ,@Query("apiKey") apiKey: String
                ,@Query("page") page: Long
                ,@Query("pageSize") pageSize: Int): Call<PageListModel>
}

위의 코드는 Retrofit 인터페이스를 선언하고 추상 메서드를 만드는 코드입니다. 추상
메서드에 @GET 어노테이션은 해당 메서드는 HTTP 메서드 중 GET을 사용한다는 것을 나타내고
그 안의 인자는 baseUrl 뒤에 Path로 들어가는 것입니다. 그리고 메서드 안의 인자에 붙은 @Query 어노테이션은 질의 문자열을 지정하는 것입니다. 어노테이션에 대한 내용은 아래에 추가로 나와있습니다.

위와 같이 메서드를 선언하고 인자로 1,2,3,4를 준다고 가정하면 연동되는 URL(BASE_URL은 https://newsapi.org/라고 가정)은 https://newsapi.org/v2/everything?q=1&apiKey=2&page=3&pageSize=4이 됩니다.


Call 객체 획득

이제 Call 객체를 만들어주는 Retrofit Service 객체를 획득하면 됩니다.

Retrofit 객체 생성 후 RetrofitService 객체 획득

// Retrofit 객체 획득
val retrofit = RetrofitFactory.getInstance()
// Retrofit 객체에 Service 인터페이스 전달하여 RetrofitService 객체 획득
retrofitService = retrofit.create(RetrofitService::class.java)

Retrofit 객체의 create() 함수를 사용하면 네트워킹을 위한 Call 객체를 가지는 Retrofit Service 객체가 자동으로 생성됩니다. 인자로는 추상 메서드를 선언한 서비스 인터페이스를 전달하면 됩니다. 이렇게 얻은 Retrofit Service 객체를 이용해 인터페이스에 정의한 함수를 호출하면 Call 객체가 반환되는 구조입니다.

// Call 객체 획득
val call: Call<PageListModel>? = retrofitService?.getList(QUERY, API_KEY, 1, 2)

Call 객체의 제네릭 정보는 획득하고자 하는 데이터 타입으로 Model 클래스에 해당합니다.


네트워킹 시도

이제 실제 네트워킹이 이루어지게 획득한 Call 객체에게 일을 시키면 됩니다. 이때 enqueue() 메서드를 사용합니다.

// 네트워크 관련된 일이기에 작업 스레드에서 적용
Runnable {
    call?.enqueue(object : Callback<PageListModel> {
            // 서버에서 정상적으로 결과 받은 경우
            override fun onResponse(
                call: Call<PageListModel>,
                response: Response<PageListModel>
            ) {
                // 응답 성공
                if(response.isSuccessful) {
                    // RecyclerView Adapter 설정
                    val adapter = RecyclerAdapter(response.body()?.articles ?: emptyList(), this@MainActivity)
                    recyclerView.adapter = adapter
                    Log.d(TAG, "response - successful")
                } else {
                    // 에러가 발생 한 경우
                    Log.d(TAG, "response - ${response.errorBody()}")
                    Log.d(TAG, "response - ${response.code()}")
                }
            }

            // 서버 연동에 실패한 경우
            override fun onFailure(call: Call<PageListModel>, t: Throwable) {
                Log.d(TAG, "failure..")
            }

        })
}.run()

우선 네트워킹과 관련된 작업이기에 메인 스레드에서 작업하지 않고 작업 스레드에서 작업하기 위해 Runnable로 감싸고 run으로 실행시켰습니다.

Call 객체의 enqueue() 메서드를 호출하면서 Callback 클래스의 객체를 매개변수로 지정합니다. 그러면 네트워킹을 시도하고 서버에서 정상적으로 결과를 받으면 onResponse() 메서드가 자동으로 호출되고 함수의 매개변수로 결과가 전달됩니다. 서버 연동에 실패하면 onFailure() 메서드가 호출됩니다.

Rest Api 응답 코드
1. 100 시리즈 -> 일시적인 응답
2. 200 시리즈 -> 클라이언트는 요청을 수락하고 서버에서 성공적으로 처리
3. 300 시리즈 -> 이 시리즈와 관련된 대부분의 코드는 URL 리디렉션용
4. 400 시리즈 -> 클라이언트 측 오류에 해당
5. 500 시리즈 -> 서버 측 오류와 관련


Retrofit 어노테이션

Retrofit을 사용할 때 인터페이스의 추상 메서드에 적절한 어노테이션을 추가하는 부분이 중요합니다.
어노테이션을 잘 추가하면 네트워킹이 알아서 되므로 여기서 Retrofit에 사용하는 어노테이션에 대해 알아보겠습니다.


@GET, @POST, @PUT, @DELETE, @HEAD

HTTP Method를 지정할 때 사용합니다. @GET("users/list")로 지정하면 "https://example.api.server.com/users/list" URL의 서버를 GET 방식으로 호출하며, @POST("users/list")로 지정하면 "https://example.api.server.com/users/list" URL의 서버를 POST 방식으로 호출합니다.(https://example.api.server.com/은 baseUrl에 해당)
이때 서버에 전송해야 하는 데이터를 @Query 어노테이션으로 명시할 수 있지만, @GET("users/list?sort=desc")처럼 URL에 직접 명시할 수도 있습니다. 이렇게 하면 서버 연동 URL은 https://example.api.server.com/users/list?sort=desc가 됩니다.


@Path: URL의 path 부분 동적 할당

URL의 일부분이 동적 데이터에 의해 결정될 때가 있습니다. 이를 위해 @Path 어노테이션을 제공합니다. URL의 동적 값이 들어가는 부분을 중괄호 { }로 명시하고 이 위치에 들어갈 데이터를 @Path 어노테이션으로 명시하는 개념입니다.

  1. 인터페이스 함수 선언
@GET("/group/{id}/users")
fun groupList(@Path("id")): Call<List<UserModel>>
  1. 함수 호출
val list: Call<List<UserModel>> = retrofitService.groupList(10)
  1. URL
    https://example.api.server.com/group/10/users

@Path 어노테이션이 지정된 매개변수 값이 URL에서 중괄호 { }로 지정한 부분에 대입됩니다. 1번처럼 선언된 인터페이스 함수를 2번처럼 호출하면 실제 이용시 이용되는 URL은 3번에 해당합니다.


@QUERY: 질의 문자열로 지정해야 하는 데이터 명시

  1. 인터페이스의 함수 선언
@GET("group/{id}/users")
fun groupList(@Path("id") groupId: Int
              , @Query("sort") sort: String
              , @Query("name") name: String): Call<List<UserModel>>
  1. 함수 호출
val list: Call<List<UserModel>> = retrofitService.groupList(10, "date", "홍길동")
  1. URL
    https://example.api.server.com/group/10/users?sort=date&name=홍길동

@QueryMap: Map 객체로 질의 데이터 지정

  1. 인터페이스의 함수 선언
@GET("group/{id}/users")
fun groupList(@Path("id") groupId: Int
              ,@QueryMap options: Map<String, String>
              ,@Query("name") name: String): Call<List<UserModel>>
  1. 함수 호출
val map: Map<String, String> = HashMap()
map["phone"] = "01000000000"
map["address"] = "seoul"
val list: Call<List<UserModel>> = retrofitService.groupList(10, map, "홍길동")
  1. URL
    https://example.api.server.com/group/10/users?address=seoul&phone=01000000000&name=홍길동

@Body: 객체를 request body에 포함(POST 방식에서 사용)

Model의 @SerializedName 어노테이션에 명시된 키값으로 필드값을 JSON 문자열로 만들어 전송합니다. 만약 Model에 @SerializedName 어노테이션이 추가되어 있지 않으면 변수명이 키값이 됩니다.

  1. 인터페이스의 함수 선언
@POST("group/{id}/users")
fun groupList(@Path("id") groupId: Int
              ,@Body user: UserModel
              ,@Query("name") name: String): Call<UserModel>
  1. 함수 호출
val userModel = UserModel()
userModel.firstName = "길동"
userModel.lastName = "홍"
val user = retrofitService.groupList(10, userModel, "홍길동")
  1. URL
    https://example.api.server.com/group/10/users?name=홍길동

  2. 서버 전송 데이터
    {"first_name":"길동", "last_name":"홍"}

FormUrlEncoded: Form-encoded data(POST 방식에 사용)

@Field 어노테이션이 추가된 데이터를 인코딩해서 전송합니다. @Field 어노테이션은 @FormUrlEncoded 어노테이션이 추가되었을 때만 사용할 수 있습니다. @FormUrlEncoded 어노테이션은 @POST 어노테이션에만 사용할 수 있습니다.

  1. 인터페이스의 함수 선언
@FormUrlEncoded
@POST("user/edit")
fun groupList(@Field("first_name") first: String
              ,@Field("last_name") last: String
              ,@Query("name") name: String): Call<UserModel>
  1. 함수 호출
val user = retrofitService.groupList("길동", "홍", "honggildong")
  1. URL
    https://example.api.server.com/user/edit?name=honggildong

  2. 서버 전송 데이터
    first_name=길동%20%EA%B8%B8%EB%8F%99last_name=홍%20%ED%99%8D

@FormUrlEncoded, @Field 어노테이션을 이용할 때는 POST 방식으로 데이터를 전송하는데 Model 클래스를 이용하지 않고 개별 매개변수를 body로 전송하고자 할 때 사용합니다.


@FormUrlEncoded 방식은 객체에는 사용할 수 없으며 배열이나 리스트 데이터에 사용할 수 있습니다. 배열이나 리스트에 담긴 데이터가 같은 키로 전달됩니다.

  1. 인터페이스의 함수 선언
@FormUrlEncoded
@POST("tasks")
fun groupList(@Field("title") titles: List<String>): Call<UserModel>
  1. 함수 호출
val list: List<String> = ArrayList()
list.add("홍길동")
list.add("연개소문")
val user = retrofitService.groupList(list)
  1. URL
    https://example.api.server.com/tasks

  2. 서버 전송 데이터
    title=%ED%99%8D%EA%B8%B8%EB&title="EB%A5%98%ED%98%84%EC

@Headers: HTTP 요청 헤더 지정

HTTP 요청 헤더는 클라이언트와 서버가 요청 또는 응답으로 부가적인 정보를 전송할 수 있도록 해줍니다. HTTP 헤더는 대소문자를 구분하지 않는 이름과 콜론(:) 다음에 오는 값(줄 바꿈 없이)으로 이루어져있습니다.

지정할 헤더가 여러 개라면 아래와 같이 쉼표(,)를 기준으로 여러 값을 넣으면 됩니다.

@Headers(
    "Accept: application/vnd.github.v3.full+json",
    "User-Agent: Retrofit-Sample-App"
)
@GET("users/{username}")
fun getUser(@Path("username") username: String): Call<UserModel>

Url: baseURL과 상관없는 특정 URL 지정

  1. 인터페이스의 함수 선언
@GET
fun getList(@Url url: String): Call<UserModel>
  1. 함수 호출
val list: Call<UserModel> = retrofitService.getList("https://www.google.com")
  1. URL
    https://www.google.com/

결론

  • 안드로이드에서 HTTP 통신을 할 때, 안드로이드에서 기본 제공하는 HTTP 통신은 거의 사용하지 않습니다. Third-Party library를 많이 사용하는데 Retrofit, Volley, Ktor 등 많은 라이브러리가 있으므로 회사에서 사용하는 라이브러리 또는 개인적으로 사용한다면 입맛에 맞는 것을 사용하는 것이 좋을 것 같습니다.

  • 해당 예제에서는 Retrofit 서비스의 반환 타입으로 Call 객체를 사용하였지만 ReactiveX 라이브러리를 사용하면 Single, Completable 등의 타입으로 반환 할 수 있습니다. ReactiveX와 Retrofit을 사용하는 예제는 이후 MVVM 패턴의 예제 코드를 작성하면 링크를 걸어놓겠습니다.



예제 코드

참조
왜 Retrofit을 사용하는지
Retrofit 라이브러리 사이트
Retrofit 기본 사용법
How does Retrofit work
깡쌤의 안드로이드 프로그래밍

틀린 부분 댓글로 남겨주시면 수정하겠습니다..!!

profile
되새기기 위해 기록

0개의 댓글