Android 네트워크 : HTTP 통신 (Retrofit)

timothy jeong·2021년 11월 21일
0

Android with Kotlin

목록 보기
52/69

Retrofit 라이브러리

2015년 스퀘어에서 만든 HTTP 통신을 간편하게 만들어 주는 라이브러리이다.

Retrofit은 네트워크 통신 정보만 주면 그대로 네트워크 프로그매잉을 대신 구현해 준다. 통신에 필요한 인터페이스를 만들고 코드를 구현하지 않은 상태에서 Retrofit에게 인터페이스 정보를 알려주면 실제 통신할 때 필요한 코드를 담은 객체를 만들어준다.

이 객체의 함수는 우리가 인터페이스에서 정의해 둔 함수이다. 이 함수를 호출하면 Call 객체를 반환하는데, Call 객체는 enqueue() 함수를 호출하는 순간 통신을 수행한다.

implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'

첫번쨰 라이브러리를 제외하고는 다른 라이브러리를 이용할 수 있다. Retrofit은 JSON 이나 XML 테이더를 모델객체로 변환해 주는데, 이때 JSON, XML 을 파싱하는 라이브러리가 필요하다. 따라서 구글에서 만든 gson 과 gson 을 이용해서 모델 객체를 만들어주는 com.squareup.retrofit2:converter-gson:2.9.0 을 추가했다. 이처럼 만약 파싱 라이브러리가 바뀌면 converter 라이브러리도 그에 맞게 바꿔줘야한다.

Retrofit 은 아래와 같은 형식의 converter 라이브러리를 지원한다.

  • Gson
  • Jackson
  • Moshi
  • Protobuf
  • Wire
  • Simple XML
  • JAXB
  • Scalars

모델 클래스 선언

서버와 주고받는 데이터를 표현하는 클래스이다. Reftrofit은 JSON, XML 데이터를 파싱해 모델 클래스 객체에 담는 것을 자동화 해준다. 데이터를 담을 모델 클래스를 선언하고 클래스 정보만 알려 주면 모델 클래스의 객체를 알아서 생성하고 그 객체에 데이터를 담아 준다. 모델 클래스는 상위 타입에 제약이 없으므로 어떤 클래스를 상속 받거나 인터페이슬 구현할 필요가 없다.

data class UserModel(
    var id: String,
    @SerializedName("first_name")
    var firstrName: String,
    var lastName: String,
    var avatar: String,
    var avatarBitmap: Bitmap
)

데이터의 키와 프로퍼티 이름이 자동으로 매칭된다. 만약 키와 프로퍼티의 이름이 다를때는 @SerializedName 에노테이션에 명시해주면 된다. 키의 언더바 다음 문자를 대문자로 바꿨을때에는 굳이 명시하지 않아도 된다.

모델 클래스를 만들 때 서버의 데이터와 상관없는 프로퍼티를 선언해도 된다. 서버의 데이터가 복잡할 때는 모델 클래스를 여러 클래스로 분리한 후 조합해서 사용할 수 있다.

UserModel 데이터를 List 형태로 받아야 하는 경우 아래와 같이 모델 객체를 조합할 수 있다.

data class UserListModel (
    var page: String,
    var perPage: String,
    var total: String,
    var totalPages: String,
    var data: List<userModel>?
)

인터페이스 정의

Retrofit 을 이용할 때 가장 중요한 부분은 네트워크 통신이 필요한 순간에 호출할 함수를 포함하는 인터페이스를 작성하는 것이다.

interface INetworkService {
    @GET("api/users")
    fun doGetUserList(@Qurey("page") page: String): Call<UserListModel>
    
    @GET
    fun getAvatarImage(@Url url: String): Call<ResponseBody>

이 인터페이스에서 인터페이스 명과 함수명은 개발자가 임의로 정한 것이다. 실제 통신하는 클래스는 Retrofit이 자동으로 만들어주는데, 이때 에너테이션을 참조한다.

@GET 에너테이션은 HTTP 메서드를 의미하고, @Query는 서버에 전달되는 데이터, @Url 은 요청 Url 을 의미한다.

Retrofit 객체 생성

Retrofit을 사용할 때 가장 먼저 Retrofit 객체를 생성하는 코드를 실행해야 한다. Retrofit 객체를 생성하는 코드는 초기 설정을 담당하므로 한 번만 생성하면 된다.

val retrofit: Retrofit
    get() = Retrofit.Builder()
        .baseUrl("http://localhost:8080/")
        .addConverterFactory(GsonConverterFactory.create())
        .build()

.baseUrl() 함수로 url 을 설정하고, 함수에서는 그 뒤에 올 경로만 지정해서 서버와 연동할 수 있다. 그리고 addConverterFactory() 함수를 통해 데이터를 파싱해 모델 객체에 담는 역할자를 지정해준다.

인터페이스 타입의 서비스 객체 얻기

Retrofit 객체를 생성한 다음에는 이 객체로 서비스 인터페이스를 구현한 클래스의 객체를 얻는다.

var networkService: INetworkService = retrofit.create(INetworkService::class.java)

Retrofit의 create() 함수에 앞에서 만든 서비스 인터페이스 타입을 전달한다. 그러면 이 인터페이스를 구현한 클래스의 객체를 반환해준다. 실제 네트워크가 필요할 때 이 객체의 함수를 호출하면 된다.

네트워크 통신 시도

이제 네트워크 통신이 필요한 순간에 Retrofit 객체로 얻은 객체의 함수를 호출만 해주면 된다. 이 클래스는 Retrofit 이 만들어주지만 우리가 만든 인터페이스를 구현한 클래스이므로 인터페이스의 함수를 호출하면 네트워크 통신을 시도한다.

// call 얻기
val userListCall = networkService.doGetUserList("1")

이렇게 Call 을 얻고, 실제 통신은 이 Call 객체의 enqueeu() 함수를 호출하는 순간 이뤄진다.

userListCall.enqueue(object : Callback<UserListModel> {
    override fun onResponse(call: Call<UserListModel>,
                            response: Response<UserListMode>) {
        val userList = response.body()
        ...
    }
    override fun onFailure(call: Call<UserListModel>, t: Throwable) {
        call.cancel()
    }
})

enqueue() 함수의 매개변수로 지정한 Callback 객체의 onResponse(), onFailure() 함수가 자동으로 호출된다. 만약 통신에 성공하면 onResponse() 이, 통신에 실패하면 onFailure() 이 호출된다.

통신에 성공해서 넘어온 데이터는 response.body() 함수를 통해 얻을 수 있다. Response 부분에 제네릭으로 선언한 클래스가 response.body() 부분에 담겨있다.

Retrofit 에너테이션

@GET, @POST, @PUT, @DELETE, @HEAD 등의 에너테이션은 HTTP 메서드를 지칭하며, 위의 @GET("api/users") 처럼 추가적인 URL 경로를 지정하여 사용할 수 있다. 또한 @GET("api/users?sort=desc") 처럼 ? 뒤에 데이터를 추가할 수 있다.

@Path

Url 경로를 동적으로 지정해야 할 때도 있다. 이러한 부분들은 중괄호 { } 로 감싸야 한다.

@GET("group/{id}/users/{name}")
fun test(
    @Path("id") userId: String,
    @Path("name") userName: String
): Call<UserModel>

@Query

경로에 ? 를 이용해 서버에 전달할 데이터를 지정할 수도 있지만, 함수의 매개변숫값을 서버에 전달하고 싶다면 @Query 에너테이션을 사용한다.

@GET("group/users")
fun test(
    @Query("sort") sort: String,
    @Query("name") name: String
): Call<UserModel>

val call: Call<UserModel> = networkService.test("age", "Tom")

이 경우 baseUrl/group/users?sort=age&name=Tom 이러한 요청을 보내게 된다.

@QueryMap

서버에 전송할 데이터가 많다면 함수의 매개변수를 여러개 넣어야해서 함수가 너무 길어진다.
이때는 @QueryMap 을 이용해 서버에 전송할 데이터를 Map 타입의 매개변수로 받으면 된다.

@Get("group/users")
fun test(
    @QueryMap options: Map<String, String>,
    @Qurey("name") name: String
): Call<UserModel>

val call: Call<UserModel> = networkService.test(mapOf<String, String>("one" to "hello", "two" to "world"), "Tom")

@Body

서버에 전송할 데이터를 모델 객체로 지정하고 싶다면(Post 에서 HTTP Body 에 넣어야 할 경우) @Body 에너테이션을 사용한다. @Body로 선언한 매개변수는 모델 객체 타입이며 이 객체의 프로퍼티명을 키로, 프로퍼티의 데이터를 값으로 해서 JSON 문자열을 만들어서 서버로 전송한다. 이때 JSON 문자열은 데이터 스트림으로 전송하므로 @Body 는 @GET 에서는 사용할 수 없으며 @POST 와 함꼐 사용해야 한다.

@POST("group/users")
fun text(
    @Body user: UserModel,
    @Query("name") name: String
): Call<UserModel>

val call : Call<UserModel> = networkService.text(
   UserModel(id = "1", firstName = "Sam", lastName = "Tom", avatar = "someUrl"), "Tom")

@FromUrlEncoded, @Field

@FromUrlEncoded 에너테이션은 데이터를 URL 인코딩 형태로 만들어 전송할 때 사용한다. 앞서 본 @Body 는 데이터를 JSON 으로 만들어 전송하지만, @FromUrlEncoded 는 서버 전송 데이터를 키-값 형태의 URL 인코딩으로 전송한다. @Field 에너테이션이 추가된 데이터를 인코딩해서 전송하며 POST 방식에서만 사용할 수 있다.

@FromUrlEncoded
@POST("user/eidt")
fun text(
    @Field("first_name") first: String?,
    @Field("last_name") last: String?,
    @Query("name") name: String?
): Call<UserModel>

val call : Call<UserModel> = networkService.test("sam", "Tom", "Tom")

이때 요청 URL 은 baseUrl/user/eidt?name=Tom 이지만, 데이터 스트림에는 first_name={인코딩된 어떤 문자}%99%last_name={...} 이런 데이터가 넘어간다.

@Field 에너테이션은 모델 객체에 사용할 수 없으며 데이터 여러 건을 한꺼번에 저장하고 싶다면 배열이나 List 객체를 이용해야 한다. 배열이나 List 객체에 @Field 에너테이션을 사용하면 데이터 여러 건을 같은 키로 서버에 전달할 수 있다.

@FromUrlEncoded
@POST("meal")
fun text(@Field("menu") menus: List<String>): Call<UserModel>

val list: MutableList<String> = ArrayList()
list.add("햇반")
list.add("참치")

val call : Call<UserModel> = networkService.test(list)

역시 데이터 스트림에서 키-값 형태로 전달된다.

서버 요청에서 헤더값을 조정하고 싶다면 @Header 에너테이션을 사용한다.

@Header("Cache-Control: max-age=640000")
@GET("widget/list")
fun text: Call<UserMode>

@Url

baseUrl 을 무시하고 전혀 다른 URL 을 지정하고 싶다면 @Url 에너테이션을 사용한다.

profile
개발자

0개의 댓글