본문 바로가기
깡샘 코틀린

18-2 HTTP 통신하기

by 농농씨 2023. 7. 13.

앱에서 네트워크 통신을 구현하려면 우선 매니페스트 파일에 다음처럼 퍼미션을 선언해야 한다.

// 인터넷 퍼미션 선언
<uses-permission android:name="android.permission.INTERNET">

 

안드로이드 앱은 네트워크 통신을 할 때 기본으로 HTTPS 보안 프로토콜을 사용한다.

만약 일반 HTTPS 프로토콜로 통신하려면 특정 도메인만 허용하도록 선언해 줘야 한다. res/xml 폴더에 임의의 이름으로 XML 파일을 만들고 다음처럼 작성한다.

// HTTP 통신 허용
<network-security-config> // 임의의 XML 파일?
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">xxx.xxx.xxx.xxx</domain>
        // xxx.xxx 부분은 HTTP 프로토콜로 접속을 허용할 IP나 도메인
    </domain-config>
</network-security-config>

<domain> 태그에 HTTP 통신을 허용할 서버의 IP나 도메인을 작성한다. 이렇게 작성한 XML 파일을 매니페스트의 <application> 태그에 networkSecurityConfig 속성으로 알려주면 해당 도메인에 한해서만 HTTP 통신을 할 수 있다.

// 매니페스트에 HTTP 허용 XML 등록
<application
    (... 생략 ...)
    android:networkSecurityConfig="@xml/network_security_config">
    // application 태그에 networkSecurityConfig 속성으로 해당 XML 파일 알려줌

또는 매니페스트에서 usesCleartextTraffic 속성을 true로 설정하면 앱 전체에서 모든 도메인의 서버와 HTTP 통신을 할 수 있다.

// 모든 HTTP 통신 허용
<application
    (... 생략 ...)
    android:usesCleartextTraffic="true" /> // 주목~

 

 

Volley 라이브러리

안드로이드 앱을 개발할 때 네트워크 프로그래밍을 돕는 라이브러리에는 여러 가지가 있지만, 이 책에서는 구글에서 제공하는 Volley스퀘어(Square)에서 제공하는 Retrofit을 이용하는 방법을 살펴볼 것이다.

Volley는 2013년 구글 IO 행사에서 공개된 라이브러리로, 안드로이드 앱에서 HTTP 통신을 좀 더 쉽게 구현하게 해준다. Volley를 사용하려면 빌드 그래들 파일의 dependencies 항목에 다음처럼 등록해야 한다.

// Volley 라이브러리 등록
implementation 'com.android.volldy:volley:1.2.1'

Volley에서 핵심 클래스는 RequestQueueXXXRequest이다.

  • RequestQueue: 서버 요청자
  • XXXRequest: XXX  타입의 결과를 받는 요청 정보

RequestQueue 객체서버에 요청을 보내는 역할을 하며 이때 서버 URL과 결과를 가져오는 콜백 등 다양한 정보XXXRequest 객체에 담아서 전송한다. 서버로부터 가져온 결과가 문자열이면 StringRequest 객체에 담아서 전송한다. 서버로부터 가져온 결과가 문자열이면 StringRequest를 이용하는 것처럼 데이터 타입에 따라 ImageRequest, JsonObjectRequest, JsonArrayRequest 등을 이용한다.

 

문자열 데이터 요청하기 - StringRequest

먼저 StringRequest를 이용하는 방법을 살펴보자. StringRequest는 서버에 문자열 데이터를 요청할 때 사용한다.

  • StringRequest(int method, String url, Response.Listener<String> listener, Response.ErrorListener errorListener)

StringRequest의 생성자에는 1️⃣HTTP 메서드, 2️⃣서버 URL 그리고 3️⃣서버로부터 결과를 받을 때 호출할 콜백과 4️⃣서버 연동에 실패할 때 호출할 콜백을 지정한다.

// 문자열 요청 정의
val stringRequest = StringRequest( // 서버에 문자열 데이터 요청하는 클래스를 구현한 객체 생성
    Request.Method.GET, // 첫번째 매개변수, HTTP 메서드
    url, // 두번째 매개변수, 서버 URL
    Response.Listener<String> { // 세번째 매개변수, 서버로부터 결과를 받을 때 호출할 콜백 지정
    // Response.Listener를 구현한 클래스의 객체
    // 이 객체의 onResponse() 함수가 자동으로 호출되며 매개변수로 서버의 데이터를 전달받음
        Log.d("kkang", "server data : $it")
    },
    Response.ErrorListener { error -> // 네번째 매개변수, 서버 연동에 실패할 때 호출할 콜백 지정
        Log.d("kkang", "error............$error")
    })

세 번째 매개변수가 서버로부터 결과 데이터를 받는 순간 호출할 콜백이며 Response.Listener를 구현한 클래스의 객체이다. 이 객체의 onResponse() 함수가 자동으로 호출되며 매개변수로 서버의 데이터를 전달받는다.

 

StringRequest에 담은 정보대로 서버에 요청을 보낼 때는 RequestQueue 객체를 이용한다.

// 서버에 요청하기
val queue = Volley.newRequestQueue(this) // 객체 얻고
queue.add(stringRequest) // 이 객체의 add() 함수에 아까 정보 담은 객체를 전달함

Volley.newRequestQueue(this)로 1️⃣RequestQueue 객체를 얻고 이 객체의 2️⃣add() 함수에 앞에서 정의한 RequestString 객체를 전달하면 서버에 요청을 보낸다.

 

만약 서버에 요청할 때 데이터를 함께 전달해야 한다면 Get 방식에서는 간단하게 URL 뒤에 추가하면 되지만, POST 방식에서는 StringRequest를 상속받은 클래스를 이용해야 한다.

// POST 방식으로 데이터 전송
val stringRequest = object : stringRequest( // StringRequest를 상속받은 클래스를 만들고
    Request.Method.POST,
    url,
    Response.Listener<String> {
        Log.d("kkang", "server data : $it")
    }
    Response.ErrorListener { error ->
        Log.d("kkang", "error............$error")
    }){
    override fun getParams(): MutableMap<String, String> {
    // getParams() 함수 재정의하여 작성하면 다옹으로 호출됨
    // 반환값인 MutableMap에 전달할 데이터 담으면 서버에 요청 보낼 때 알아서 함께 전송해줌
        return mutableMapOf<String, String>("one" to "hello", "two" to "world")
    }
}

StringRequest를 상속받은 클래스를 만들고 getParams() 함수를 재정의하여 작성하면 자동으로 호출된다. 이 함수의 반환값은 MutableMap인데 이 객체에 전달할 데이터를 담아서 반환하면 서버에 요청을 보낼 때 알아서 함께 전송해 준다.

 

 

이미지 데이터 요청하기 - ImageRequest

서버에 이미지를 요청할 때는 ImageRequest를 이용한다.

  • public ImageRequest(String url, Response.Listener<Bitmap> listener, int maxWidth, int maxHeight, ScaleType scaleType, Config decodeConfig, @Nullable Response.ErrorListener errorListener)

ImageRequest의 생성자에 지정해야 하는 매개변수는 다음과 같다.

  • url: 서버 URL
  • listener: 결과를 가져오는 콜백
  • maxWidth, maxHeight: 지정한 값으로 이미지 크기 조절해서 전달. 만약 0으로 설정하면 크기 조절 없이 서버가 전달하는 이미지를 그대로 받음
  • scaleType: 영역에 맞게 이미지의 크기를 확대 또는 축소하는 스케일 타입
  • decodeConfig: 이미지 형식 지정
  • errorListener: 오류 콜백
// 이미지 요청 정의
val imageRequest = ImageRequest(
    url,
    Response.Listener { response -> binding.imageView.setImageBitmap(response) },
    // 두 번째 매개변수에 콜백 함수를 Bitmap 타입으로 지정해서 서버에서 가져온 이미지를 Bitmap 객체로 전달받음
    0,
    ImageView.ScaleType.CENTER_CROP,
    null,
    Response.ErrorListener {
        Log.d("kkang", "error............$error")
    })

val queue = Volley.newRequestQueue(this)
queue.add(imageRequest) // 이 이미지를 이미지 뷰에 출력함(??) 
// RequestQueue 객체의 add() 함수로 서버에 요청을 보낸다는게 출력한단건가

두 번째 매개변수에 콜백 함수를 Bitmap 타입으로 지정했으므로 서버에서 가져온 이미지를 Bitmap 객체로 전달받는다. 이 이미지를 이미지 뷰에 출력했다.

 

화면 출력용 이미지 데이터 요청하기 - NetworkImageView

만약 서버에서 가져온 이미지를 화면에 출력만 한다면 ImageRequest를 사용할 수도 있지만, NetworkImageView를 사용하면 조금 더 편리하다. NetworkImageView는 Volley에서 제공하는 이미지 출력용 뷰이다.

// 화면 출력용 이미지 데이터 요청하기
<com.android.volley.toolbox.NetworkImageView
    android:id="@id/networkImageView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />

1️⃣액티비티 화면을 구성하는 레이아웃 XML에 <com.android.volley.toolbox.NetworkImageView>를 작성하고 2️⃣이 객체의 setImageUrl() 함수를 호출하면 서버에서 이미지를 가져오는 통신부터 이미지를 NetworkImageView에 출력하는 것까지 자동으로 이뤄진다.

  • setImageUrl(String url, ImageLoader imageLoader)

setImageUrl() 함수만으로도 서버에 요청을 보낼 수 있다. 즉, RequestQueue의 add() 함수를 호출하지 않아도 서버와 자동으로 연동된다. 그 대신 이 함수의 두 번째 매개변수에 ImageLoader를 상속받은 하위 클래스의 객체를 지정해야 한다.

// setImageUrl() 함수로 요청하기
val queue = Volley.newRequestQueue(this) // RequestQueue 객체
val imgMap = HashMap<String, Bitmap>()
imageLoader = ImageLoader(queue, object : ImageLoader.ImageCache {
// ImageLoader 클래스의 객체의 매개변수에, 또 ImageLoader를 상속받은 하위 클래스를 지정함
    override fun getBitmap(url: String): Bitmap? { 
        return imgMap[url]
    }
    override fun putBitmap(url: String, bitmap: Bitmap) {
    // getBitmap() 함수가 널을 반환하여 서버로부터 이미지 가져오면 자동으로 호출되어
    // 서버 이미지를 putBitmap() 두 번째 매개변수로 전달해줌 ➡️ 결국 같은 URL의 이미지를 반복해서 가져오지 않도록 함
        imgMap[url] = Bitmap
    }
})
binding.networkImageView.setImageUrl(url, imageLoader)
// setImageUrl() 함수의 두 번째 매개변수에 ImageLoader를 상속받은 하위 클래스의 객체를 지정함
// (근데 하위클래스의 객체면 imageLoader가 아니라 object 아님?...)
// ImageLoader 객체를 setImageUrl() 함수의 두 번째 매개변수에 지정하면 
// 서버 이미지를 가져오기 전에 ImageLoader의 getBitmap() 함수가 자동으로 호출됨(?)

ImageLoader 객체를 setImageUrl() 함수의 두 번째 매개변수에 지정하면, 서버 이미지를 가져오기 전에 IamageLoader의 getBitmap() 함수가 자동으로 호출된다. 이 함수의 반환값이 이면 서버에 요청을 보내고 Bitmap 객체이면 요청을 보내지 않고 Bitmap 객체를 그대로 NetworkImageView에 출력한다.

또한 getBitmap() 함수가 널을 반환하여 서버로부터 이미지를 가져오면 putBitmap() 함수가 자동으로 호출되어 서버 이미지를 putBitmap() 두 번째 매개변수로 전달해 준다. 결국 같은 URL의 이미지를 반복해서 가져오지 않도록 한다.

 

*JSON 데이터 요청하기 - JsonObjectRequest

서버에 JSON 데이터를 요청할 때는 JsonObjectRequest 객체를 이용한다. 그러면 자동으로 JSON 데이터를 파싱한 JSONObject 객체가 콜백 함수에 전달된다.

더보기

*JSON이란?

JSON 데이터는 이름과 값의 쌍, key : value 형식으로 구성, 중괄호({})로 둘러쌓아 표현

참조: https://lxxyeon.tistory.com/153

  • JsonObjectRequest(int method, String url, JSONObject jsonRequest, Response.Listener<JSONObject> listener, REsponse.ErrorListener errorListener) 

서버에 요청할 때 전송해야 하는 데이터를 JsonObjectRequest() 함수의 세 번째 매개변수에 JSONObject로 지정할 수 있으며 널이면 서버에 전송할 데이터가 없다는 의미이다. 

// JSON 데이터 요청하기
val jsonRequest = 
    JsonObjectRequest(
        Request.Method.GET,
        url,
        null, // 서버에 요청할 때 전송해야 하는 데이터를 JSONObjectRequest() 세 번째 매개변수에 
        // JSONObject로 지정할 수 있음
        // null이면 서버에 전송할 데이터가 없다는 의미
        Response.Listener<JSONObject> { response -> // 네 번째 매개변수가 결과를 받는 콜백.
        // 서버로부터 넘어오는 JSON을 파싱한 JSONObject 객체가 전달됨
        // 이 객체의 게터함수를 이용해 가져올 데이터의 키값을 명시함
            val title = response.getString("title") // 키값이 title인 JSON 데이터를 가져옴
            val date = response.getString("date")
            Log.d("kkang", "$title, $date")
        }
        Response.ErrorListener { error -> Log.d("kkang", "error....$error")
    })
val queue = Volley.newRequestQueue(this)

이 코드는 서버로부터 다음의 JSON 데이터가 전달되었다는 가정 아래 작성한 코드이다.

{
    "title": "복수초",
    "date": "2021-01-01"
}

JsonObjectRequest의 네 번째 매개변수가 결과를 받는 콜백이며 서버로부터 넘어오는 JSON을 파싱하 JSONObject 객체가 전달된다. 이 객체의 게터 함수를 이용해 가져올 데이터의 키값을 명시하면 된다. 예를 들어 response.getString("title") 함수는 키값이 title인 JSON 데이터를 가져온다.

 

JSON 배열 요청하기 - JsonArrayRequest

서버에 JSON 배열을 요청할 때는 JsonArrayRequest를 이용한다.

  • JsonArrayRequest(String url, JSONArray jsonRequest, Response.Listener<JSONArray> listener, Response.ErrorListener errorListener)
// JSON 배열 요청하기
val jsonArrayRequest = jsonArrayRequest(
    Request.Method.GET,
    url,
    null, // 세 번째 매개변수, 서버에 요청할 때 전송해야 하는 데이터. null이면 서버에 전송할 데이터가 없음.
    Response.Listener<JSONArray> { response -> // 결과 받는 콜백
        for (i in 0 until response.length()) {
            val jsonObject = response[i] as JSONObject // 배열 요청함(아마두
            val title = jsonObject.getstring("title")
            val date = jsonObject.getString("date")
            Log.d("kkang", "$title, $date")
        }
    },
    Response.ErrorListener { error -> Log.d("kkang", "error....$error")}
)
val queue = Volley.newRequestQueue(this)
queue.add(jsonArrayRequest)

이 코드는 서버에서 다음과 같은 JSON 데이터가 전달된다는 가정 아래 작성했다.

[
  {
    "title": "복수초",
    "date": "2021-01-01"
  },
  {
    "title": "구절초",
    "date": "2021-01-02"
  }
]

 

 

Retrofit 라이브러리

Retrofit스퀘어에서 만든 HTTP 통신을 간편하게 만들어 주는 라이브러리이다(Volley도 안드로이드 앱에서 HTTP 통신을 좀 더 쉽게 구현하게 해주는 라이브러리이다). Retrofit은 1 버전과 2 버전이 있지만 Retrofit2가 2015년에 나왔으므로 지금 Retrofit이라고 하면 Retrofit2라고 봐도 무방하다. 따라서 이 책에서 소개하는 Retrofit은 Retrofit2를 의미한다.

 

Retrofit을 이용하려면 먼저 프로그램의 구조를 이해해야 한다. Retrofit은 네트워크 통신 정보만 주면 그대로 네트워크 프로그래밍을 대신 구현해 준다.

(그림) 인터페이스 a(), b()가 Retrofit에 Input으로 들어가면 서비스 객체인 a(){}, b(){}가 만들어진다.(맞나?)

이 그림에서 인터페이스코틀린의 interface 키워드로 직접 만들어야 한다. 그리고 인터페이스의 함수통신할 때 필요하다. 즉, a(), b() 함수를 호출해서 통신하겠다는 의미이다. 그런데 인터페이스에는 함수를 선언만 하며 통신할 때 필요한 어떤 코드도 담지 않는다.

 

이렇게 만든 인터페이스를 Retrofit에 알려주면 인터페이스 정보를 보고 실제 통신할 때 필요한 코드를 담은 서비스 객체를 만들어 준다. 여기서 서비스는 안드로이드 컴포넌트가 아니라 통신을 하게 해준다는 의미의 서비스이다.

Retrofit은 우리가 알려 준 인터페이스를 바탕으로 서비스를 만들므로 인터페이스에 선언한 함수를 그대로 포함한다. 이 서비스의 함수를 호출하면 Call 객체를 반환하는데 이 Call 객체의 enqueue() 함수를 호출하는 순간 통신을 수행한다.

지금까지 설명한 Retrofit 동작 방식을 정리하면 다음과 같다.

  1. 통신용 함수를 선언한 인터페이스를 작성한다.
  2. Retrofit에 인터페이스를 전달한다.
  3. Retrofit이 통신용 서비스 객체를 반환한다.
  4. 서비스의 통신용 함수(ex. a(), b())를 호출한 후 Call 객체를 반환한다.
  5. Call 객체의 enqueue() 함수를 호출하여 네트워크 통신을 수행한다.

 

라이브러리 선언

Retrofit을 이용하려면 빌드 그래들의 dependencies 항목에 다음처럼 라이브러리를 등록해야 한다.

// Retrofit2 사용 등록
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을 이용할 때 필수이며 나머지 2개는 다른 라이브러리를 이용할 수도 있다. Retrofit은 JSON이나 XML 데이터를 모델 (VO 클래스) 객체로 변환해 주는데 이때 JSON, XML을 파싱하는 라이브러리가 필요하다.(뭔말이지) 따라서 구글에서 만든 gson 라이브러리를 사용하고자 com.google.code.gson:gson을 등록했으며, Retrofit에서도 gson을 이용해 모델 객체로 변환해 주는 com.squareup.retrofit2:converter-gson을 함께 등록했다. 만약 파싱 라이브러리가 바뀌면 converter 라이브러리도 그에 맞게 바뀌어야 한다.

*파싱이란

:파싱 (Parsing : 구문분석) 은 하나의 프로그램을 런타임환경 (예를 들면, 브라우저 내 자바스크립트 엔진)이 실제로 행할 수 있는 내부 포맷으로 분석하고 변환하는 것을 의미합니다.

  • Gson: com.squareup.retrofit2:converter-gson
  • Jackson: com.squareup.retrofit2:converter-jackson
  • Moshi: com.squareup.retrofit2:converter-moshi
  • Protobuf: com.squareup.retrofit2:converter-protobuf
  • Wire: com.squareup.retrofit2:converter-wire
  • Simple XML: com.squareup.retrofit2:converter-simplexml
  • JAXB: com.squareup.retrofit2:converter-jaxb
  • Scalars(primitives, boxed, andstring): com.squareup.retrofit2:converter-scalars

 

모델 클래스 선언

모델 클래스서버와 주고 받는 데이터를 표현하는 클래스이다. 흔히 VO(value-object) 클래스라고도 하며 JSON, XML 데이터를 파싱해 모델 클래스 객체에 담는 것을 자동화 해준다. 만약 서버에서 넘어오는 JSON 데이터가 다음과 같다고 가정해 보자.

{
    "id": 7,
    "email": "michae.lawson@reqres.in",
    "first_name": "Michael",
    "last_name": "Lawson",
    "avatar": "https://reqres.in/img/faces/7-image.jpg"
}

원래는 JSON 데이터를 코드에서 직접 파싱해서 이용해야 하는데, 데이터를 담을 모델 클래스를 선언하고 클래스 정보만 알려주면 모델 클래스의 객체를 알아서 생성하고 그 객체에 데이터를 담아 준다. 모델 클래스는 상위 타입에 제약이 없으므로 어떤 클래스를 상속받거나 인터페이스를 구현할 필요는 없다. 순전히 개발자가 작성하는 클래스이다.

 

앞의 JSON 정보를 담을 모델 클래스는 다음처럼 작성할 수 있다.

// 모델 클래스
data class UserModel(
    var id: string,
    @Serializedname("first_name")
    var firstaName: String,
    // @SerializedName("last_name") // 주목~
    var lastname: String,
    var avatar: String,
    var avatarBitmap: Bitmap
)

모델 클래스의 프로퍼티에 데이터가 자동으로 저장되는 기본 규칙은 데이터의 키와 프로퍼티 이름을 매칭하는 것이다. 예를 들어 id라는 키값은 id 프로퍼티에 저장된다. 만약 키와 프로퍼티 이름이 다를 때는 @SerializedName 이라는 애너테이션*으로 명시해 주면 된다.(?)

코드에서 주석으로 지정한 @SerializedName("first_name")의 의미는 first_name 이라는 키의 데이터가 fitstName 프로퍼티에 저장된다는 의미이다. 그런데 키와 프로퍼티 이름이 다르더라도 밑줄 다음에 오는 단어의 첫 글자를 대문자로 바꾼 프로퍼티명이 있을 때는 @SerializedName 애너테이션을 사용하지 않아도 된다. 예를 들어 키가 last_name이면 자동으로 lastName 프로퍼티에 저장된다.

모델 클래스를 만들 때 서버의 데이터와 상관없는 프로퍼티를 선언해도 된다. 코드를 보면 avatarBitmap 이라는 프로퍼티를 선언했는데 서버로부터 넘어오는 JSON에는 이와 관련된 데이터가 없다. 이처럼 모델에 서버 연동과 상관없는 데이터를 담는 프로퍼티를 선언해도 된다.

서버의 데이터가 복잡할 때는 모든 데이터를 하나의 모델 클래스로 표현하지 않고 여러 클래스로 분리한 후 조합해서 사용할 수도 있다. 예를 들어 목록 화면을 구성하는 서버 데이터가 다음처럼 전달된다고 가정해 보자.

{
    "page": 2,
    "per_page": 6,
    "total": 12,
    "total_pages": 2,
    "data": [
        {
            "id": 7,
            "email": "michael.lawson@reqres.in",
            "first_name": "Michael",
            "last_name": "Lawson",
            "avatar": "https://reqres.in/img/faces/7-image.jpg"
        },
        {
            "id": 8,
            "email": "lindsay.ferguson@reqres.in",
            "first_name": "Lindsay",
            "last_name": "Ferguson",
            "avatar": "https://reqres.in/img/faces/8-image.jpg"
        }
    ]
}

이 데이터를 하나의 모델 클래스에 담아도 되지만 data 키값을 저장하는 UserModel 클래스와 전체 페이지 정보를 저장하는 UserListModel 클래스로 분리해 작성하고 UserListModel에서 UserModel을 이용하면 된다.

// 모델 클래스 분리 이용
data class UserListModel(
    var page: String,
    @SerializedName("per_page")
    var perpage: String,
    var total: String,
    @SerializedName("total_pages")
    var totalPages: String,
    var data: List<UserModel>? // 주목~
)

Retrofit을 이용할 때 UserListModel을 알려주면 JSON 데이터를 파싱해 프로퍼티에 저장하며 data 키값은 data 프로퍼티에 선언된 UserModel 클래스의 객체에 담는다. 음 글쿤~~~~~...

 

서비스 인터페이스 정의

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

// 서비스 인터페이스 정의
interface INetworkService{
    @GET("api/users")
    fun doGetUserList(@Query("page") page: String): Call<UserListMode>
    @GET
    fun getAvatarImage(@Url url: String): Call<ResponseBody>
}

INetworkService 라는 이름의 인터페이스를 선언하고 그 안에 doGetUserList(), getAvatarImage() 라는 이름의 함수를 정의했다. 그런데 이 인터페이스명과 함수명은 개발자가 지은 이름일 뿐이다. 이 인터페이스를 구현해 실제로 통신하는 클래스는 Retrofit이 자동으로 만들어주는데 이때 애너테이션을 참조한다. 즉, 함수에 선언한 애너테이션을 보고 그 정보대로 네트워크 통신을 할 수 있는 코드를 자동으로 만들어 준다.

함수에 선언한 애너테이션을 살펴보면, @GET은 서버와 연동할 때 GET 방식으로 해달라는 의미이며 @Query는 서버에 전달되는 데이터, @Url은 요청 URL을 뜻한다. 결국 Retrofit을 이용할 때는 인터페이스의 애너테이션이 중요하며 이 내용은 잠시 후에 자세히 정리할 것이다.

 

Retrofit 객체 생성

Retrofit을 사용할 때 가장 먼저 Retrofit 객체를 생성하는 코드를 실행해야 한다.

// Retrofit 객체 생성
val retrofit: Retrofit // Retrofit 객체 생성. 한 번만 하면 됨.
    get() = Retrofit.Builder()
        .baseUrl("https://reqres.in/") 
        // baseUrl 설정하면 이후에 이 URL 뒤에 올 경로만 지정해서 서버와 연동할수 있음
        .addConverterFactory(GsonConverterFactory.create())
        .build()

Retrofit 객체를 생성하는 코드는 초기 설정을 하므로 한 번만 생성하면 된다. baseUrl() 함수로 URL을 설정하면 이후에 이 URL 뒤에 올 경로만 지정해서 서버와 연동할 수 있다. 예를 들어 baseUrl을 앞의 코드처럼 선언하고 어디선가 @GET("api/users")처럼 경로를 지정했다면 서버 요청 URL은 https://reqres.in/api/users가 된다. 물론 baseUrl을 선언했더라도 전혀 다른 URL로 요청할 수도 있다.

그리고 addConverterFactory() 함수로 데이터를 파싱해 모델 객체에 담는 역할자를 지정해준다. 앞에서는 GsonConverterFactory.create()로 작성했으므로 GsonConverter를 이용하겠다는 의미이다.

 

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

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

// 서비스 객체 얻기
var networkService: INetworkService = retrofit.create(INetworkService::class.java)
// 서비스 인터페이스를 구현한 클래스의 객체를 얻음

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

 

네트워크 통신 시도

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

// Call 객체 얻기
val userListCall = networkService.doGetUserList("1")
// Retrofit 객체로 얻은 서비스 객체의 함수(인터페이스에 선언함 함수)를 호출함. Call 객체 반환받음
// 반환받은 Call 객체의 enqueue() 함수를 호출함으로써 실제 통신 이루어짐

인터페이스에 선언한 함수를 호출하면 위 코드에서 userListCall처럼 Call 객체가 반환된다. 실제 통신은 이 Call 객체의 enqueue() 함수를 호출하는 순간 이뤄진다.

// 네트워크 통신 수행
userListCall.enqueue(object : Callback<UserListModel> {
// Call 객체의 enqueue() 함수를 호출함으로써 비로소 통신 수행됨
// enqueue() 함수의 매개변수로 지정한 Callback 객체의 함수가 자동으로 호출됨
    override fun onResponse(call: Call<UserListModel>,
    // Callback 객체의 함수(통신 성공하면 자동 호출됨)
                            response: Response<UserListModel>) {
                            // 통신 성공하면 서버에서 넘어온 데이터가 매개변수인 Response 객체에 전달됨
        val userList = response.body()
        // 그리고 그 전달된 데이터를 response 객체의 body() 함수로 얻을 수 있음
        // Response<UserListModel>로 선언했으므로 response.body()함수는 UserListModel 객체의 값을 반환함
        (... 생략 ...)
    }
    override fun onFailute(call: Call<UserListModel>, t: Throwable) {
    // Callback 객체의 함수(통신 실패하면 자동 호출됨)
        call.cancel()
    }
})

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

통신에 성공하면 서버에서 넘어온 데이터가 onResponse() 함수의 매개변수인 Response 객체로 전달되며 이 데이터를 response.body() 함수로 얻을 수 있다. 위 코드를 보면 Response<UserListModel>로 선언했으므로 response.body() 함수가 반환하는 값은 UserListModel 객체이다. 즉, 제네릭*으로 선언한 클래스의 객체에 담아서 전달해 준다.

 

 

Retrofit 애너테이션

지금까지 Retrofit의 기본 구조(인터페이스에 함수를 선언만 해놓으면 얘가 필요한 서비스 객체를 만들어서 함수 호출해줌)를 이해했으므로 이제 서비스 인터페이스를 만들 때 통신 개요를 설정하는 애너테이션을 알아보자. Retrofit은 우리가 작성한 서비스 인터페이스에 따라 통신을 수행하므로 결국 어떤 애너테이션을 작성할 것인지가 핵심이다.

 

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

HTTP 메서드를 정의하는 애너테이션이다. @GET처럼 메서드명만 지정하거나 @GET("users/list")처럼 URL 경로를 지정해도 된다. 또한 @GET("users/list?sort=desc")처럼 ?로 URL뒤에 데이터를 추가할 수도 있다. 이처럼 경로를 지정하면 baseURL 뒤에 추가되어 최종 서버 요청 URL이 된다.

// HTTP 메서드 애너테이션
//인터페이스에 선언한 함수
@GET("users/list?sort=desc") // URL 경로 지정➡️최종 서버 요청 URL
fun test1(): Call<UserModel>

// Call 객체를 얻는 구문
val call: Call<UserModel> = networkService.test1()

// 최종 서버 요청 URL
https://reqres.in/users/list?sort=desc

 

 

@Path

URL의 경로를 동적으로 지정해야 할 때도 있다. 예를 들어 group/1/users나 group/2/users처럼 1, 2가 들어가는 부분을 동적으로 처리하려면 중괄호{}로 감싸야 한다. group/{id}/users라고 지정하면 {id} 영역은 동적 데이터가 들어갈 자리이며 id는 개발자가 임의로 작성하면 된다. 이 id 영역에 들어갈 데이터를 함수의 매개변수로 받으려면 그 매개변수에 @Path 애너테이션을 추가해야 한다.

// 동적인 경로 애너테이션
// 인터페이스에 선언한 함수
@GET("group/{id}/users/{name}") // 매개변수 활용하여 URL 경로 지정
fun test2(
    @Path("id") userId: String, // 데이터 받을 매개변수에 @PATH 애너테이션 추가
    @Path("name") arg2: String
): Call(UserModel)

// Call 객체를 얻는 구문
val call: Call<UserMode> = networkService.test2("10", "kkang")

// 최종 서버 요청 URL
https:/reqres.in/group/10/users/kkang

test2() 함수의 첫 번째 매개변수에 @Path("id") userId: String이라고 작성했다. 이렇게 하면 첫 번째 매개변숫값이 경로에서 {id} 영역에 대입된다.

 

 

@Query

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

// 질의 애너테이션 예
// 인터페이스에 선언한 함수
@GET("group/users") // URL 경로 지정
fun test3(
    @Query("sort") arg1: String, // @Query 애너테이션 이용해서 함수의 매개변숫값을 서버에 전달
    @Query("name") arg2: String // "name"을 키로, 매개변숫값을 값으로 해서 서버에 데이터 전달함
): Call<UserModel>

// Call 객체를 얻는 구문
val call: Call<UserModel> = networkService.test3("age", "kkang")

// 최종 서버 요청 URL
https://reqres.in/group/users?sort=age&name=kkang

함수의 매개변수에 @Query("name")이라고 선언하면 서버에 요청할 때 name을 키로, 매개변숫값을 값으로 해서 서버에 데이터를 전달한다.

 

@QueryMap

만약 서버에 전달할 데이터가 많다면 함수의 매개변수를 여러 개 선언해야 하는 부담이 있다. 이때에는 @QueryMap을 이용해 서버에 전송할 데이터를 Map 타입의 매개변수로 받으면 된다.

// 질의 맵 애너테이션 예
// 인터페이스에 선언한 함수
@GET("group/users")
fun test4(
    @QueryMap options: map<String, String>, // 서버에 전송할 데이터를 Map 타입의 매개변수로 받음
    @Query("name") name: String
): Call<UserModel>

// Call 객체를 얻는 구문
val call: Call<UserModel> = networkService.test4(
    mapOf<String, String>("one" to "hello", "two" to "world"),
    "kkang"
)

// 최종 서버 요청 URL
https://reqres.in/group/users?one=hello&two=world&name=kkang

 

 

@Body

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

// 모델 객체 애너테이션 예
// 인터페이스에 선언한 함수
@POST("group/users") // @Body로 선언한 매개변수는 @GET에서는 사용못하고 @POST와 함께 사용해야 함
fun test5(
    @Body user: UserModel, // 모델 객체 타입으로 매개변수 선언. 이 객체의 프로퍼티명을 키로, 프로퍼티의 데이터를 값으로 해서
    // JSON 문자열을 만들어 서버에 전송함
    @Query("name") name: String
): Call<UserModel>

// Call 객체를 얻는 구문
val call: Call<UserModel> = networkService.test5(
    UserModel(id="1", firstName = "gildong", lastName = "hong", avatar = "someurl"),
    "kkang"
)

// 최종 서버 요청 URL
https://reqres.in/group/users?name=kkang

// 서버에 스트림으로 전송되는 데이터
{"id":"1", "first_name":"gildong","last_name":"hong","avatar":"someurl"}

@Body 애너테이션을 사용하면 서버 요청 URL은 바뀌지 않는다. @Body로 지정한 모델의 데이터는 객체의 내용을 JSON 문자열로 만들어 URL이 아닌 데이터 스트림으로 서버에 전송된다. 

 

 

@FormUrlEncoded와 @Field

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

// URL 인코딩 애너테이션 예
// 인터페이스에 선언한 함수
@FormUrlEncoded // 서버 전송 데이터를 '키=값' 형태의 URL 인코딩으로 전송하는 애너테이션
@POST("user/edit") // 데이터를 URL 인코딩 형태로 만들어 전송하려면 POST 방식에서만 사용 가능
fun test6(
    @Field("fitst_name") first: String?, // @Field 애너테이션이 추가된 데이터가 인코딩 대상
    @Field("last_name") last: String?,
    @Query("name") name: String?
): Call<UserModel>

// Call 객체를 얻는 구문
val call: Call<UserModel> = networkService.test6(
    "gildong 길동",
    "hong 홍",
    "kkang"
)

// 최종 서버 요청 URL
https://reqres.in/user/edit?name=kkang

// 서버에 스트림으로 전송되는 데이터
first_name=gildong%20%어쩌구last_name=hong%20% 어쩌구

 

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

// 리스트에 필드 애너테이션 사용 예
// 인터페이스에 선언한 함수
@FormUrlEncoded
@POST("tasks")
fun test6(@Field"title" titles: List<String>): Call<Usermodel>

// Call 객체를 얻는 구문
val list: MutableList<String> = ArrayList() // 배열리스트? 객체 생성하고
list.add("홍길동") // 데이터 여러건 한꺼번에 지정하고
lsit.add("류현진")
val call = networkService.test7(list) // 같은 키로 한번에 서버에 전달

// 최종 서버 요청 URL
https://reqres.in/tasks

// 서버에 스트림으로 전송되는 데이터
title=%어쩌구title=어쩌구

 

 

@Header

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

// 헤더 애너테이션 예
// 인터페이스에 선언한 함수
@Headers("Cache-Control: max-age=640000")
@GET("widget/list")
fun test8(): Call<UserModel>

 

@Url

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

// URL 애너테이션 예
// 인터페이스에 선언한 함수
@GET
fun test9(@Url url: String, @Query("name") name: String): call<Usermodel>

// Call 객체를 얻는 구문
val call = networkService.test9("http://www.googld.com", "kkang")

// 최종 서버 요청 URL
http://www.googld.com/?name=kkang