본문 바로가기
Android

[Android] 안드로이드 Retrofit(레트로핏) 사용법 - 경기도데이터드림 OPEN API 사용해보기

by JongSeok 2023. 1. 13.

Retrofit 이란?

안드로이드에서 Retrofit은 서버와 클라이언트 간 HTTP API 통신을 할 때 사용하는 라이브러리입니다.

REST API 기반의 웹 서비스를 통해 JSON 구조의 데이터를 쉽게 가져오고 업로드할 수 있습니다.

 

또한, 이전에 사용되던 HttpClient와 달리 Retrofit은 별도의 Async Task 없이 자체적으로 백그라운드에서 쓰레드를 관리합니다. 이로 인해, 속도와 성능이 크게 개선되었고, 백그라운드에서 Callback을 통해 메인 쓰레드의 UI를 갱신합니다.

 

Retrofit 인터페이스에서 어노테이션(@)을 사용해 서버에 요청할 함수와 파라미터를 미리 정의해 놓고, 네트워크 통신이 필요할 때 해당 함수를 호출하는 형태이기 때문에 코드를 작성하기도 수월하고 가독성도 좋다는 특징도 있습니다.

 

안드로이드 개발에서 가장 많이 사용되는 라이브러리 중 하나이고, 매우 유용하게 사용하기 때문에 반드시 알아둬야겠습니다-!


Retrofit 사용법

Retrofit을 사용하기 위해서는 크게 세 가지 클래스가 필요합니다.

 

  1. JSON 형태의 모델 클래스(data class)
  2.  HTTP 통신에 사용할 인터페이스 정의
  3. Retrofit.Builder

먼저 app수준 build.gradle에 다음을 추가합니다. converter-moshi은 JSON 형식의 응답 결과를 object 형식으로 변환시켜 주기 위해 추가했습니다.

    // Retrofit
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'

    // OkHttp
    implementation 'com.squareup.okhttp3:okhttp:4.10.0'
    implementation 'com.squareup.okhttp3:logging-interceptor:4.10.0'

그리고 HTTP 통신을 위해 AndroidManifest.xml에 인터넷 사용 권한을 추가합니다.

<uses-permission android:name="android.permission.INTERNET" />

 

경기도 공공배달앱 배달특급 가맹점 | 데이터셋 상세 Open API | 경기데이터드림

경기도 공공배달앱 배달특급 가맹점 리스트입니다. 배달특급 가맹점명, 주소, 사업자등록번호 등에 대한 정보를 제공합니다. ※ 데이터 원본 출처 : 배달특급 스마트오더 사장님 사이트

data.gg.go.kr

저는 경기도데이터드림 사이트의 '경기도 공공배달앱 배달특급 가맹점' 오픈 API를 이용하겠습니다.
회원가입 후 인증키를 신청해서 인증키를 발급받은 상황에서 진행하겠습니다.

 

먼저 API 통신 시 Model 역할을 할 data class를 생성해야 합니다.

JSON 형식의 샘플을 데이터 클래스로 생성하기 위해 해당 주소에서 샘플을 복사합니다.

https://openapi.gg.go.kr/GGEXPSDLV?type=json 

 

안드로이드 스튜디오에는 JSON 형식의 파일을 data class로 변환해 주는 JSON to Kotlin Class 플러그인이 존재합니다.

해당 플러그인이 설치되어 있지 않다면 Marketplace에서 설치합니다.

data class를 생성할 패키지에서 위에서 복사한 JSON 샘플을 변환합니다.

'Format'을 클릭하면 보기 좋게 정렬이 되고 'Advanced'에서 세부설정이 가능한데 저는 기본값으로 Generate했습니다.

data class ApiResponse(
    @field:Json(name = "GGEXPSDLV")
    val gGEXPSDLV: List<GGEXPSDLV>
)

data class GGEXPSDLV(
    @field:Json(name = "head")
    val head: List<Head>,
    @field:Json(name = "row")
    val row: List<Row>
)

data class Head(
    @field:Json(name = "api_version")
    val apiVersion: String,
    @field:Json(name = "list_total_count")
    val listTotalCount: Int,
    @field:Json(name = "RESULT")
    val rESULT: RESULT
)

data class RESULT(
    @field:Json(name = "CODE")
    val cODE: String,
    @field:Json(name = "MESSAGE")
    val mESSAGE: String
)

data class Row(
    @field:Json(name = "BIZREGNO")
    val bIZREGNO: String,
    @field:Json(name = "INDUTYPE_NM")
    val iNDUTYPENM: String,
    @field:Json(name = "REFINE_LOTNO_ADDR")
    val rEFINELOTNOADDR: String,
    @field:Json(name = "REFINE_ROADNM_ADDR")
    val rEFINEROADNMADDR: String,
    @field:Json(name = "REFINE_WGS84_LAT")
    val rEFINEWGS84LAT: String,
    @field:Json(name = "REFINE_WGS84_LOGT")
    val rEFINEWGS84LOGT: String,
    @field:Json(name = "REFINE_ZIPNO")
    val rEFINEZIPNO: String,
    @field:Json(name = "SIGUN_NM")
    val sIGUNNM: String,
    @field:Json(name = "STR_NM")
    val sTRNM: String
)

처음 data class를 생성하면 @field:Json이 아니라 @Json으로 생성되는데 @Json만으로는 Kotlin에서 변환할 수 없기 때문에 @field:Json으로 고쳐줍니다. (변경할 부분 그래그 → Ctrl + R)

 

build.gradle에 converter-moshi 의존성을 추가한 이유입니다.

객체와 컴퓨터에서 인식할 수 있는 포맷으로 변환하는 과정을 직렬화라고 하는데 우리가 사용할 포맷이 'JSON'이고 객체와 JSON 파일의 형태로 변환해 주는 라이브러리가 Moshi입니다.

 

item_rv_store.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/layout_viewholder"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="?attr/selectableItemBackground"
    android:gravity="center_vertical"
    android:orientation="vertical"
    android:padding="10dp">

    <TextView
        android:id="@+id/tv_name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="3dp"
        android:text="매장명"
        android:textSize="24sp" />

    <TextView
        android:id="@+id/tv_name2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="3dp"
        android:text="업종"
        android:textSize="18sp" />

    <TextView
        android:id="@+id/tv_address"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="주소"
        android:textSize="18sp" />

    <TextView
        android:id="@+id/tv_phone"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="전화번호"
        android:textSize="18sp" />

    <TextView
        android:id="@+id/tv_logt"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="경도"
        android:textSize="18sp" />

    <TextView
        android:id="@+id/tv_lat"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="위도"
        android:textSize="18sp" />

</LinearLayout>

RecyclerView의 아이템이 될 레이아웃입니다.

API 응답 중 필요한 정보를 사용해 매장 리스트를 구성하겠습니다.

StoreAdapter

class StoreAdapter: ListAdapter<Row,StoreAdapter.StoreViewHolder>(DiffUtilCallback) {
    inner class StoreViewHolder(private val binding: ItemRvStoreBinding): RecyclerView.ViewHolder(binding.root) {
        fun bind(row: Row) {
            binding.tvName.text = row.sTRNM    // 매장명
            binding.tvName2.text = row.iNDUTYPENM    // 업종
            binding.tvAddress.text = row.rEFINELOTNOADDR    // 지번주소
            binding.tvLat.text = row.rEFINEWGS84LAT    // 위도
            binding.tvLogt.text = row.rEFINEWGS84LOGT    // 경도
        }
    }
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StoreViewHolder {
        val binding = ItemRvStoreBinding.inflate(LayoutInflater.from(parent.context),parent,false)
        return StoreViewHolder(binding)
    }

    override fun onBindViewHolder(holder: StoreViewHolder, position: Int) {
        holder.bind(currentList[position])
    }
}

object DiffUtilCallback : DiffUtil.ItemCallback<Row>() {
    override fun areItemsTheSame(oldItem: Row, newItem: Row): Boolean {
        return oldItem.hashCode() == newItem.hashCode()
    }

    override fun areContentsTheSame(oldItem: Row, newItem: Row): Boolean {
        return oldItem == newItem
    }
}

Row 클래스의 정보를 RecyclerView의 ViewHolder에 뿌려 줄 Adapter를 작성합니다.

 

ListAdapter와 DiffUtil의 상세내용은 'ListAdapter의 작동원리'를 살펴보시기 바랍니다.

 

[Android] ListAdapter 사용 중 리스트가 갱신되지 않은 문제

문제상황 문제 상황입니다. 수정을 시도하고 있지만 확인 버튼을 눌러도 리스트가 갱신되지 않고 있습니다. 로그를 확인한 결과 LiveData, ViewModel, ListAdapter에는 문제가 없었습니다. RecyclerView는 Lis

develop-oj.tistory.com

MainActivity

class MainActivity : AppCompatActivity() {
    private val binding by lazy {
        ActivityMainBinding.inflate(layoutInflater)
    }
    private lateinit var storeAdapter: StoreAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        storeAdapter = StoreAdapter()

        binding.rvStore.apply {
            setHasFixedSize(true)
            layoutManager = LinearLayoutManager(this@MainActivity)
            adapter = storeAdapter
            addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
        }
    }
}

MainActivity에서 뷰 바인딩과 StoreAdapter를 RecyclerView와 연결해 줍니다.

RetrofitService

interface RetrofitService {
    @GET("GGEXPSDLV")
    fun getStoreData(
        @Query("KEY") KEY: String,
        @Query("Type") Type: String
    ) : Call<ApiResponse>
}

RetrofitService 인터페이스를 정의합니다.

경기도 공공배달앱 배달특급 가맹점 샘플

요청 주소는 위와 같고, KEY와 Type의 기본값은 sample key와 xml이므로 각각 고유키와 json으로 요청하기 위해 getStoreData의 파라미터로 정의했습니다.

RetrofitInstance

object RetrofitInstance {
    const val API_KEY = "마이페이지 -> 인증키 발급 내역"

    private val okHttpClient: OkHttpClient by lazy {
        val httpLoggingInterceptor = HttpLoggingInterceptor()
            .setLevel(HttpLoggingInterceptor.Level.BODY)
        OkHttpClient.Builder()
            .addInterceptor(httpLoggingInterceptor)
            .build()
    }

    private val retrofit: Retrofit by lazy {
        Retrofit.Builder()
            .addConverterFactory(MoshiConverterFactory.create())
            .client(okHttpClient)    // Logcat에서 패킷 내용을 로그로 남기는 속성
            .baseUrl("https://openapi.gg.go.kr/")
            .build()
    }

    val retrofitService: RetrofitService by lazy {
        retrofit.create(RetrofitService::class.java)
    }
}

RetrofitInstance 객체를 매번 생성하면 비용이 너무 높기 때문에 object 키워드를 사용해 싱글톤으로 한 번만 생성한 후 필요시 RetrofitInstance를 호출하여 retrofitService 객체에 접근합니다.

 

Retrofit.Builder()를 통해 retrofit 객체를 생성합니다. 저는 JSON 컨버터로 Moshi 라이브러리를 이용하고 있기 때문에 addConverterFactory에 MoshiConverterFactory로 지정하고 baseUrl은 요청주소를 참고합니다.

 

API_KEY는 회원별로 고유한 키가 생성되므로 마이페이지에서 확인해서 넣어줍니다.

Retrofit 작동

private fun retrofitWork() {
    val service = RetrofitInstance.retrofitService

    service.getStoreData(RetrofitInstance.API_KEY,"json")
        .enqueue(object : retrofit2.Callback<ApiResponse> {
            override fun onResponse(call: Call<ApiResponse>, response: Response<ApiResponse>) {
                val result = response.body()?.gGEXPSDLV?.get(1)?.row
                storeAdapter.submitList(result!!)
            }

            override fun onFailure(call: Call<ApiResponse>, t: Throwable) {
                Log.d("태그",t.message.toString())
            }
        })
}

먼저 RetrofitInstance에서 싱글톤으로 작성했던 retrofitService 객체를 생성합니다.

getStoreData()는 KEY와 Type을 인자로 받도록 인터페이스에서 정의했기 때문에 API_KEY와 "json"을 넘겨줍니다.

 

enquere()는 request를 백그라운드에서 비동기적으로 response는 메인 쓰레드에서 콜백으로 받습니다.

enqueue가 성공했을 때에는 onResponse, 실패했을 때에는 onFailure가 호출됩니다.

 

저는 요청이 성공했을 경우에 응답을 RecyclerView에 전달에 UI를 갱신할 수 있도록 작성했습니다.


실행결과

실행 결과

 

Retrofit 호출하고 응답을 처리하는 과정을 코루틴을 사용해 좀 더 효율적으로 개선해 봐야겠습니다!!


코루틴(Coroutine) 적용해보기 (추가)

retrofitWork()에서 service에 enqueue하는 과정은 request는 백그라운드에서, response의 콜백은 메인 쓰레드에서 처리했습니다. 하지만 코루틴을 사용하면 콜백을 통하지 않고 동일한 처리가 가능합니다.

RetrofitService

interface RetrofitService {
//    @GET("GGEXPSDLV")
//    fun getStoreData(
//        @Query("KEY") KEY: String,
//        @Query("Type") Type: String
//    ) : Call<ApiResponse>

    @GET("GGEXPSDLV")
    suspend fun getStoreData(
        @Query("KEY") KEY: String,
        @Query("Type") Type: String
    ) : Response<ApiResponse>
}

Service 인터페이스를 수정합니다. 코루틴 수행을 위해 suspend 키워드를 붙여주고, 코루틴을 통해 비동기 처리를 하기 때문에 반환 값을 Call이 아닌 Response 객체를 반환하도록 변경합니다.

RetrofitWork

private fun retrofitWork() {
    val service = RetrofitInstance.retrofitService

    CoroutineScope(Dispatchers.IO).launch {
        val response = service.getStoreData(API_KEY,"json")

        withContext(Dispatchers.Main) {
            if (response.isSuccessful) {
                val result = response.body()?.gGEXPSDLV?.get(1)?.row
                storeAdapter.submitList(result!!)
            } else {
                Log.d("태그",response.code().toString())
            }
        }
    }
}

이제 retrofitWork 

Service를 호출하는 작업은 IO 쓰레드에서, UI를 변경하는 작업은 메인 쓰레드에서 수행하도록 스위칭합니다.

728x90
반응형