Retrofit
Retrofit 은 Square사에서 제공하는 오픈 소스 라이브러리이다.
REST API를 안드로이드에서 쉽게 사용하도록 도와준다.
OkHttp 라이브러리를 자신의 HTTP클라이언트로 사용한다.
래트로핏은 HTTP 게이트웨이 클래스 생성을 도와준다.
- 애노테이션을 지정된 함수를 갖는 인터페이스를 작성한다.
- Retrofit이 인터페이스의 구현 클래스를 생성한다.
- 이 클래스에서 HTTP 요청(request)을 하고 OkHttp.ResponseBody 로 HTTP 응답(response)을 파싱한다.
여기서 중요한 포인트는, ResponseBody를 그대로 받아오는 것보다 형변환을 한번 거쳐서 가져오는 것이 더 편하고 좋다.
응답 변환기 Response Converter
앱에서 필요한 타입으로 형변환을 해주는 컨버터이다.
과정
1. API 인터페이스를 정의해준다.
package com.패키지명.api
import retrofit2.Call
import retrofit2.http.GET
interface SampleApi {
// @GET을 통해 데이터를 가져온다."/"은 기본 URL로 전송됨을 의미한다.
@GET("/")
fun fetchContents() : Call<String> // Call(래트로핏)을 통해 요청에 의한 반환값을 Call형으로 받고, 그 안의 형태는 String으로 정한 것이다.
}
2. Retrofit 인스턴스와 API 인스턴스 생성해준다.
원하는 화면, API를 통신할 화면의 Activity나 Fragment에서 인스턴스를 생성해준다.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 프래그먼트화면생성 전에 oncreate에서 진행하여 통신을 진행하는 것이다.
// site와 연결해준다.
val retrofit : Retrofit = Retrofit.Builder()
.baseUrl("https:/사이트주소/")
.addConverterFactory(ScalarsConverterFactory.create()) // 스칼라 컨버터(객체변환기)를 추가해준다.
.build()
// 해당 연결 사이트를, retrofit 인터페이스와 연결하여 인스턴스를 생성해준다.
val sampleApi : SampleApi = retrofit.create(SampleApi::class.java)
}
3. 웹 요청 실행하기
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
....
// 웹 요청을 나타내는 Call 객체 생성
val sampleHomePageRequest : Call<String> = sampleApi.fetchContents()
sampleHomePageRequest.enqueue(object : Callback<String> {
override fun onResponse(call: Call<String>, response: Response<String>) {
Log.d(TAG, "Response received: ${response.body()}")
}
override fun onFailure(call: Call<String>, t: Throwable) {
Log.e(TAG,"Failed to fetch photos", t)
}
})
}
enqueue() 함수를 호출하여 비동기로 실행해주는 것이다.
Call.enqueue() 는 Call 객체가 나타내는 웹 요청을 백그라운드 스레드에서 실행한다.
이 때, Retrofit이 백그라운드 스레드를 관리하므로 신경 쓰지 않아도 된다.
레트로핏은 가장 중요한 두 가지 안드로이드 스레드 규칙을 지켜야 한다.
1. 시간이 걸리는 작업을 main thread 가 아닌 background thread 에서 실행한다.
2. UI 변경은 main thread 에서만 하며 절대로 background thread 에서 하지 않는다.
(안드로이드는 네트워크 작업 수행을 main thread에서 하는 것을 허용하지 않는다. - NetworkOnMainThreadException 발생)
백그라운드 스레드는 해야 할 작업을 Queue 나 List 에 유지하고 관리한다.
enqueu()를 호출함으로써 백그라운드 스레드에 작업 규에 추가한다. 그리고 다수의 요청을 큐에 넣을 수 있다.
이 때 Retrofit은 큐가 비워질 때까지 하나씩 요청을 처리한다.
enqueue() 함수에서는 Callback객체를 인자로 전달하여 통신의 결과를 알려주는 콜백 함수가 있다.
이것이 위의 코드에서 onResponse(), onFailure() 함수 이다.
Call.enqueue() 와 Call.execute() enqueue() 는 비동기로 요청이 실행된다. execute() 는 동기로 요청이 실행된다.
4. 네트워크 퍼미션 요청하기
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.패키지명">
<uses-permission android:name="android.permission.INTERNET"/>
<application>
....
</application>
</manifest>
INTERNET 퍼미션을 허용해두자.
5. 네트워크 관련 코드 리팩토링하기 (Refactoring)
프래그먼트나 액티비티에 네트워크 관련 코드가 들어있는 상황이다. 이러한 코드는 좋지 않으므로, 따로 class를 형성하여 구분해주는 작업을 한다.
package com.패키지명
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.패키지명.api.FlickrApi
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.scalars.ScalarsConverterFactory
private const val TAG = "SampleFetchr"
class SampleFetchr {
private val sampleApi : SampleApi
init {
val retrofit : Retrofit = Retrofit.Builder()
.baseUrl("https://사이트명/")
.addConverterFactory(ScalarsConverterFactory.create())
.build()
flickrApi = retrofit.create(SampleApi::class.java)
}
fun fetchContents() : LiveData<String> {
val responseLiveData : MutableLiveData<String> = MutableLiveData()
val sampleRequest : Call<String> = sampleApi.fetchContents()
sampleRequest.enqueue(object : Callback<String> {
override fun onResponse(call: Call<String>, response: Response<String>) {
Log.d(TAG, "Response received!")
responseLiveData.value = response.body()
Log.d(TAG, "Response Data: ${responseLiveData.value}")
}
override fun onFailure(call: Call<String>, t: Throwable) {
Log.e(TAG,"Failed to fetch photos", t)
}
})
return responseLiveData
}
}
이렇게 새로운 class에 네트워크 관련 코드를 옮겨 준뒤, 원래의 oncreate()에 있던 네트워크 코드는 지워준다.
그리고 아래의 코드처럼 새로운 네트워크 클래스의 함수를 불러와준다.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 네트워크 관련 코드를 SampleFetchr로 이동시켜주었다.
val sampleLiveData : LiveData<String> = SampleFetchr().fetchContents()
sampleLiveData.observe(
this,
Observer { responseString ->
Log.d(TAG, "Response received: $responseString")
}
)
}
여기서 SampleFetchr는 기본적인 래포지터리 역할을 한다. (Repository 저장소)
저장소 클래스는 하나 또는 다수의 데이터 소스로부터 데이터를 가져오는 로직을 갖는다.
로컨 데이터베이스나 원격 서버로부터 특정 데이터 셋을 가져오고 저장하는 방법을 결정하며,
UI 코드에서는 저장소에 모든 데이터를 요청만 한다.
사이트에서 데이터를 직접 가져와서 바로 사용하는 코드이지만,
대부분의 앱은 이러한 작업보다는 DB 에 먼저 저장한 후 저장소 클래스에서 꺼내쓰기 때문에 이러한 리팩토링은 반드시 필요하다. ( 즉, 애초에 작업을 할 때, 기본적으로 통신에 필요한 코드는 분리해서 작성하도록 하자)
정상적으로 통신이 되면, 아래와 같은 로그가 뜬다
2021-08-18 19:13:32.871 20539-20539/com.패키지명 D/SampleFetchr: Response received!
2021-08-18 19:13:32.879 20539-20539/com.패키지명 D/SampleFetchr: Response Data: <!DOCTYPE html>
<html xmlns:cc="http://creativecommons.org/ns#" lang="en-us" class="no-js fluid html-sohp-slideshow-sohp-e-view scrolling-layout ">
<head>
<meta property="fb:app_id" content="137206539707334" />
<meta property="og:site_name" content="Flickr" />
<meta property="og:updated_time" content="2021-08-18T10:13:32.549Z" />
<meta name="robots" content="archive" />
<meta name="googlebot" content="archive" />
<script type="application/ld+json">
[{
"@context": "http://schema.org",
"@type": "WebSite",
"name": "Flickr",
"url": "https://www.flickr.com",
"potentialAction": {
"@type": "SearchAction",
"target": "https://www.flickr.com/search?text={search_term_string}&structured=yes",
"query-input": "required name=search_term_string"
}
},
{
"@context": "http://schema.org",
/....
. - ` : ` '.' `` . - '` ` .
' ,gi$@$q pggq pggq . ' pggq
+ j@@@P*\7 @@@@ @@@@ _ : @@@@ ! ._ , . _ - .
. . @@@K @@@@ ; -` `_,_ ` . @@@@ ;/ ` _,,_ `
; pgg@@@@gggq @@@@ @@@@ .' ,iS@@@@@Si @@@@ .6@@@P' !!!! j!!!!7 ;
@@@@@@@@@@@ @@@@ @@@@ ` j@@@P*"*+Y7 @@@@ .6@@@P !!!!47*"*+;
`_ @@@@ @@@@ @@@@ .@@@7 . ` @@@@.6@@@P ` !!!!; . '
. @@@@ ' @@@@ @@@@ :@@@! !: @@@@7@@@K `; !!!! ' ` '
@@@@ . @@@@ @@@@ `%@@@. . @@@@`7@@@b . !!!! :
! @@@@ @@@@ @@@@ \@@@$+,,+4b @@@@ `7@@@b !!!!
@@@@ : @@@@ @@@@ `7%S@@hX!P' @@@@ `7@@@b !!!! .
: """" """" """" :. `^"^` """" `""""" ''''
` - . . _._ `
6. 통신에 성공했다면, JSON가져오기
위의 예시들은 www.flickr.com의 api 를 활용한 사례이다.
이 사이트에 api는 https://www.flickr.com/services/api/ 으로 이동하여 사용할 수 있다.
어찌 저찌 하여 API키를 만들어서 통신에 성공하면, 아래와 같이 JSON데이터를 수령할 수 있는데,
대략적인 url은 아래와 같다.
https://www.flickr.com/services/rest/?method=flickr.interestingness.getList&api_key=API키&extras=url_s&format=json&nojsoncallback=1
위의 url을 들어가면, 아래와 같이 JSON형태의 데이터를 수령할 수 있는데, 이렇게 보기 힘들다.
그렇다면, 좀 간편하게 보기위해, VScode를 켜서 복붙을 해보자 그리고 파일명 형식에 .json을 붙여주면 자동으로 정리해준다.
이제 통신이 가능한 이 데이터를 xml 형태로 android studio project에 반영해줘야 하는데,
위의 SampleApi 인터페이스를 변경해줘야 한다.
package com.패키지명.api
import retrofit2.Call
import retrofit2.http.GET
interface SampleApi {
// @GET을 통해 데이터를 가져온다."/"은 기본 URL로 전송됨을 의미한다.
// @GET("/")
// fun fetchContents() : Call<String> // Call(래트로핏)을 통해 요청에 의한 반환값을 Call형으로 받고, 그 안의 형태는 String으로 정한 것이다.
// 통신이 될 api키가 포함된 url 을 Get에 추가해준뒤, 바로 아래에 함수를 추가해준다.
@GET(
"services/rest/?method=flickr.interestingness.getList" +
"&api_key=API 키" +
"&format=json"+
"&nojsoncallback=1"+
"&extras=url_s"
)
fun fetchPhotos(): Call<String>
}
@GET 애노테이션 안에 위의 url을 붙여준다. '&'으로 된 부분은 옵션이라고 보면되는데, 이러한 부분을 한줄로 쓰려면
엄청 길어지기 때문에 + 로 나눠서 붙여준다. 그리고 위의 fetchContents부분을 바꿔준다. fetchPhotos 로 해준다.
package com.패키지명
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.패키지명.api.FlickrApi
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.scalars.ScalarsConverterFactory
private const val TAG = "SampleFetchr"
class SampleFetchr {
private val sampleApi : SampleApi
init {
val retrofit : Retrofit = Retrofit.Builder()
.baseUrl("https://사이트명/")
.addConverterFactory(ScalarsConverterFactory.create())
.build()
flickrApi = retrofit.create(SampleApi::class.java)
}
fun fetchPhotos() : LiveData<String> {
val responseLiveData : MutableLiveData<String> = MutableLiveData()
val sampleRequest : Call<String> = sampleApi.fetchPhotos()
sampleRequest.enqueue(object : Callback<String> {
override fun onResponse(call: Call<String>, response: Response<String>) {
Log.d(TAG, "Response received!")
responseLiveData.value = response.body()
Log.d(TAG, "Response Data: ${responseLiveData.value}")
}
override fun onFailure(call: Call<String>, t: Throwable) {
Log.e(TAG,"Failed to fetch photos", t)
}
})
return responseLiveData
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 네트워크 관련 코드를 SampleFetchr로 이동시켜주었다.
val sampleLiveData : LiveData<String> = SampleFetchr().fetchPhotos()
sampleLiveData.observe(
this,
Observer { responseString ->
Log.d(TAG, "Response received: $responseString")
}
)
}
JSON응답 데이터를 받았다면, Log에 해당 JSON데이터가 뜰 것이다.
이제는 이러한 데이터를 정리하고 원하는 데이터를 뽑아내야 한다.
이렇게 하기 위해서는 먼저 데이터 파싱하여 필요한 모델 객체에 맞게 넣어줘야 한다.
package com.패키지명
data class GalleryItem(
var title: String = "",
var id : String = "",
var url: String = ""
)
위와 같은 데이터 클래스를 만들어준뒤, 이 클래스의 인스턴스를 생성하여 API 로 부터 받은 JSON 응답 데이터를 저장한다.
7. JSON 데이터를 데이터 클래스로 파싱하기 (역직렬화) Deserialized
역직렬화는 겹겹히 감싸져있는 JSON데이터를 원하는 형태로 껍질을 벗겨 사용하고자 하는 형태로 만드는 것이라 보면된다.
보통 GSON 라이브러리를 사용하여 간단하게 역직렬화 한다.
의존성을 추가해준다. (build.gradle app - gson 라이브러리 추가)
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
위의 데이터 클래스에서 url 은 JSON데이터의 url_s와는 다른 명칭을 사용하고 있다.
이러한 경우, 보통 일치시켜주거나 @SerializedName(json명칭) 을 통해 원하는 명칭으로 사용가능하다.
import com.google.gson.annotations.SerializedName
data class GalleryItem(
var title: String = "",
var id : String = "",
@SerializedName("url_s") var url: String = ""
)
JSON 데이터의 형식이 위와 같이 들어오기 때문에, 제일 밖에 photos를 걸러주고,
photo를 걸러주는 클래스들을 생성해준다.
photoresponse의 경우 해당 데이터의 안쪽은 리스트로 되어있기 때문에, List<GalleryItem>의 형태로 받아준다.
즉, GalleryItem에서 id,title, url만 적었으므로, 여기서 원하는 데이터만 필터링을 할 수도 있는 것이다.
class FlickrResponse {
lateinit var photos : PhotoResponse
}
class PhotoResponse {
@SerializedName("photo")
lateinit var galleryItems : List<GalleryItem>
}
즉 이제는 Retrofit에서도 GSON을 사용하도록 설정해줘야 하는데,
SampleApi 인터페이스의 함수 반환값을 GSON형태로 변경해준다.
package com.패키지명.api
import retrofit2.Call
import retrofit2.http.GET
interface SampleApi {
@GET(
"services/rest/?method=flickr.interestingness.getList" +
"&api_key=API 키" +
"&format=json"+
"&nojsoncallback=1"+
"&extras=url_s"
)
fun fetchPhotos(): Call<FlickrResponse> // 제일 밖의 Photos를 받는 부분을 걸러준다.
}
위의 프래그먼트, 클래스의 <string> 부분을 전부 GalleryItem으로 변경해주거나 FlickrResponse로 변경해준다.
GSON을 사용하는 것이다.
Retrofit에 대한 느낌을 느껴보기위해서는 정말 다양하기 때문에 정리가 어렵다는 것을 느꼈다.
좀 더 깔끔하게 Retrofit을 느끼고 실질적으로 어떻게 사용해야할지 다시 한번 정리해봐야겠다.
'Programming > Android' 카테고리의 다른 글
Message (0) | 2021.08.24 |
---|---|
JetPack AAC: DataBinding (0) | 2021.08.05 |
Accessibility (0) | 2021.08.04 |