현실 개발업무 시 중요한 원격 서버의 응답을 다루는 방법을 배운다. Retrofit 을 사용해 네트워크 엔드포인트에서 데이터를 가져오고, Moshi 를 이용해 JSON 페이로드를 코틀린 데이터 객체로 파싱하고 Glide 를 사용해 ImageView 에 이미지를 로드하는 방법을 알 수 있게 된다.
여기서 다룰 내용은 아래와 같다.
REST 는 서버에서 데이터를 가져오는 아키텍처 중 하나이다. 6가지 제약조건을 갖는데 클라이언트-서버 구조, 무상태, 캐싱, 계층 구조, 코드 온 디맨드, 인터페이스 일관성이다.
이를 웹 서비스의 API(애플리케이션 프로그래밍 인터페이스, Application Programming Interface) 에 적용하면 HTTP 기반의 RESTful-API 가 된다. HTTP 인터넷 기반 데이터 통신의 기초 프로토콜이다.
RESTful-API 는 표준 HTTP 메서드인 GET, POST, PUT, DELETE, PATCH 를 이용해 데이터를 가져오고 반환한다.
HTTP 메서드를 실행하기 위해서는 자바에서 제공하는 HttpURLConnection 클래스를 사용할 수 있다. gzipping, Redirection, Retry, Async call 등은 OkHttp 같은 라이브러리를 통해 구현할 수 있다. 현재는 산업표준인 Retrofit 이 추천된다. 타입 안정성이 높다.
대부분의 경우 데이터는 JSON 으로 표현한다. XML 도 자주 사용되는데 데이터 크기가 더 크다. JSON 페이로드는 대부분 문자열이고 내장된 org.json 패키지와 GSON, Jackson, Moshi 를 조합해 사용할 수 있다.
마지막으로 웹에서 이미지를 효율적으로 로드하는 방법인 Moshi 를 살펴보자.
우선 AndroidManifest.xml 파일의 Application 태그 바로 앞에 다음 코드를 추가한다.
<uses-permission android:name="android.permission.INTERNET" />
그 다음 Retrofit 의존성 추가를 한다.
// https://mvnrepository.com/artifact/com.squareup.retrofit2/retrofit
implementation("com.squareup.retrofit2:retrofit:3.0.0")
Retrofit 은 엔드포인트에 대한 엑세스 스펙을 interface 로 정의한다.
interface TheCatApiService {
@GET("images/search")
fun searchImages(
@Query("limit") limit: Int,
@Query("size") format: String
) : Call<String>
}
이제 Retrofit 으로 실제 코드를 작성한다. 반환된 값을 String 으로 변환하는 방식을 넣어야 한다. 이를 위해 ScalarsConverterFactory 를 사용한다.
var retrofit = Retrofit.Builder()
.baseUrl("https://api.thecatapi.com/v1/").build()
var theCatApiService = retrofit
.create(TheCatApiService::class.java)
var retrofit = Retrofit.Builder()
.baseUrl("https://api.thecatapi.com/v1/")
.addConverterFactory(ScalarsConverterFactory.create())
.build()
val theCatApiService by lazy { retrofit.create(TheCatApiService::class.java) }
이런 Retrofit 코드는 클린 아키텍처에 따르면 레포지토리에 위치해야 한다. 이를 통해 테스트 용이성을 추가해야 한다. 레포지토리는 데이터 소스를 갖는데 네트워크용 데이터 소스가 따로 있다. 여기에 네트워크 호출을 구현해야 한다. 그리고 이 코드를 ViewModel 이 호출하는 것이다.
API 로부터 가져온 JSON 을 사용하려면 JSON 페이로드를 파싱해주는 구글의 GSON, Square 의 Moshi 가 주로 쓰인다. 이런 JSON 라이브러리는 data class를 JSON 문자열로 변환하거나(직렬화) JSON 문자열을 data class로 변환한다(역직렬화).
MvnRepository 사이트에서 retrofit converter moshi 로 검색하여 라이브러리를 찾아 추가한다. 그리고 JSON 문자열을 data class 로 변환하기 위해 data class 를 생성한다. 주로 이름은 접미사 Data 혹은 Entity 를 붙이는 것이 관례이다.
import com.squareup.moshi.Json
data class ImageResultData(
@Json(name = "url") val imageUrl: String,
val id: String,
val width: Int,
val height: Int
)
프로퍼티에 정의하지 않는 데이터는 무시된다. 위의 코드에 @Json 은 실제 JSON 이름을 다르게 쓰고 싶을 때 사용할 수 있는 어노테이션이다.
이제 새로운 Retrofit 인스턴스와 인터페이스 그리고 컨버터가 필요하다.
interface TheCatApiService {
@GET("images/search")
fun searchImages(
@Query("limit") limit: Int,
@Query("size") format: String
) : Call<List<ImageResultData>>
}
// 최근 추가된 기능인 듯. 아래 오류가 발생할 수 있음.
//
// E Caused by: java.lang.IllegalArgumentException: Cannot serialize Kotlin type com.example.catagentproject.ImageResultData. Reflective serialization of Kotlin classes without using kotlin-reflect has undefined and unexpected behavior. Please use KotlinJsonAdapterFactory from the moshi-kotlin artifact or use code gen from the moshi-kotlin-codegen artifact. (Ask Gemini)
private val moshi by lazy {
Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
}
private val retrofit by lazy {
Retrofit.Builder()
.baseUrl("https....")
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
}
private val theCatApiService by lazy {
retrofit.create(TheCatApiService::class.java) }
// ... 실제 호출 및 사용
fun getImageResponse() {
val call = theCatApiService.searchImage(1, "full") // limit, size
call.enqueue(object: Callback<List<ImageResultData>> { // TheCatApiService 반환 타입 참고.
override fun onFailure(call: Call<List<ImageResultData>>, t: Throwable) {
Log.e(
"MainActivity",
"Failed to get search results",
t)
}
override fun onResponse(
call: Call<List<ImageResultData>>,
response: Response<List<ImageResultData>>
) {
if (response.isSuccessful) {
val imageResults = response.body()
val firstImageUrl = imageResults?.firstOrNull()?.imageUrl ?: "No URL"
serverResponseView.text = "Image URL: $firstImageUrl"
} else {
Log.e(
"MainActivity",
"Failed to get search results")
}
}
})
}
이제 이미지 URL 을 이용해 이미지를 표시해야 한다. 먼저 URL에서 바이너리 스트림으로 이미지를 가져온 뒤 이 스트림을 이미지로 변환한다. 그 다음엔 비트맵 인스턴스로 변환하고 크기를 조정해서 메모리 효율성을 충족시킨다.
이 작업을 대신해주는 라이브러리는 Square 의 Picaso, Bump Technologies 의 Glide, Facebook 의 Fresco, Coil 이 있다. 여기서는 Glide 를 사용한다.
MvnRepository 에서 Glide 를 검색해서 추가한 뒤 ImageLoader 인터페이스를 추가한 뒤 ImageView 에 사용해보자.
interface ImageLoader {
fun loadImage(imageUrl: String, imageView: ImageView)
}
// context 는 Activity 또는 Fragment
class GlideImageLoader(private val context: Context): ImageLoader {
override fun loadImage(imageUrl: String, imageView: ImageView) {
Glide.with(context)
.load(imageUrl).centerCrop().into(imageView)
}
}
private val imageView: ImageView by lazy {
findViewById(R.id.image_view) }
private val imageLoader by lazy {
GlideImageLoader(this) }
// ...Retrofit, Moshi 를 이용해 데이터 클래스로 JSON 을 변환함(역직렬화)
if (response.isSuccessful) {
val body = response.body()
val result = body?.firstOrNull()
if (result != null) {
val imageUrl = result.imageUrl
imageLoader.loadImage(imageUrl, imageView)
} else {
Log.d("MainActivity", "Failed to get search results")
}
}