728x90


앱에서 가장 많이 수행하는 처리 중 하나는 서버에 데이터를 요청하고 받아온 데이터를 단말기로, 즉 클라이언트의 화면에 표시하는 일이다. 이때 클라이언트와 서버가 통신하는 방식은 크게 소켓 연결과 HTTP 연결 두 가지로 나눌 수 있다.

 

📌  소켓 연결 & HTTP 연결

소켓 연결

  • 소켓은 네트워크 상의 두 프로그램 사이에 일어나는 양방향 통신 중 한쪽의 엔드 포인트를 의미한다. (엔드 포인트란 IP와 포트의 조합)
  • 소켓 연결방식에서는 클라이언트와 서버가 특정 포트를 통해 연결을 계속 유지하고 있기 때문에 실시간으로 양방향 통신을 할 수 있다.
  • 주로 동영상 스트리밍이나, 온라인 게임등에서 사용된다.

HTTP 연결

  • 80번 포트를 사용하여 웹 상에서 정보를 주고받을 수 있는 프로토콜
  • HTTP 통신에서는 클라이언트가 서버에 헤더(header)와 바디(body)로 이루어진 메시지를 요청 즉 Request 한다.
  • 서버는 이 요청을 처리하고 응답코드와 함께 응답 즉 Response를 하게 된다.

HTTP에 대한 자세한 내용은 다음 포스팅을 참고하면 된다.

https://jminie.tistory.com/116?category=1031317 

 

HTTP와 SSL 그리고 HTTPS

📌 HTTP HTTP란 텍스트 기반의 통신 규약으로 인터넷에서 데이터를 주고받을 수 있는 프로토콜이다. 쉽게 말해 HTML, CSS, JS, 이미지, 동영상 들과 같은 컨텐츠들을 서버와 클라이언트가 주고받기

jminie.tistory.com

 

📌  Android HTTP 통신 라이브러리

Volley

구글에서 2013년 발표한 기존에서 사용하던 HttpUrlConnection의 불편함을 해소할 수 있는 라이브러리

 

 

Volley의 특징

  • Network Request 들을 자동으로 스케줄링
  • 다중 동시 네트워크 연결 지원
  • Requset 우선순위 부여 기능
  • 커스터마이징 하기 편하다. (예를 들어 retry 나 backoff)
  • 디버깅과 트래킹 툴을 지원한다.

 

Volley 사용법

HTTP 메서드와 url 정보를 가진 Request를 만들어서 RequestQueue에 넣어준다. 그러면 Volley가 알아서 스레드를 만들고 HttpUrlConnection으로 통신을 수행한 뒤 response를 반환한다.

val textView = findViewById<TextView>(R.id.text)

val queue = Volley.newRequestQueue(this)
val url = "https://www.google.com"

val stringRequest = StringRequest(Request.Method.GET, url,
        Response.Listener<String> { response ->
            // Display the first 500 characters of the response string.
            textView.text = "Response is: ${response.substring(0, 500)}"
        },
        Response.ErrorListener { textView.text = "That didn't work!" })

queue.add(stringRequest)

 

 

Retrofit

OkHttp를 개발한 Square에서 2013년 발표한 라이브러리이다.

HttpURLConnection을 사용하기 편하도록 랩핑 한게 Volley라면 Retrofit은 OkHttp를 랩핑한 것이다.

 

 

Retrofit의 특징

  • OkHttp에서는 사용 시 대개 Asynctask를 통해 비동기로 실행하는데, Asynctask가 성능상 느리다는 이슈가 있었다. Retrofit에서는 Asynctask를 사용하지 않고 자체적인 비동기 실행과 스레드 관리를 통해 속도를 훨씬 빠르게 끌어올렸다.
  • OkHttp에서도 쿼리스트링, request, response 설정 등 반복적인 작업이 필요한데, Retrofit에서는 이런 과정을 모두 라이브러리에 넘겨서 처리하도록 하였다. 따라서 사용자는 함수 호출시에 파라미터만 넘기면 되기 때문에 작업량이 훨씬 줄어들고 사용하기 편리하다.
  • 인터페이스 내에 annotation을 사용하여 호출할 함수를 파라미터와 함께 정의해놓고, 네트워크 통신이 필요한 순간에 구현없이 해당 함수를 호출하기만 하면 통신이 이루어지기에 코드를 읽기가 매우 편하다. Asynctask를 쓰지 않기에 불필요하게 코드가 길어질 필요도 없으며, 콜백 함수를 통해 결과가 넘어오도록 되어있어 매우 직관적인 설계가 가능하다.

 

 

Retrofit 사용법

REST API 콜을 인터페이스 형식으로 준비한다. 그리고 Retrofit 객체를 만들어서 인터페이스의 인스턴스를 생성하고, 마지막으로 인터페이스를 동기 혹은 비동기적으로 구동시켜 response를 반환받는다.

public interface GitHubService {
  @GET("users/{user}/repos")
  Call<List<Repo>> listRepos(@Path("user") String user);
}

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.github.com/")
    .build();
GitHubService service = retrofit.create(GitHubService.class);

Call<List<Repo>> repos = service.listRepos("octocat");

 

 


 

📌  앱 개발

의존성 추가

build.gradle에 Retrofit과 JSON을 다루기 위한 Moshi 컨버터(gson도 가능하다), 그리고 로그를 찍기 위한OkHttp 라이브러리를 추가한다.

    // 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.9.1'
    implementation 'com.squareup.okhttp3:logging-interceptor:4.9.1'

 

Mainfest에 권한 추가

HTTP 통신을 하려면 기본적으로 인터넷을 이용해야 하므로 AndroidMainfest에 인터넷 관련 권한을 추가해준다.

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

 

DTO 준비

데이터 형식 확인

가져오려는 데이터는 경기도 데이터 드림의 코로나 선별 진료소 관련 데이터이다.

 

해당 사이트에 접속하여 API 주소인 https://openapi.gg.go.kr/TBGGSCREECLSTM?type=json 

로 들어가 json 파일을 가져와 json beautify를 지원하는 프로그램으로 데이터 형식을 살펴보자.

"head" 안의 항목은 필요하지 않은 메타정보이고, "row" 안의 내용이 Recyclerview로 표시해야 하는 것이다.

{
  "TBGGSCREECLSTM": [
    {
      "head": [
        {
          "list_total_count": 115
        },
        {
          "RESULT": {
            "CODE": "INFO-000",
            "MESSAGE": "정상 처리되었습니다."
          }
        },
        {
          "api_version": "1.0"
        }
      ]
    },
    {
      "row": [
        {
          "SIGUN_NM": "광명시",
          "INST_NM": "광명시보건소",
          "OPR_CONT": "09:30~17:00 (소독)12:00~13:30",
          "SAT_OPR_CONT": "09:30~13:00",
          "HOLDY_OPR_CONT": "09:30~13:00",
          "TELNO": "02-2680-2577",
          "JURISD_NM": "광명시보건소",
          "INST_TELNO": "02-2680-2577",
          "RM": null,
          "DATA_STD_DE": "2022-03-29",
          "REFINE_ROADNM_ADDR": "경기도 광명시 오리로 613",
          "REFINE_LOTNO_ADDR": "경기도 광명시 하안1동 230-1 오리로613",
          "REFINE_ZIPNO": "14303",
          "REFINE_WGS84_LAT": "37.4554107848",
          "REFINE_WGS84_LOGT": "126.8781649434"
        },
        {
          "SIGUN_NM": "광주시",
          "INST_NM": "참조은병원",
          "OPR_CONT": "09:00~17:00",
          "SAT_OPR_CONT": "09:00~12:00",
          "HOLDY_OPR_CONT": "미운영",
          "TELNO": "1600-9955",
          "JURISD_NM": "광주시보건소",
          "INST_TELNO": "031-760-8654",
          "RM": null,
          "DATA_STD_DE": "2022-03-29",
          "REFINE_ROADNM_ADDR": "경기도 광주시 광주대로 45",
          "REFINE_LOTNO_ADDR": "경기도 광주시 경안동 20-61번지",
          "REFINE_ZIPNO": "12756",
          "REFINE_WGS84_LAT": "37.4109140026",
          "REFINE_WGS84_LOGT": "127.2599526938"
        }
        ...
        ...
      ]
    }
  ]
}

 

 

DTO 클래스 작성

JSON 형식으로 받아온 텍스트는 앱에서 사용할 수 있는 데이터 객체로 변환하게 되는데 이때 그 객체의 틀이 되는 것이 DTO이다.

DTO는 그저 계층 간 데이터 교환이 이루어질 수 있도록 하는 객체이기 때문에, 특별한 로직을 가지지 않는 순수한 데이터 객체여야 한다. 또한 DB에서 꺼낸 값을 DTO에서 임의로 조작할 필요가 없기 때문에 DTO에는 Setter를 만들 필요가 없고 생성자에서 값을 할당한다.

 

따라서 코틀린에서 지원하는 data class를 이용하여 DTO 클래스를 생성한다.

 

안드로이드 스튜디오에서 지원하는  JSON To Kotlin Class 플러그인을 이용하여 5개의 data class를 만들어준다.

이때 옵션은 Val, Nullable, MoShi (Reflect)를 선택한다.

 

 

package com.example.retrofit

import com.squareup.moshi.Json

data class EmgMedResponse(
    @field:Json(name = "TBGGSCREECLSTM")
    val tBGGSCREECLSTM: List<TBGGSCREECLSTM>?
)

data class TBGGSCREECLSTM(
    @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 = "DATA_STD_DE")
    val dATASTDDE: String?,
    @field:Json(name = "HOLDY_OPR_CONT")
    val hOLDYOPRCONT: String?,
    @field:Json(name = "INST_NM")
    val iNSTNM: String?,
    @field:Json(name = "INST_TELNO")
    val iNSTTELNO: String?,
    @field:Json(name = "JURISD_NM")
    val jURISDNM: String?,
    @field:Json(name = "OPR_CONT")
    val oPRCONT: 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 = "RM")
    val rM: Any?,
    @field:Json(name = "SAT_OPR_CONT")
    val sATOPRCONT: String?,
    @field:Json(name = "SIGUN_NM")
    val sIGUNNM: String?,
    @field:Json(name = "TELNO")
    val tELNO: String?
)

처음 data class가 만들어지면 어노테이션이 @Json으로 되어있는데 이를 커멘드 + R 키를 이용하여 모두 @field:Json으로 바꾸어 준다.

 

그 이유는 우리가 Moshi를 이용하기 때문이다.

통신에서는 객체 그 자체를 통신에 사용하기가 힘들기 때문에 컴퓨터에서는 파일을 다른 컴퓨터로 보내기 전 통신이 가능하면서 나중에 재구성할 수 있는 포맷으로 변환해 주어야 하는데, 이러한 과정을 직렬화라고 한다. 

이렇게 변환된 포맷의 일종이 바로 JSON이다. 역직렬화란 직렬화된 파일을 다시 객체 형태로 변환해주는 것이다.

 

Moshi

Moshi란 JSON과 객체 사이의 직렬화와 역직렬화를 쉽고 안전하게 돕는 라이브러리이다.

 

이러한 Moshi를 사용하기 위해 사용하는 방법 중 하나가 바로 

@field:Json : JSON Key - Value이다.

@field:Json은 name이라는 파라미터를 가지는데 이 파라미터는 위 코드처럼 json 포맷의 key값에 대응시켜야 한다.

 

 

 

아이템 디자인 구성

Recyclerview에서 사용할 아이템의 디자인을 만들어준다. LinearLayout을 써서 API 응답 중 Row의 내용을 표시하는 화면을 만들어준다.

<?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="horizontal"
    android:padding="10dp">

    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="9"
        android:orientation="vertical">

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

        <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_addr"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="주소"
            android:textSize="18sp" />

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

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

    <ImageView
        android:layout_width="30dp"
        android:layout_height="30dp"
        android:layout_gravity="center"
        android:src="@drawable/ic_baseline_reorder_24" />

</LinearLayout>

 

레이아웃 작성

JSON 응답 결과를 표시할 Recyclerview를 하나 배치하고 네트워크 연결을 시작하는 데 사용할 플로팅 버튼을 하나 만들어준다.

<?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">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:scrollbars="vertical"
        tools:listitem="@layout/item_emgmed"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/btn_get"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="20dp"
        android:clickable="true"
        android:focusable="true"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:srcCompat="@android:drawable/ic_menu_add"
        android:contentDescription="@string/floating_button" />

</androidx.constraintlayout.widget.ConstraintLayout>

 

 

Recyclerview 어댑터 작성

DTO 중 Row 클래스의 내용을 받아 ViewHolder에 표시하는 Recyclerview 어댑터를 작성한다. 데이터의 갱신은 ListAdapter를 사용하여 수행한다.

Recyclerview와 ListAdapter와 관련된 내용은 아래 포스팅을 참고하길 바란다.

https://jminie.tistory.com/144

 

안드로이드 [Kotlin] - RecyclerView로 랜덤한 색상으로 사각형 채워보기

📌 RecyclerView 란? https://developer.android.com/guide/topics/ui/layout/recyclerview RecyclerView로 동적 목록 만들기  | Android 개발자  | Android Developers RecyclerView로 동적 목록 만들기   A..

jminie.tistory.com

https://jminie.tistory.com/146

 

안드로이드 [Kotlin] - RecyclerView에서 ListAdapter와 DiffUtil 사용기

ListAdapter와 DiffUtil에 대해 알아보기 전에 우선 RecyclerView와 LiveData에 대해 알아봐야 한다. 따라서 전에 작성한 포스팅을 첨부한다. https://jminie.tistory.com/144?category=1040997 안드로이드 [Kotl..

jminie.tistory.com

 

package com.example.retrofit

import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.example.retrofit.databinding.ItemEmgmedBinding

class EmdMedAdapter : ListAdapter<Row, EmdMedAdapter.EmgMedViewHolder>(EmgMedCallback) {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EmgMedViewHolder {
        val binding = ItemEmgmedBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return EmgMedViewHolder(binding)
    }

    override fun onBindViewHolder(holder: EmgMedViewHolder, position: Int) {
        holder.bind(getItem(position))
    }

    class EmgMedViewHolder(private val binding: ItemEmgmedBinding) : RecyclerView.ViewHolder(binding.root) {
        @SuppressLint("SetTextI18n")
        fun bind(item: Row) {
            with(binding) {
                tvName.text = item.iNSTNM // 선별진료소 이름
                tvPhone.text = item.tELNO // 선별진료소 전화번호
                tvAddr.text = item.rEFINEROADNMADDR // 선별진료소 주소
                tvWgs84lat.text = "위도: ${item.rEFINEWGS84LAT}" // 선별진료소 위도
                tvWgs84lon.text = "경도: ${item.rEFINEWGS84LOGT}" // 선별진료소 경도
            }
        }
    }
}

object EmgMedCallback : 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
    }
}

 

 

그리고 MainActivity의 onCreate에서 Recyclerview를 설정한다.

binding 객체와 어댑터 객체는 데이터를 불러올 때 객체가 생성될 수 있게 by lazy를 통해 선언해준다.

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

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

        binding.recyclerView.apply {
            layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
            adapter = emgMedAdapter
            addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
        }
}

 

 

서비스 작성

사용할 선별 진료소 API는 다음과 같은 인자를 받을 수 있게 만들어져 있다.

 

이중 KEY와 Type 인자를 전달받아 GET 요청을 수행하는 Retrofit 서비스를 만들어보자.

  •  API 요청 주소가 https://openapi.gg.go.kr/TBGGSCREECLSTM 이므로 고정 주소인 Base url은 https://openapi.gg.go.kr이다.
  •  @GET 요청 시 인자는 Base url 뒤의 TBGGSCREECLSTM이다.
  • KEY와 Type 인자는 @Query 어노테이션을 써서 전달해준다.
import retrofit2.Call
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Query

interface EmgMedService {
    @GET("TBGGSCREECLSTM")
    fun getEmgMedData(@Query("KEY") KEY : String,
                      @Query("Type") Type : String): Call<EmgMedResponse>
}

인터페이스의 메서드인getEmgMedData Call 객체를 반환하도록 정의된다. Call은 서버에 request를 보내고 그 응답을 반환하는 1회용 객체이다.

 

위 코드를 해석하면 

  • getEmgMedData는 BASE_URL/EmgMedInfo?KEY="KEY"&Type="Type" 이라는 GET 요청을 보내고 Call <EmgMedResponse> 객체를 반환받는 동작을 하는 함수

라는 뜻이 된다.

 

 

Retrofit 객체 구현

Retrofit API 작성

RetrofitApi 객체는 비용이 높기 때문에 여러 객체가 만들어지면 자원낭비 및 통신에 혼선이 올 수 있기 때문에 object 키워드를 통해 싱글턴으로 만들어준다.

 

이때 각 변수에는 아까 전 MainActivity와 마찬가지로 by lazy를 적용함으로써 실제 사용되는 순간이 와야 만들어지게 한다.

import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory

object RetrofitApi {
    private const val BASE_URL = "https://openapi.gg.go.kr/"

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

    private val retrofit: Retrofit by lazy {
        Retrofit.Builder()
            .addConverterFactory(MoshiConverterFactory.create())
            .client(okHttpClient) // 로그캣에서 패킷 내용을 모니터링 할 수 있음
            .baseUrl(BASE_URL)
            .build()
    }

    val emgMedService: EmgMedService by lazy {
        retrofit.create(EmgMedService::class.java)
    }
}

빌더 패턴을 통해 retrofit 객체를 만들어준다. addConverterFactory에서 DTO 변환에 사용할 JSON 컨버터를 Moshi로 지정하고,BASE_URL을 전달한 뒤 build()로 객체를 생성한다. 그러고 나서 retrofit 인스턴스의 create 명령을 이용해서 emgMedService의 인스턴스를 생성해준다.

 

이때 retrofit 객체를 생성할 때client 속성에 okHttpClient를 넘겨주면 로그 캣에서 패킷 내용을 모니터링할 수 있다.

 

Retrofit 구동

MainActivity의 플로팅 버튼에 대해 다음 내용의 retrofitWork()를 실행하는 클릭 리스너를 구성한다.

private fun retrofitWork() {
    val service = RetrofitApi.emgMedService

    service.getEmgMedData(getString(R.string.api_key), "json")
        .enqueue(object : Callback<EmgMedResponse> { // request 는 백그라운드 스레드에서 비동기적으로 보내고 response 는 메인 스레드에서 콜백으로 받는다.
            override fun onResponse( // 성공했을때
                call: Call<EmgMedResponse>,
                response: Response<EmgMedResponse>
            ) {
                if (response.isSuccessful) {
                    val result = response.body()?.tBGGSCREECLSTM?.get(1)?.row
                    emgMedAdapter.submitList(result!!)
                }
            }

            override fun onFailure(call: Call<EmgMedResponse>, t: Throwable) { // 실패했을때
                Log.d("TAG", t.message.toString())
            }
        })
}

위에서 싱글턴으로 만들었던 Retrofit으로 서비스 인스턴스를 생성하고 getEmgMedData를 실행해서 Call 객체를 획득 합한다. 인증키는 strings.xml에 저장한 뒤 R.string.api_key를 이용해 입력하고 서버로부터 json 데이터를 반환받도록 한다. 인증키는 경기도 데이터 드림 사이트에서 로그인하여 신청하면 쉽게 얻을 수 있다.

 

Call 작업은 두 가지 방법으로 실행시킬 수 있다.

  • execute를 사용하면 request를 보내고 response를 받는 행위를 동기적으로 수행한다.
  • enqueue 작업을 실행하면 request는 비동기적으로 보내고, response는 콜백으로 받게 된다.

enqueue의 통신 결과는 성공했을 때 onResponse, 실패했을 때 onFailure를 리스너로 받을 수 있으므로 각 상황에 따른 처리를 해 준다. 통신에 성공했을 경우 던져주는 response.isSuccessful을 확인하여 사용할 수 있는 응답이 돌아왔는지 확인하고, body에서 값을 꺼내어 Recyclerview에 넘겨준다.

 

https://square.github.io/retrofit/2.x/retrofit/retrofit2/Call.html#execute--

 

Call (Retrofit 2.7.1 API)

An invocation of a Retrofit method that sends a request to a webserver and returns a response. Each call yields its own HTTP request and response pair. Use clone() to make multiple calls with the same parameters to the same webserver; this may be used to i

square.github.io

 

 

 

📌  코루틴(coroutine)에서 사용하기

enqueue는 request를 백그라운드 스레드에서, response 콜백은 메인 스레드에서 처리하게 된다. 하지만 코루틴을 사용하면 콜백을 쓰지 않아도 동일한 처리를 할 수 있다.

 

코루틴 의존성 추가

// 코루틴
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0'

 

서비스 인터페이스에코루틴 메서드 추가

코루틴으로 작동하는 서비스를 인터페이스에 추가해 준다. 서비스가 코루틴 안에서 수행되어야 하므로 suspend 키워드를 붙여주고, 반환 값은Call 객체가 아니라 Response 객체로 받도록 변경한다. 코루틴 자체적으로 비동기적인 처리를 수행하기 때문에, Call이 제공하는 흐름 처리 기능은 더 이상 필요가 없다.

import retrofit2.Call
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Query

interface EmgMedService {
    @GET("TBGGSCREECLSTM")
    fun getEmgMedData(@Query("KEY") KEY : String,
                      @Query("Type") Type : String): Call<EmgMedResponse>

    // 반환값이 Call 이 아닌 Response 인 이유는 코루틴이 자체적으로 비동기 처리를 하기 때문에
    // Call 이 제공하는 흐름처리 기능은 이제 필요가 없다.
    @GET("TBGGSCREECLSTM")
    suspend fun getDataFromCoroutine(@Query("KEY") KEY: String,
                                     @Query("Type") Type: String): Response<EmgMedResponse>
}

 

retrofitWork 메서드 수정

코루틴에서 네트워크 작업은 IO 디스패처 작업을 수행하므로  IO 디스패처에서 Response 반환을 획득한다. 그리고 반환 값을 RecylerView에 표시할 때만 withContext을 통해 메인 스레드로 스위칭시켜준다.

    private fun retrofitWork() {
        val service = RetrofitApi.emgMedService

        CoroutineScope(Dispatchers.IO).launch {
            // response 를 받아오는 작업이 네트워크 작업이기 때문에 Dispatcher.IO 에서 작업을 실시한다.
            val response = service.getDataFromCoroutine(getString(R.string.api_key), "json")

            // 반환값을 가져와서 리사이클러뷰에 표시하는 부분은 UI 작업이기 때문에 Dispatcher.IO 에서 작업한다.
            withContext(Dispatchers.Main) {
                if (response.isSuccessful) {
                    val result = response.body()?.tBGGSCREECLSTM?.get(1)?.row
                    result?.let {
                        emgMedAdapter.submitList(it)
                    }
                } else {
                    Log.d("TAG", response.code().toString())
                }
            }
        }
    }

 

 

📱  결과물

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Reference:

https://www.youtube.com/watch?v=Dnmifx4BSQc 

https://cliearl.github.io/posts/android/download-json-data-using-retrofit/

https://cliearl.github.io/posts/android/android-http-library-review/

https://kotlinworld.com/117#:~:text=Moshi%EB%9E%80%20JSON%EA%B3%BC%20%EA%B0%9D%EC%B2%B4,%EC%88%98%20%EC%9E%88%EB%8F%84%EB%A1%9D%20%EB%8F%95%EB%8A%94%20%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC%EC%9D%B4%EB%8B%A4.

https://ko.wikipedia.org/wiki/HTTP_%EC%83%81%ED%83%9C_%EC%BD%94%EB%93%9C

https://data.gg.go.kr/portal/data/service/selectServicePage.do?page=1&rows=10&sortColumn=&sortDirection=&infId=H1DKLV185I8ZJ9ZFDVT031801022&infSeq=3&order=&loc=&searchWord=%EC%BD%94%EB%A1%9C%EB%82%98 

 

복사했습니다!