Android 공부 (5)

백상휘·2025년 10월 18일
0

Android

목록 보기
3/5

현실 개발업무 시 중요한 원격 서버의 응답을 다루는 방법을 배운다. Retrofit 을 사용해 네트워크 엔드포인트에서 데이터를 가져오고, Moshi 를 이용해 JSON 페이로드를 코틀린 데이터 객체로 파싱하고 Glide 를 사용해 ImageView 에 이미지를 로드하는 방법을 알 수 있게 된다.

여기서 다룰 내용은 아래와 같다.

  • REST, API, JSON, XML 소개
  • 네트워크 엔드포인트에서 데이터 가져오기
  • JSON 응답 파싱
  • 원격 URL에서 이미지 로딩

REST, API, JSON, XML 소개

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>
}
  • GET(@GET) 메소드를 사용해 HTTP 통신을 수행한다.
  • 컨텍스트 패스는 images/search, 함수 이름은 searchImages 이다. 둘을 연관지어 적절한 이름을 짓는다.
  • HTTP Query (@Query) 로 파라미터를 지정한다.
    • 이외에도 @Path 를 이용해 url path 를 사용할 수도 있다.
    • @Header, @Headers, @HeaderMap 으로 헤더를 사용할 수 있다.
    • @Body 는 POST, PUT 에서 본문 내용을 전달한다.
  • 반환 타입인 Call 인터페이스는 네트워크 요청을 동기 혹은 비동기적으로 실행하는데 사용한다.
    • 코루틴에서는 suspend 로 대체 가능

이제 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 이 호출하는 것이다.

JSON 응답 파싱

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 을 이용해 이미지를 표시해야 한다. 먼저 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")
    }
}
profile
plug-compatible programming unit

0개의 댓글