Retrofit을 처음 사용하면서 있었던 이것저것, 그리고 문제들

김성진·2024년 8월 2일
0

Retrofit

(자료 및 내용에 대한 출처)

Retrofit이란?

정의

  • 서버와 앱 간의 데이터 전달(?)을 위한 HTTP 클라이언트 라이브러리
  • REST API의 HTTP 요청을 받아서 자바 인터페이스(?)로 변환하는 것이 주 목적

일단 알아보기 전에

  • 이걸 알기 위해선 서버, 클라이언트, JSON... 등을 알아야 한다... ㅋㅋㅋ

서버와 클라이언트

  • 서버 : 데이터나 리소스를 제공하는 시스템. 사용자의 요청을 기다리고, 요청이 들어오면 그에 맞는 응답을 전송한다.

  • 클라이언트 : 사용자를 대표하여 서버에 정보나 서비스를 요청하는 시스템. 웹 브라우저, 모바일 앱, 데스크톱 앱 등... 형태가 있다.

  • 이렇게 보면 무슨 소리인지 모를 것 같아서... 예시를 들자면

    -> 서버는 식당으로 따지면 점원의 역할과 같다.
    -> 클라이언트는 음식을 주문하는 손님.... 과 같다(?)
    -> 그 둘 사이의 소통을 하는 방법이 있듯이 웹 서버와 클라이언트 간의 소통도 방법이 있다.
    -> 클라이언트가 서버로 필요한 데이터를 요청하면 서버가 그걸 받아서 자기만의 방식으로 준다. 그걸 다시 가공해서(?) 코드에 적용시킨다.

통신 방식

  • 어어엄청 많은데 사실 이번에 주로 배운 건 HTTP이다.

  • 주요 통신 방식들
    -> HTTP/HTTPS: 웹 기반의 애플리케이션에서 주로 사용됩니다. REST API나 SOAP와 같은 웹 서비스 통신 방식에서 기반이 됩니다.
    -> WebSockets: 실시간 양방향 통신이 필요한 애플리케이션 (예: 채팅 애플리케이션, 실시간 게임)에서 사용됩니다.
    -> Socket (TCP/UDP): TCP 또는 UDP 프로토콜을 사용하여 데이터를 전송합니다. 지속적인 연결을 유지하면서 양방향 통신이 가능합니다.
    -> FTP (File Transfer Protocol): 파일 전송에 특화된 프로토콜입니다. 클라이언트와 서버 간의 파일 전송을 위해 사용됩니다.
    -> RPC (Remote Procedure Call): 네트워크를 통해 다른 주소 공간에 있는 프로그램의 프로시저(또는 함수)를 호출하는 방식입니다.
    -> SOAP (Simple Object Access Protocol): XML 기반의 메시징 프로토콜로, 주로 웹 서비스에서 사용됩니다.
    -> GraphQL: 페이스북이 개발한 데이터 쿼리 및 조작 언어로, 클라이언트가 필요한 데이터의 구조를 명시하여 서버로부터 데이터를 받아옵니다.
    -> gRPC: 구글이 개발한 RPC 프로토콜로, 효율적인 양방향 통신을 지원하며 여러 프로그래밍 언어를 지원합니다.
    -> MQTT: IoT (Internet of Things) 기기와 같은 경량 클라이언트에서 사용되는 메시지 프로토콜입니다.
    -> Message Queues (예: RabbitMQ, Kafka): 비동기 메시징을 위한 시스템으로, 메시지를 전송하는 생산자와 메시지를 받아 처리하는 소비자 사이에서 메시지를 전송합니다.

프로토콜(?)

  • 클라이언트와 서버는 서로 "HTTP"라는 프로토콜(?)이라는 것으로 대화를 나눈다.
  • 프로토콜은 서로 대화하기 위한 약속? 같은 것이다.

API (중요)

  • 서버는 클라이언트가 서버를 잘 활용해서 쓸 수 있도록 인터페이스(?)라는 것을 제공한다.
  • 식당에서 서로 소통할 때 메뉴판을 쓰듯 서버의 리소스 사용 설명서? 라고 생각하면 된다.
  • 각 서버 별로 API 문서가 있는데 서버 개발자가 그것을 앱 개발자에게 주면, 우린 그걸 쓰면 된다.

REST API

REST API? 그게 뭔데?

  • Respresentational State Transfer Application Programming Interface의 약자이다.
  • 웹의 장점을 최대한 활용 가능한 아키텍처(?) 스타일 이라고 한다...
  • 아무튼 우린 서버의 API 중 REST API를 가져다 쓴다? 라고 이해하고 있다.

JSON

정의

  • 데이터를 저장하거나 전송할 때 사용되는 경량의 Data 교환 형식
  • JSON이라는 형식으로 서버로부터 데이터를 가지고 온다.
  • NAME과 VALUE로 구성 되어 있다.
  • NAME은 String타입이며, VALUE는 여러 값들이 다양하게 온다.

예시

"dog": [
    {"name" : "식빵", "family" : "웰시코기", "weight" = 1.5},
    {"name" : "콩콩", "family" : "푸들", "weight" = 3.2},
    {"name" : "강아지", "family" : "포메라니안", "weight" = 2.45}
]

이런 식이다.

GSON

정의

  • 웹 서버는 JSON의 형태로 데이터를 제공해 준다.
  • 근데 그걸 Kotlin은 그걸 해석할 줄 몰라서 Kotlin으로 변환해서 넣어주어야 한다. 이걸 GSON이 도와준다.

사용법

  • JSON -> Kotlin
val myClassInstance: MyClass = gson.fromJson(jsonString, MyClass::clss.java)
  • 데이터 클래스에서 Kotlin에 쓰고 싶은 값 이름이 JSON 키 이름이랑 다를 경우
파라미터 정의
data class Person (
    @SerializedName("person_name")   <- 이게 문서에 적힌 이름..?
    val name : String                <- 이게 내가 실제로 선언한 변수
)

실제로 사용해보기 (간단한(?) 미세먼지 앱)

전체 코드

// MainActivity.kt

package com.example.androidappcodingtesttraining

import android.graphics.Color
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.example.androidappcodingtesttraining.databinding.ActivityMainBinding
import com.skydoves.powerspinner.IconSpinnerAdapter
import kotlinx.coroutines.launch

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

    private var items = mutableListOf<DustItem>()   // 미세먼지 데이터를 여기다 저장한다.

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

       
		// 시나 도를 나타내는 데이터를 받아온 데이터 클래스에서 꺼내오는 메소드를 호출
    	binding.spinnerViewSido.setOnSpinnerItemSelectedListener<String> { _, _, _, text ->
            communicateNetWork(setUpDustParameter(text))
        }
        
		// 구나 시를 클릭하면 뽑아낸 시/도랑 구/시를 텍스트에 출력함
        binding.spinnerViewGoo.setOnSpinnerItemSelectedListener<String> { _, _, _, text ->

            Log.d("miseya", "selectedItem: spinnerViewGoo selected >  $text")
            // 파라미터들이 담긴 items에서 stationName을 기준으로 필터링해서 selectedItem에 저장
            var selectedItem = items.filter { f -> f.stationName == text }
            Log.d("miseya", "selectedItem: sidoName > " + selectedItem[0].sidoName)
            Log.d("miseya", "selectedItem: pm10Value > " + selectedItem[0].pm10Value)

			// 무슨 원리인지는 모르겠지만 예를 들어서 서울의 송파구를 선택해 selectedItem으로 들어오면
            // 그 송파구에 해당하는 각각의 데이터들을 각각의 위젯에 값으로 설정한다.
            binding.tvCityname.text = selectedItem[0].sidoName + "  " + selectedItem[0].stationName
            binding.tvDate.text = selectedItem[0].dataTime
            binding.tvP10value.text = selectedItem[0].pm10Value + " ㎍/㎥"

			// 미세먼지 지수를 바탕으로 UI 설정
            when (getGrade(selectedItem[0].pm10Value)) {
                1 -> {
                    binding.mainBg.setBackgroundColor(Color.parseColor("#9ED2EC"))
                    binding.ivFace.setImageResource(R.drawable.mise1)
                    binding.tvP10grade.text = "좋음"
                }

                2 -> {
                    binding.mainBg.setBackgroundColor(Color.parseColor("#D6A478"))
                    binding.ivFace.setImageResource(R.drawable.mise2)
                    binding.tvP10grade.text = "보통"
                }

                3 -> {
                    binding.mainBg.setBackgroundColor(Color.parseColor("#DF7766"))
                    binding.ivFace.setImageResource(R.drawable.mise3)
                    binding.tvP10grade.text = "나쁨"
                }

                4 -> {
                    binding.mainBg.setBackgroundColor(Color.parseColor("#BB3320"))
                    binding.ivFace.setImageResource(R.drawable.mise4)
                    binding.tvP10grade.text = "매우나쁨"
                }
            }
        }
    }

    private fun communicateNetWork(param: HashMap<String, String>) = lifecycleScope.launch() {
    	// 클라이언트는 getDust라는 함수로 연결된다. (interface에 있다)
        val responseData = NetWorkClient.dustNetWork.getDust(param)
        Log.d("Parsing Dust ::", responseData.toString())

		// SpinnerAdapter를 통해서 items에 파라미터들을 저장 (dustBody -> dustItem)
        val adapter = IconSpinnerAdapter(binding.spinnerViewGoo)
        items = responseData.response.dustBody.dustItem!!

		// items의 여러 파라미터 중 stationName을 goo 변수에 리스트로 저장
        val goo = ArrayList<String>()
        items.forEach {
            Log.d("add Item :", it.stationName)
            goo.add(it.stationName)
        }

		// spinnerViewGoo라는 이름의 spinnerView에 그 goo를 차례로 넣는다.
        runOnUiThread {
            binding.spinnerViewGoo.setItems(goo)
        }

    }

    private fun setUpDustParameter(sido: String): HashMap<String, String> {
    	// 키를 API서버 문서에서 받아서 여기다 적기
        val authKey =
            "QZUxotLLH0wmG17OBRLz8yAvsGZikpfmdTFG/ALLJ/PVLOoSOulqicLZFURik7KII8Ygk664CEW7Izd0RXS3GA=="

		// 이건 요청 파라미터들 (결과값 파라미터들은 데이터 클래스에)
		// interface에 선언해야 하는데 특별히 여기로 옮김
        return hashMapOf(
            "serviceKey" to authKey,
            "returnType" to "json",
            "numOfRows" to "100",
            "pageNo" to "1",
            "sidoName" to sido,
            "ver" to "1.0"
        )
    }

	// 미세먼지 지수가 들어오면 각각 해당하는 숫자를 반환
	// 위에서 그 숫자를 가지고 등급을 나누어 UI를 바꾼다.
    fun getGrade(value: String): Int {
        val mValue = value.toInt()
        var grade = 1
        grade = if (mValue >= 0 && mValue <= 30) {
            1
        } else if (mValue >= 31 && mValue <= 80) {
            2
        } else if (mValue >= 81 && mValue <= 100) {
            3
        } else 4
        return grade
    }
}

// DustDTO.kt

// 각각의 파라미터들을 문서에 나와있는 상태 그대로 선언해 주면 되는데...
// 너무 많을 때도 있으니 공식 문서에서 받아서 이렇게 바꿔주는 flugin을 쓰드록 하자.

package com.example.androidappcodingtesttraining

import com.google.gson.annotations.SerializedName

data class Dust(val response: DustResponse)

data class DustResponse(
    @SerializedName("body")
    val dustBody: DustBody,
    @SerializedName("header")
    val dustHeader: DustHeader
)

data class DustBody(
    val totalCount: Int,
    @SerializedName("items")
    val dustItem: MutableList<DustItem>?,
    val pageNo: Int,
    val numOfRows: Int
)

data class DustHeader(
    val resultCode: String,
    val resultMsg: String
)

data class DustItem(
    val so2Grade: String,
    val coFlag: String?,
    val khaiValue: String,
    val so2Value: String,
    val coValue: String,
    val pm25Flag: String?,
    val pm10Flag: String?,
    val o3Grade: String,
    val pm10Value: String,
    val khaiGrade: String,
    val pm25Value: String,
    val sidoName: String,
    val no2Flag: String?,
    val no2Grade: String,
    val o3Flag: String?,
    val pm25Grade: String,
    val so2Flag: String?,
    val dataTime: String,
    val coGrade: String,
    val no2Value: String,
    val stationName: String,
    val pm10Grade: String,
    val o3Value: String
)

// NetWorkInterface.kt

package com.example.androidappcodingtesttraining

import retrofit2.http.GET
import retrofit2.http.QueryMap

interface NetWorkInterface {
	// GET 키워드를 통해 받는다.
    @GET("getCtprvnRltmMesureDnsty")   //  End Point
    suspend fun getDust(@QueryMap param: HashMap<String, String>): Dust
}

// Main Activity에서 HashMap 형태로 선언되어서 여기에는 비어있다.

// NetworkClient.kt

package com.example.androidappcodingtesttraining

import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit

object NetWorkClient {

	// BaseUrl 이거도 문서에 적혀 있다.
    private const val DUST_BASE_URL = "http://apis.data.go.kr/B552584/ArpltnInforInqireSvc/"

    private fun createOkHttpClient(): OkHttpClient {
        val interceptor = HttpLoggingInterceptor()

//        if (BuildConfig.DEBUG)
            interceptor.level = HttpLoggingInterceptor.Level.BODY
//        else
//            interceptor.level = HttpLoggingInterceptor.Level.NONE

        return OkHttpClient.Builder()
            .connectTimeout(20, TimeUnit.SECONDS)
            .readTimeout(20, TimeUnit.SECONDS)
            .writeTimeout(20, TimeUnit.SECONDS)
            .addNetworkInterceptor(interceptor)
            .build()
    }

    private val dustRetrofit = Retrofit.Builder()
        .baseUrl(DUST_BASE_URL)
        .addConverterFactory(GsonConverterFactory.create())
        .client(createOkHttpClient())    // 이거는 써도 되고, 안 써도 된다.
        .build()

	// 이게 문제의 JSON 객체 생성 및 변환
    val dustNetWork: NetWorkInterface = dustRetrofit.create(NetWorkInterface::class.java)

}

API 문서에서 직접 값들을 확인해보기

API 문서 링크

코드와 문서 비교

  • 인증키
// MainActivity.kt에서 나온 코드 (원래는 interface에 있어야 맞다)
val authKey = "QZUxotLLH0wmG17OBRLz8yAvsGZikpfmdTFG/ALLJ/PVLOoSOulqicLZFURik7KII8Ygk664CEW7Izd0RXS3GA=="
package com.example.androidappcodingtesttraining

import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit

object NetWorkClient {

	// 바로 여기!
    private const val DUST_BASE_URL = "http://apis.data.go.kr/B552584/ArpltnInforInqireSvc/"

    private fun createOkHttpClient(): OkHttpClient {
        val interceptor = HttpLoggingInterceptor()

//        if (BuildConfig.DEBUG)
            interceptor.level = HttpLoggingInterceptor.Level.BODY
//        else
//            interceptor.level = HttpLoggingInterceptor.Level.NONE

        return OkHttpClient.Builder()
            .connectTimeout(20, TimeUnit.SECONDS)
            .readTimeout(20, TimeUnit.SECONDS)
            .writeTimeout(20, TimeUnit.SECONDS)
            .addNetworkInterceptor(interceptor)
            .build()
    }

    private val dustRetrofit = Retrofit.Builder()
        .baseUrl(DUST_BASE_URL)
        .addConverterFactory(GsonConverterFactory.create())
        .client(createOkHttpClient())
        .build()

    val dustNetWork: NetWorkInterface = dustRetrofit.create(NetWorkInterface::class.java)

}
  • End_Point
    -> 요청 주소에서 서비스 Uri 뺀 나머지 뒷 부분 (ArpltnInforInqireSvc 뒤에 있는 거)를 적는 코드
package com.example.androidappcodingtesttraining

import retrofit2.http.GET
import retrofit2.http.QueryMap

interface NetWorkInterface {
    @GET("getCtprvnRltmMesureDnsty")  // 여기에 들어간다.
    suspend fun getDust(@QueryMap param: HashMap<String, String>): Dust
}
  • Build.gradle 설정
dependencies {
    implementation("com.google.code.gson.gson:2.10.1")
    implementation("com.squareup.okhttp3:okhttp:4.10.0")
    implementation("com.squareup.okhttp3:logging-interceptor:4.10.0")
    implementation("com.squareup.retrofit2:retrofit:2.9.0")
    implementation("com.squareup.retrofit2:converter-gson:2.9.0")
}

android {
    buildFeatures {
        viewBinding = true
        buildConfig = true
    }
}
  • AndroidManifest.kt
<uses-permission android:name="android.permission.INTERNET"/>
profile
김성진의 개발 관련 내용 정리 블로그

0개의 댓글