1. Retrofit2란?
Retrofit2는 안드로이드 애플리케이션에서 REST API를 사용하여 서버와 통신하는 라이브러리입니다.
스퀘어에서 만들었으며 HTTP 통신을 간편하게 만들어줍니다. Retrofit1도 있지만, Retroift2가 2015년에 나왔으므로 이제 Retrofit하면 전부 Retrofit2라고 봐도 무방합니다.
1-1. REST API
REST API(Representational State Transfer Application Programming Interface)는 웹 기반 응용 프로그램에서 사용되는 소프트웨어 인터페이스입니다. 클라이언트 - 서버 모델을 기반으로 하며, HTTP 프로토콜을 통해 서버와 클라이언트 간 통신을 합니다. 전송하는 데이터 형식은 일반적으로 JSON과 XML 형태이며, 클라이언트는 REST API를 통해 서버에 HTTP 요청을 보내고, 서버는 HTTP 응답을 반환하여 클라이언트와 통신을 합니다.
HTTP 메서드(GET, POST, PUT, DELETE 등)을 사용하며 서버에서 데이터를 검색하거나 업데이트하고, URI를 사용하여 서버의 리소스에 접근합니다.
쉽게 정리하면 웹 기반 어플리케이션에서 사용하는 일종의 규약이며 아래와 같은 규칙을 따릅니다.
- 자원(Resource): 모든 자원은 고유한 ID를 가지며, 이를 통해 식별합니다.
- 행위(Verb): HTTP 메서드(GET, POST, PUT, DELETE 등)를 사용하여 자원에 대한 행위를 정의합니다.
- 표현(Representation): 서버에서 반환되는 자원은 클라이언트가 이해할 수 있는 형식(JSON, XML 등)으로 표현됩니다.
- 자기서술적 메시지(Self-descriptive message): 메시지는 자체적으로 해석 가능하도록 구성되어야 합니다.
- 하이퍼미디어(Hypermedia): 하이퍼미디어를 통해 서버에서 반환된 자원과 연결된 다른 자원에 대한 링크 정보를 제공합니다.
1-2. Retrofit의 동작 방식
Retrofit의 동작 방식은 아래와 같습니다.
- 통신용 함수를 선언한 인터페이스를 작성합니다.
- Retrofit에 인터페이스를 전달합니다.
- Retrofit이 통신용 서비스 객체를 반환합니다.
- 서비스의 통신용 함수를 호출한 후 Call 객체를 반환합니다.
- Call 객체의 enqueue() 함수를 호출하여 네트워크 통신을 수행합니다.
2. Retrofit 사용법
Retrofit 사용법은 이번에 제가 개인 프로젝트를 진행한 날씨 API를 가져오는 것을 예시로 만들겠습니다.
2-1. 라이브러리 선언
먼저 라이브러리를 선언해야 합니다.
// retrofit library
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11'
converter에는 moshi나 gson등 다양한 방법이 있는데 저는 gson을 이용하였습니다.
2-2. 모델 클래스 선언
모델 클래스는 서버와 주고받는 데이터를 표현하는 클래스입니다.
JSON과 XML 중에서 저는 JSON 형식을 선택하였습니다.
data class WeatherData(
@SerializedName("main")
val main: MainData
)
data class MainData(
@SerializedName("temp")
val temperature: Double,
@SerializedName("feels_like")
val feelsLike: Double,
@SerializedName("temp_min")
val minTemperature: Double,
@SerializedName("temp_max")
val maxTemperature: Double
)
openWeatherMap API에서 받아올 정보 중에 제가 필요한 정보만 따로 뽑아서 JSON 형태로 제작한 것입니다.
@SerializedName은 JSON 데이터에서 해당 값을 파싱할 속성의 이름을 지정하는 어노테이션입니다.
예를 들어서 위의 코드는 openWeatherMap API에서 반환하는 JSON 데이터에서 "main" 속성에 대한 데이터를 파싱하기 위해 작성한 코드입니다. openWeatherMap API에서 반환하는 JSON 데이터 형식은 아래와 같습니다.
{
"main": {
"temp": 23.2,
"feels_like": 20.8,
"temp_min": 22.0,
"temp_max": 24.4
}
}
2-3. 서비스 인터페이스 정의
Retrofit을 이용할 때 가장 중요한 부분은 네트워크 통신이 필요한 순간에 호출할 함수를 포함하는 서비스 인터페이스를 작성하는 것입니다.
interface WeatherAPIService {
@GET("data/2.5/{path}")
fun getWeatherData(
@Path("path") path: String,
@Query("q") q: String,
@Query("appid") appid: String
): Call<WeatherData>
}
REST API에 규약된 GET 통신 방법을 이용하여 opneWeather API 서버로부터 지정된 경로에 따라 데이터를 받는 것입니다.
@path는 경로에 넣을 데이터를 의미하고, @Query는 서버에 전달되는 데이터를 의미합니다. 이 함수를 통해 데이터를 받으면 미리 지정한 model의 형식에 맞게 Call<WeatherData> 객체를 반환합니다.
2-4. Retrofit 객체 생성
Retrofit을 사용할 때 가장 먼저 Retrofit 객체를 생성하는 코드를 실행해야 합니다.
object WeatherAPIClient {
fun getClient(context: Context,url: String): WeatherAPIService{
val cacheSize = (5 * 1024 * 1024).toLong() // 5MB
val cache = Cache(context.cacheDir, cacheSize)
val client: OkHttpClient = OkHttpClient.Builder()
.addInterceptor(HeadInterceptor())
.addNetworkInterceptor(HeadInterceptor())
.cache(cache)
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.build()
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(url)
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build()
return retrofit.create(WeatherAPIService::class.java)
}
}
Retrofit 객체를 만드는 인스턴스는 하나만 있어도 되므로 싱글톤 오브젝트로 제작합니다.
getClient 메서드를 통해 Retrofit 객체를 생성하고 반환합니다.
GsonConverterFactory는 JSON 형식의 응답 데이터를 자바 객체로 변환하는데 사용됩니다.
OkHttpClient 라이브러리를 사용하여 HTTP 요청과 응답을 처리합니다.
여기서 cache나 timeout은 그냥 통신의 안전성을 높이기 위해 만든 코드이고 핵심만 뽑자면 아래와 같습니다.
OkHttpClient를 사용한 것은 retrofit 통신을 사용할 때 추가적인 기능(timeout, interceptor, cache 등)을 구현하기 위해 만든 것이고, 가볍게 구현할 때에는 그냥 okHttpClinet를 적지 않고 retrofit과 return 부분만 구현해도 상관 없습니다.
따라서 이번 설명에서는 HeadInterceptor에 대한 설명은 생략하겠습니다.
2-5. 네트워크 통신 시도
사실상 모든 준비가 끝났습니다. 네트워크 통신이 필요한 순간 Retrofit 객체로 얻은 서비스 객체의 함수를 호출하면 됩니다. Call 객체를 얻고 이 객체의 enqueue() 함수를 호출하는 클래스를 만들었습니다.
interface GetDataSource {
fun getWeatherInfo(
jsonObject: JSONObject,
onResponse: (Response<WeatherData>) -> Unit,
onFailure: (Throwable) -> Unit
)
}
class GetDataSourceImpl(private val context: Context): GetDataSource {
override fun getWeatherInfo(
jsonObject: JSONObject,
onResponse: (Response<WeatherData>) -> Unit,
onFailure: (Throwable) -> Unit
) {
val weatherAPIService = WeatherAPIClient.getClient(context, jsonObject.getString("url"))
weatherAPIService.getWeatherData(jsonObject.getString("path"), jsonObject.getString("q"), jsonObject.getString("appid"))
.enqueue(object : Callback<WeatherData>{
override fun onResponse(call: Call<WeatherData>, response: Response<WeatherData>) {
onResponse(response)
}
override fun onFailure(call: Call<WeatherData>, t: Throwable) {
onFailure(t)
}
})
}
}
통신을 하기 위해 보내는 Header에 추가할 내용을 담은 jsonObject와 성공할 경우와 실패할 경우의 데이터를 정의합니다.
jsonobject에서 "url"과 "path", "q", "appid"를를 담고 요청을 보냅니다. 이를 통해 API에 접근하고 Call 객체의 enqueue() 함수를 호출하면 통신이 수행됩니다. 그리고 enqueue() 함수의 매개변수로 지정한 Callback 객체의 onResponse와 onFailure 함수가 자동으로 호출됩니다. 각각 성공할 경우와 실패할 경우에 호출 됩니다.
만약 통신에 성공하면 가져온 데이터를 Response<WeatherData>. 즉, WeatherData 형태로 onResponse에 전달합니다.
원래는 여기서 val weatherData = response.body() 형태로 가져와야 하지만, 저는 MVVM 형식으로 구성하여 따로 repository와 ViewModel 클래스를 만들어 그곳에서 처리하도록 만들었습니다. 자세한 설명은 따로 제가 제작한 날씨 앱에 대한 설명을 할 때 하겠습니다. 지금은 그냥 이런 구조로 작동하는 구나 정도로만 이해하시면 될 것 같습니다.
일반적인 형태의 예시는 아래와 같습니다.
// Call 객체 획득
val userListCall = networkService.doGetUserList("1")
// 네트워크 통신 수행
userListCall.enqueue(object : Callback<UserListModel>{
override fun onResponse(call: Call<UserListModel>,
response: Response<UserListModel>) {
val userList = response.body()
}
override fun onFailure(call: Call<UserListModel>, t: Throwable) {
call.cancle()
}
})
2-6. 실제 작동
이제 mainAcitivty에서 실제로 작동하게 하면 됩니다. 보통 네트워크와 같이 시간이 오래 걸리는 작업은 IO 영역에서 작업을 돌립니다. 안드로이드 스튜디오에서는 코루틴이라는 비동기적 작업을 지원하는 기능이 있으니 이것을 사용하도록 합니다. 코루틴에 대한 설명은 아래 링크를 통하시면 됩니다.
개념 정리 : 코루틴
1. 정의 1-1. 코루틴이란? 코루틴은 비동기 프로그래밍을 지원하는 Kotlin의 라이브러리이다. 스레드와 비슷한 역할을 하지만, 비교적 가벼우며, 쉽게 사용할 수 있다. 1-2. 비동기 작업은? 비동기 작
baboprograming.tistory.com
실제 제 코드에서 사용한 형식은 아래와 같습니다.
private fun updateLocation(){
GlobalScope.launch(Dispatchers.IO) {
while(isActive){
weatherViewModel.getWeatherInfoView(jsonObjectW)
weatherViewModel.getForecastInfoView(jsonObjectF)
withContext(Dispatchers.Main){
observeData()
}
delay(300000)
}
}
}
5분을 주기로 업데이트를 진행합니다. 제가 확인해본 결과 openWeatherAPI의 정보는 10분 주기로 업데이트가 됩니다. 보통 정보 업데이트는 이 주기의 절반을 사용한다고 하여 5분으로 지정하였습니다.
observeData()는 해당 앱에서 UI 업데이트를 담당하는 함수로 안드로이드에서 UI는 Main Thread에서 담당하여 withContext 구문으로 따로 지정을 해줍니다.
3. 추가 정리 - Retrofit 어노테이션
3-1. Retrofit 어노테이션이란?
어노테이션은 저희가 서비스 인터페이스를 제작할 때, @GET 이나 @POST와 같이 통신 방식을 지정하던 것을 의미합니다. 통신 방식 외에 Retrofit 서비스 인터페이스를 만들 때 통신 개요를 설정하는 역할을 합니다.
3-2. @GET, @POST, @PUT, @DELETE, @HEAD
HTTP 메서드를 정의하는 어노테이션입니다.
- @GET: HTTP GET 요청을 나타냅니다. GET 요청은 서버에서 지정된 리소스를 가져오기 위해 사용됩니다. 예를 들어, 게시물 목록을 가져오기 위한 GET 요청은 다음과 같이 정의될 수 있습니다.
@GET("posts")
fun getPosts(): Call<List<Post>>
- @POST: HTTP POST 요청을 나타냅니다. POST 요청은 서버에 데이터를 제출하기 위해 사용됩니다. 예를 들어, 새로운 게시물을 작성하기 위한 POST 요청은 다음과 같이 정의될 수 있습니다.
@POST("posts")
fun createPost(@Body post: Post): Call<Post>
- @PUT: HTTP PUT 요청을 나타냅니다. PUT 요청은 서버에서 지정된 리소스를 업데이트하기 위해 사용됩니다. 예를 들어, 게시물을 수정하기 위한 PUT 요청은 다음과 같이 정의될 수 있습니다.
@PUT("posts/{id}")
fun updatePost(@Path("id") postId: Int, @Body post: Post): Call<Post>
- @DELETE: HTTP DELETE 요청을 나타냅니다. DELETE 요청은 서버에서 지정된 리소스를 삭제하기 위해 사용됩니다. 예를 들어, 게시물을 삭제하기 위한 DELETE 요청은 다음과 같이 정의될 수 있습니다.
@DELETE("posts/{id}")
fun deletePost(@Path("id") postId: Int): Call<Void>
- @HEAD: HTTP HEAD 요청을 나타냅니다. HEAD 요청은 GET 요청과 유사하지만, 서버에서 응답 본문(body)을 제외하고 헤더 정보만 가져옵니다. 예를 들어, 파일 다운로드를 위한 HEAD 요청은 다음과 같이 정의될 수 있습니다.
@HEAD("files/{id}")
fun getFileHeaders(@Path("id") fileId: Int): Call<Void>
3-3. @Path
URL 경로를 동적으로 지정해야 할 때 사용합니다.
interface WeatherAPIService {
@GET("data/2.5/{path}")
fun getWeatherData(
@Path("path") path: String,
@Query("q") q: String,
@Query("appid") appid: String
): Call<WeatherData>
}
동적으로 지정해야 할 부분을 중괄호로 감싸고 매개변수에 @Path 어노테이션을 추가합니다.
3-4. @Query
함수의 매개변수 값을 서버에 전달하고 싶을 때 사용합니다.
위의 코드에서 지정한 쿼리 "q"하고 "appid"는 Call 객체를 얻을 때 아래와 같이 추가하여 사용합니다.
보통 경로에 ? 뒤에 붙는 값들을 지정할 때 사용합니다.
하나 예시를 들어보면 네이버 웹툰에서 "재벌집 막내아들"이라는 웹툰을 보려고 하면 URL은 아래와 같습니다.
https://comic.naver.com/webtoon/list?titleId=800770
여기서 ?뒤에 붙은 titleld = 800770이 여기에 해당합니다.
이를 코드로 구성하면 아래와 같습니다.
// Interface 선언
@GET("weebtoon/list")
fun showWebtoon(
@Query("titled") titleNumber: String,
): Call<WebtoonModel>
// Call 객체 획득
val call: Call<WebtoonModel> = networkService.showWebtoon("800770")
3-5. @QueryMap
하지만 실제 페이지를 깊숙히 들어갈 수록 지정해야할 Query가 많아지는 것을 확인할 수 있습니다.
아래 URL은 재벌집 막내아들 9화의 URL입니다.
https://comic.naver.com/webtoon/detail?titleId=800770&no=9
여기서는 "no"라는 변수가 하나 더 추가 되었지만, 다른 상황에서는 이런 변수가 5개, 10개 이런 식으로 많이 늘어날 수 있습니다. 이럴 때 하나하나 @Query를 붙이기에는 번거로우니 @QueryMap을 이용합니다.
예시 코드는 아래와 같습니다.
// Interface 선언
@GET("weebtoon/list")
fun showWebtoon(
@QueryMap query: Map<String, String>
): Call<WebtoonModel>
// Call 객체 획득
val queryMap = hashMapOf<String, String>()
queryMap["titled"] = "800770"
queryMap["no"] = "9"
val call: Call<WebtoonModel> = networkService.showWebtoon(queryMap)
3-6. @Body
HTTP 메서드를 정의하는 어노테이션을 확인하면 @GET과 다르게 @POST에서는 @Body라는 어노테이션이 들어가는 것을 확인할 수 있습니다. 이는 서버에 전송할 데이터를 모델 객체로 지정하고 싶을 때 사용합니다. @GET에서는 사용할 수 없으며 @POST와 함께 사용해야 합니다. 전송하는 형태는 JSON으로 전송합니다.
3-7. @FormUrlEncoded와 @Field
POST 방식의 폼 데이터를 전송하기 위한 어노테이션입니다.
@FormUrlEncoded 어노테이션은 요청 바디가 폼 URL 인코딩된 데이터를 전송할 것임을 명시합니다. 폼 URL 인코딩이란, ASCII 문자 외의 문자를 URL 인코딩 문자로 변경하는 것을 말합니다.
@Field 어노테이션은 요청 바디에 전송할 필드 값을 나타냅니다. 이 어노테이션은 주로 @FormUrlEncoded 어노테이션이 붙은 메소드의 매개변수에 사용됩니다.
예시 코드는 아래와 같습니다.
@FormUrlEncoded
@POST("user/register")
fun registerUser(
@Field("name") name: String,
@Field("email") email: String,
@Field("password") password: String
): Call<User>
위 예제에서는 @FormUrlEncoded 어노테이션이 붙은 POST 방식의 요청을 보내는 registerUser() 메소드가 정의되어 있습니다. 이 메소드는 name, email, password 3개의 필드 값을 전송하기 위해 @Field 어노테이션을 사용하고 있습니다. 이렇게 사용하면 Retrofit은 전달된 필드 값을 자동으로 인코딩하고, 요청 바디에 포함시킵니다.
@Body와 다른 점은 @Body는 JSON 형식으로 데이터를 전송하고, @FromUrlEncoded는 Key = Value 방식으로 데이터를 전송합니다. 마찬가지로 POST 방식에서만 사용이 가능합니다.
3-8. @Header
서버 요청에서 헤더값을 조정하고 싶을 때 사용합니다.
헤더는 HTTP에서 요청 및 응답에서 메타데이터를 전송하는데 사용되는 정보입니다.
메타데이터는 데이터를 설명하는 데이터로, 데이터의 특성, 구조, 용도 등의 설명하는 정보를 가지고 있습니다. 예를 들어 작성자, 제목, 날짜, 태그 등의 정보가 포함됩니다.
예시 코드는 아래와 같습니다.
@GET("users")
@Headers("Authorization: Bearer <your_token>", "Cache-Control: max-age=640000")
fun getUsers(): Call<List<User>>
위 예제에서는 @Headers 어노테이션을 사용하여 HTTP 요청에 추가될 헤더를 정의하고 있습니다. Authorization 헤더에는 토큰 값을, Cache-Control 헤더에는 캐싱 정보를 설정하고 있습니다.
4. 참고 사이트
[안드로이드] Retrofit2 '레트로핏' - 기본 사용법
Retrofit2 - REST API 통신 라이브러리 'Retrofit' - REST통신 라이브러리 기본 개념 & 사용법 통신 라이브러리 중 가장 많이 사용되는 대표적인 라이브러리 ( Squareup 사의 라이브러리) Retrofit 이란? REST API
jaejong.tistory.com
'Kotlin > 개념 정리' 카테고리의 다른 글
개념 정리: JSON 구조 (0) | 2023.03.08 |
---|---|
개념 정리 : 뷰 모델 (0) | 2023.02.17 |
개념 정리 : 라이브 데이터(Live Data) (0) | 2023.02.17 |
개념 정리 : 데이터 바인딩 (0) | 2023.02.17 |
개념 정리 : 뷰 바인딩 (0) | 2023.02.17 |