๊พธ์ค€ํ•จ์ด ์ง„๋ฆฌ๋‹ค!!

์–ด์ œ๋ณด๋‹ค ๋ฐœ์ „ํ•œ ์˜ค๋Š˜์ด ๋˜๊ณ ํ”ˆ ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป ์˜ ๋ธ”๋กœ๊ทธ

Android/Kotlin

[Kotlin][Android] ์•ˆ๋“œ๋กœ์ด๋“œ REST ํ†ต์‹  - Retrofit2(๋ ˆํŠธ๋กœํ•)์˜ ์‚ฌ์šฉ๋ฒ•์— ๋Œ€ํ•ด์„œ

๋ށ์š” 2022. 11. 7. 18:05

์•ˆ๋…•ํ•˜์„ธ์š” ๐Ÿ‘‹

์˜ค๋Š˜์€ ์•ˆ๋“œ๋กœ์ด๋“œ์—์„œ Rest ํ†ต์‹ ์„ ํ•˜๋Š” ๋ฒ•๊ณผ

Retrofit2์˜ ์‚ฌ์šฉ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ๊ณต๋ถ€ํ•œ ๋‚ด์šฉ์„ ํฌ์ŠคํŒ…ํ•˜๋ ค ํ•ฉ๋‹ˆ๋‹ค.

 

REST(RE’presentational ‘S’tate ‘T’ransfer)๋ž€?

HTTP URI(Uniform Resource Identifier)๋ฅผ ํ†ตํ•ด ์ž์›(Resource)์„ ๋ช…์‹œํ•˜๊ณ , HTTP Method(POST, GET, PUT, DELETE)๋ฅผ ํ†ตํ•ด ํ•ด๋‹น ์ž์›์— ๋Œ€ํ•œ CRUD Operation์„ ์ ์šฉํ•˜๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

 

๊ทธ๋ ‡๋‹ค๋ฉด Retrofit2์ด๋ž€ ๋ฌด์—‡์ผ๊นŒ์š”?

 

Retrofit2 ๋ž€?

OkHttp๋ฅผ ๋งŒ๋“  Square์‚ฌ์—์„œ ๋งŒ๋“  ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ

Annotation์„ ์ด์šฉํ•˜์—ฌ REST๋ฅผ ๋‹ค๋ฃฐ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

(OkHttp๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ reflect๋ฅผ ์ ์šฉํ•˜์—ฌ ๊ฐœ๋ฐœ)

 


๊ทธ๋ ‡๋‹ค๋ฉด Retrofit2๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ Http ํ†ต์‹ ์„ ํ•˜๋Š” ๋ฒ•์„ ์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ๐Ÿ˜Ž

 

1. ์ธํ„ฐ๋„ท ํ†ต์‹ ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” permission์„ manifestํŒŒ์ผ์— ์ถ”๊ฐ€ํ•ด์ค๋‹ˆ๋‹ค.

//์ธํ„ฐ๋„ท ์‚ฌ์šฉ Permission ์ถ”๊ฐ€

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

 

2. ์ „๋‹ฌ๋ฐ›์„ JSON ๋ฐ์ดํ„ฐ์— ๋”ฐ๋ผ model class๋ฅผ ํ•˜๋‚˜ ์ƒ์„ฑํ•ด์ค๋‹ˆ๋‹ค.

(์ œ๊ฐ€ ์‚ฌ์šฉํ•œ ๋ฐ์ดํ„ฐ๋Š” ๊ณต๊ณต๋ฐ์ดํ„ฐ ํฌํ„ธ์— ์žˆ๋Š” 'ํ•ด์ˆ˜์š•์žฅ ํ˜ผ์žก๋„'๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ๋กœ JSON ๋ฐ์ดํ„ฐ๋Š” ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.)

"Beach7": {
        "etlDt": "202209021330",
        "seqId": 33,
        "poiNm": "๋ด‰์ˆ˜๋Œ€",
        "uniqPop": 691,
        "congestion": "1"
    },
    "Beach8": {
        "etlDt": "202209021330",
        "seqId": 34,
        "poiNm": "๋ด‰ํฌ๋ฆฌ",
        "uniqPop": 1152,
        "congestion": "1"
    },
    "Beach9": {
        "etlDt": "202209021330",
        "seqId": 35,
        "poiNm": "์‚ผํฌ",
        "uniqPop": 738,
        "congestion": "1"
    },

์œ„์™€ ๊ฐ™์€ JSON์„ ๋‹ด์„ class๋ฅผ ํ•˜๋‚˜ ๋งŒ๋“ค์–ด ์ค๋‹ˆ๋‹ค.

data class BeachCongestionModel(
    val etlDt : String = "",
    val seqId : Int = -1,
    val poiNm : String = "",
    val uniqPop : Int = -1,
    var congestion : String = ""
)
class BeachCongestionList {
    private lateinit var mBeachList : MutableList<BeachCongestionModel>

    @SerializedName("Beach0")
    val beach0 : BeachCongestionModel? = null
    ... // ๋ฐ‘์— ๋” ๋งŽ์€ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์ง€๋งŒ ์ƒ˜ํ”Œ ํ™”๋ฉด์„ ์œ„ํ•˜์—ฌ ์ƒ๋žตํ•˜๊ณ  
    // ์ด๋ฒˆ ํฌ์ŠคํŒ…์—์„œ๋Š” ํ•ด๋ณ€์ •๋ณด ํ•˜๋‚˜๋งŒ์„ ๊ฐ€์ ธ์™€ ์‚ฌ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค.
}

 

3. ๋ ˆํŠธ๋กœํ•์„ ํ†ตํ•ด ํ†ต์‹ ํ•  ์ˆ˜ ์žˆ๋Š” interface๋ฅผ ํ•˜๋‚˜ ๋งŒ๋“ค์–ด ์ค๋‹ˆ๋‹ค.

interface BeachCongestionService {
    @GET("/seantour_map/travel/getBeachCongestionApi.do")
    suspend fun getBeachCongestion() : Response<BeachCongestionList>

    companion object{
        private lateinit var instance : BeachCongestionService

        fun getInstance() : BeachCongestionService{
            if(!::instance.isInitialized){
                val retrofit = Retrofit.Builder()
                    .baseUrl(BEACH_CONGESTION_URL)
                    .addConverterFactory(GsonConverterFactory.create())
                    .build()
                instance =  retrofit.create(BeachCongestionService::class.java)
            }
            return instance
        }
    }
}

์œ„์™€ ๊ฐ™์ด annotaion์„ ํ†ตํ•ด ํ†ต์‹ ํ•  ๋ฐฉ์‹์„ ์ •ํ•˜์—ฌ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. 

(@GET ์™ธ์— @POST , @DELETE , @PUT ๋“ฑ์ด ์žˆ์Šต๋‹ˆ๋‹ค.)

 

4. Retrofit2๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ†ต์‹ ํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

private fun startRetrofitButton() {
        binding.progressBar.visibility = View.VISIBLE
            val response = RetrofitService.getInstance().getBeachCongestion()
            response.enqueue(object : Callback<BeachCongestionList>{
                override fun onResponse( // ์„ฑ๊ณต์‹œ์— ์‹คํ–‰๋˜๋Š” ์ฝ”๋“œ
                    call: Call<BeachCongestionList>,
                    response: Response<BeachCongestionList>
                ) {
                    val model = response.body()?.beach0
                    CoroutineScope(Dispatchers.Main).launch {
                        with(binding){
                            beachName.text = model?.poiNm
                            congestionText.text = convertCongestion(model?.congestion as String)
                            timeText.text = model.etlDt
                            uniqPopText.text = model.uniqPop.toString()
                            seqIdText.text = model.uniqPop.toString()
                        }
                        Toast.makeText(this@MainActivity , "์ •์ƒ์ ์œผ๋กœ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์™”์Šต๋‹ˆ๋‹ค." , Toast.LENGTH_SHORT).show()
                        binding.progressBar.visibility = View.GONE
                    }
                }

                override fun onFailure(call: Call<BeachCongestionList>, t: Throwable) {
                // ์‹คํŒจ์‹œ์— ์‹คํ–‰๋˜๋Š” ์ฝ”๋“œ
                    Toast.makeText(this@MainActivity , "์„œ๋ฒ„์—์„œ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค" , Toast.LENGTH_SHORT).show()
                }
            })
    }

์ด๋ฒˆ ํฌ์ŠคํŒ…์—์„œ๋Š” Response์‹œ์— Call๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ๊ตฌํ˜„ํ•˜์˜€๊ธฐ์— ์œ„์™€ ๊ฐ™์ด

enqueue()๋ฅผ ํ†ตํ•ด '๋น„๋™๊ธฐ'๋ฐฉ์‹์œผ๋กœ ์ฒ˜๋ฆฌํ•˜์˜€์Šต๋‹ˆ๋‹ค. ์ด ์™ธ์—๋„

execute()๋ฅผ ํ†ตํ•ด '๋™๊ธฐ'๋ฐฉ์‹์œผ๋กœ ์ฒ˜๋ฆฌ๋„ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

excute()๋กœ ๊ฐ’์„ ์ˆ˜์‹  ์‹œ response.isSuccessful์„ ํ†ตํ•ด ์„ฑ๊ณต ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ

๊ทธ์— ๋”ฐ๋ผ ๋™์ž‘์„ ๊ตฌํ˜„ํ•ด์ฃผ๋ฉด ๋ฉ๋‹ˆ๋‹ค. ๐Ÿ˜ƒ

 

์ „์ฒด ์ฝ”๋“œ๋Š” ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <ProgressBar
        android:id="@+id/progressBar"
        android:layout_width="50dp"
        android:layout_height="50dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:indeterminateTint="#000"
        app:layout_constraintBottom_toBottomOf="parent"
        android:visibility="gone"
        />

    <TextView
        android:id="@+id/timeText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="100dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/seqIdText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toBottomOf="@id/timeText"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        android:layout_marginTop="20dp"
        />

    <TextView
        android:id="@+id/beachName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toBottomOf="@id/seqIdText"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        android:layout_marginTop="20dp"
        />

    <TextView
        android:id="@+id/congestionText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toBottomOf="@id/beachName"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        android:layout_marginTop="20dp"
        />

    <TextView
        android:id="@+id/uniqPopText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toBottomOf="@id/congestionText"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        android:layout_marginTop="20dp"
        />

    <Button
        android:id="@+id/startButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="์‹œ์ž‘"
        android:layout_marginBottom="32dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintHorizontal_bias="0.498"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent" />


</androidx.constraintlayout.widget.ConstraintLayout>

 

package com.lee.retrofitexamplepj

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.widget.Toast
import com.lee.retrofitexamplepj.databinding.ActivityMainBinding
import com.lee.retrofitexamplepj.retrofit.model.BeachCongestionList
import com.lee.retrofitexamplepj.retrofit.model.BeachCongestionModel
import com.lee.retrofitexamplepj.retrofit.model.RetrofitService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response

class MainActivity : AppCompatActivity() {
    private lateinit var binding : ActivityMainBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater).also {
            setContentView(it.root)
        }
        addListeners()
    }
    private fun addListeners() {
        with(binding){
            startButton.setOnClickListener {
                startRetrofitButton()
            }
        }
    }

    private fun startRetrofitButton() {
        binding.progressBar.visibility = View.VISIBLE
            val response = RetrofitService.getInstance().getBeachCongestion()
            response.enqueue(object : Callback<BeachCongestionList>{
                override fun onResponse(
                    call: Call<BeachCongestionList>,
                    response: Response<BeachCongestionList>
                ) {
                    val model = response.body()?.beach0
                    CoroutineScope(Dispatchers.Main).launch {
                        with(binding){
                            beachName.text = model?.poiNm
                            congestionText.text = convertCongestion(model?.congestion as String)
                            timeText.text = model.etlDt
                            uniqPopText.text = model.uniqPop.toString()
                            seqIdText.text = model.uniqPop.toString()
                        }
                        Toast.makeText(this@MainActivity , "์ •์ƒ์ ์œผ๋กœ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์™”์Šต๋‹ˆ๋‹ค." , Toast.LENGTH_SHORT).show()
                        binding.progressBar.visibility = View.GONE
                    }
                }

                override fun onFailure(call: Call<BeachCongestionList>, t: Throwable) {
                    Toast.makeText(this@MainActivity , "์„œ๋ฒ„์—์„œ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค" , Toast.LENGTH_SHORT).show()
                }
            })
    }
    
     /**
     * ์ˆ˜์‹  ๊ฐ’์— ๋”ฐ๋ผ text๋ฅผ ๋‹ฌ๋ฆฌํ•ด์ฃผ๋Š” ํ•จ์ˆ˜
     **/
    fun convertCongestion(congestion : String) : String {
        return when(congestion){
            "1" -> "๋ณดํ†ต"
            "2" -> "์กฐ๊ธˆ ํ˜ผ์žก"
            "3" -> "ํ˜ผ์žก"
            else -> "Error"
        }
    }
}

 

๋งˆ์ง€๋ง‰์€ ์‹คํ–‰ํ™”๋ฉด์œผ๋กœ ํฌ์ŠคํŒ…์„ ๋งˆ๋ฌด๋ฆฌํ•˜๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

 

์˜ค๋Š˜๋„ ์ฆ์ฝ”ํ•˜์„ธ์š” :)