그냥 가끔의 기록장

[Kotlin] Github API를 이용해 예제 App 만들기 - 1. Retrofit 정복 본문

Android

[Kotlin] Github API를 이용해 예제 App 만들기 - 1. Retrofit 정복

the phoenix 2022. 8. 1. 23:07

[사족]

Open API를 이용해 예제 앱을 만들고, apk 파일까지 추출하는 단계를 하나씩 진행해 보려 한다. 일단 생각하는 단계는 다음과 같다.

1. Retrofit 이용해 Github API 연동, Gson으로 변환 후 Log로 출력해 보기 <-- 현재 글

2. RecyclerView + ListAdapter 구현하여 1단계의 정보들 UI로 표현하기

3. Android Jetpack Room + Diff Util 같이 적용해보기

4. Coroutine 정복하기

5. MVVM 적용하기 - Android Jetpack ViewModel, LifeCycle, LiveData 같이 알아보기

6. DI란? Android Jetpack Hilt 적용기

7. apk가 뭐지.. Android Studio에서 apk 파일 추출해보기

+ 8. 가능하면 Unit Test 도입

 

한 번 정도는 위의 단계들을 훑으면서 안드로이드 앱을 한번쯤은 만들어보고 싶었기에, 간단한 예제 앱을 만들며 개념을 훑어보려 한다.

 

Retrofit

 

1.  개요

(1) Retrofit이란?

  • Android에서 REST API 통신을 지원하기 위한 라이브러리
  • Sqaureup사에서 만든 HTTP 통신 라이브러리 중 가장 많이 사용되는 라이브러리로, Sqaureup사의 OkHttp 라이브러리의 상위 구현체
  • 기존 OkHttp에선 AsyncTask라는 것을 사용했으나, 이것이 Android에서 deprecated됨에 따라 OkHttp를 이용해 비동기적으로 서버와 통신하던 방법이 불필요해짐. Retrofit2에선 AsyncTask 없이 Backgroud에서 작업 수행 후 Callback을 통해 MainThread에서 UI Update 하도록 동작함
  • Android에서의 서버 통신 역사:
    1. 초기 Android에서는 서버와 통신하기 위해 HttpClinet를 사용함
    2. Android 5.1부터 HttpClinet가 deprecated되면서 Okhttp 사용함
    3. OkHttp의 AsyncTask가 deprecated되면서 Retrofit2 사용됨 <- 현재 여기

 

(2) 그래서 REST API 랑 HTTP가 뭔데??

  • REST API: HTTP URI를 통해 자원을 명시하고, HTTP Method를 통해 해당 자원에 대한 CRUD Operation을 적용하는 것
  • HTTP: HTML과 같은 하이퍼미디어 문서, JSON, XML 등 다양한 형태의 정보를 전송하는 어플리케이션 계층의 프로토콜. 웹 브라우저(Client)와 웹 서버 간의 통신을 위해 디자인된 프로토콜이다.

  우리가 매일 쓰는 웹을 예로 들면, 이들은 서버-클라이언트 관계로 통신을 하며 사용자들에게 자원(이미지, 문서, 데이터.. 등등)을 제공해주고 해당 자원을 쓰고, 읽고, 수정하고, 삭제하는 등의 처리를 가능하게 해준다. 최근에는 웹서핑을 할 수 있는 브라우저 (크롬, 사파리, 파이어폭스, 웨일 등) 도 다양하고, 안드로이드, IOS와 같은 모바일 디바이스처럼 다양한 클라이언트에 대해서도 서버가 통신이 가능해야 한다. 이러한 멀티 플랫폼을 지원해주기 위해 "자원"을 기준으로 정의된 아키텍처 중 하나가 REST다.

 

  다시 REST API에 대한 정의를 들여다 보면, "HTTP URI를 통해 자원을 명시하고, HTTP Method를 통해 해당 자원에 대한 CRUD Operation을 적용하는 것" 은 아래와 같이 풀어 쓸 수 있다.

 

  • URL = 프로토콜 + URL (즉, baseUrl이 되는 부분) => 예: https://api.github.com/ 
    HTTP URI = 각 자원을 구별하기 위해 부여되는 id => 예: users/leesoeun98
  • HTTP Method: GET, POST, PUT, DELETE로 각각 자원을 CREATE, READ, UPDATE, DELETE 조작 (일명 CRUD)

=> 웹 브라우저 상단의 링크바 부분을 보면, https://api.github.com/users/leesoeun98 와 같은 링크를 여러번 볼 수 있다. 해당 링크는 상단에 적힌 것처럼 URL, URI로 분류되며 URL은 고정되고 URI 부분이 서로 다른 자원들을 나타내기 위해 계속 바뀐다. 예로 leesoeun98에 대한 사용자 페이지라면 users/leesoeun98이 URI가 될 것이고, example이라는 사용자 페이지를 보여주려면 URI가 users/example로 바뀔 것이다. 사용자를 Create 한다면 GET Method가 쓰일 것이고, 사용자를 Delete한다면 DELETE Method가 쓰일 것이다.

 

  한마디로 Client가 특정 자원에 대해 CRUD 조작을 하고 싶을 때, 서버에 알맞은 Request를 하고 Response를 받아야 한다. 서버단에서 자원을 중심으로 CRUD 조작을 정의한 아키텍처가 REST API이다. Client에 해당하는 Android가 Server의 REST API를 호출해 Request를 보내고 Response를 받을 때 이 과정을 Android에서 보다 쉽게 하기 위해 Retrofit2라는 라이브러리를 사용하는 것이다!

 

  Client                                                                           Server

(Android) <-----Retrofit 라이브러리 이용-----> Server의 REST API

 

보다 자세한 REST API에 대한 설명은 다음 링크를 참고하면 된다. 

 

https://soeun-87.tistory.com/35

 

[CS] REST API 바로 알기

[사족] 내가 원래 공부하던 Android와 전혀 다른 업무를 보는..회사에 입사한 이후 이직을 준비하고자 다시 Android 공부를 하기로 결심했고, 공부를 다시 시작하다보니 대학생 때 내가 얼마나 허술

soeun-87.tistory.com

 

2.  3가지 구성요소 

  1. DTO(POJO)
    • 'Data Transfer Object', 'Plain Old Java Object' 형태의 모델로, Json 타입 변환에 사용
    • Response로 받아올 Json을 원하는 object로 변환할 수 있게 해주는 Data Class
  2. Interface
    • 사용할 HTTP Method (POST, GET, PUT, DELETE 등)을 정의해놓는 Interface
  3. Retrofit.Builder
    • Interface를 사용할 Instance로 BaseURL과 Gson Converter를 설정한다

 

3. 구현 과정

(1) API 정보

 

(2) Gradle에 Dependency 추가

  • Retrofit 라이브러리
  • Gson Converter 라이브러리: JSON 타입의 Response를 Object로 매핑해주는 Converter

Module에 해당하는 Build.gradle에 해당 dependency를 추가하면 된다.

    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'

 

(3) Manifest에 Internet Permission 추가

  • Network 통신에 필요한 인터넷 권한 추가

 

(4) DTO 만들기

  • REST API의 Response로 받아올 JSON Data를 변환해 매핑할 DTO를 선언
  • JSON 데이터의 속성명/변수명, 타입(String, Int 등)은 반드시 일치해야 한다. 단, JSON은 _를 사용해 snake convention을 사용하는데, Kotlin은 camelCase 이므로 변수명이 다를 수 있다. 이 경우 @SerializedName("속성명")으로 속성명(Json)을 일치시켜주면 변수명이 달라도 괜찮다. (SerializedName안쓰면 JSON 속성명이랑 일치해야 함에 주의!!)
package com.example.usinggithubapisampleapp

import com.google.gson.annotations.SerializedName

data class User(
    @SerializedName("bio")
    val bio: String,
    @SerializedName("url")
    val userUrl: String,
    @SerializedName("login")
    val userName: String,
    @SerializedName("avatar_url")
    val thumbnail: String
)

 

(5) Interface 정의하기

  • @GET("users"): Request Method는 GET, URI는 users
  • fun getUserList/getUser의 매개변수
    • @Header는 통신 Header 값, Github API의 경우 accept_header가 ""로 고정됨
    • @Path는 반드시 넘겨줘야 하는 Parameter로 @GET 내부 {username}에 값이 대입됨
    • @Query는 사용자 파라미터로 URL뒤에 붙여서 사용함 흔히 링크에 있는 ?뒤의 값
  • fun getUserList/getUser의 메서드명: 자유
  • fun getUserList/getUser의 반환타입
    • Call<User>에서 Call은 Response가 왔을 때 Callback으로 불려질 타입
    • User는 Response를 받아 DTO 객체화할 클래스 타입
package com.example.usinggithubapisampleapp

import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Path
import retrofit2.http.Query

interface GithubInterface {
    @GET("users")
    fun getUserList(@Header("accept") accept: String = "application/vnd.github+json",
                    @Query("per_page")per_page: Int): Call<List<User>>

    @GET("users/{username}")
    fun getUser(@Header("accept")accept: String = "application/vnd.github+json",
                @Path("username")username: String): Call<User>
}

 

(6) Retrofit.Builder Instance 생성하기

  • val retrofit: BASE_URL 등록, JSON을 변환해줄 Gson Converter 등록
  • val userAPI: retrofit을 이용해 Interface의 Instance 구현
package com.example.usinggithubapisampleapp

import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

class RetrofitService {
    companion object {
        private const val BASE_URL = "https://api.github.com/"

        val retrofit = Retrofit.Builder()
            .baseUrl(this.BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .build()

        val userAPI: GithubInterface = retrofit.create(GithubInterface::class.java)
    }
}

 

(7) MainActivity에서 호출하기

  • fun getUser: RetrofitService.userAPI Instance의 getUser 함수 구현
    • enqueue: 비동기 작업 실행, 통신 종료 후 이벤트 처리를 위해 Retrofit2의 Callback 등록
      • onResponse, onFailure override 필수, MainThread에서 작업이 처리됨
      • 단, onResponse는 200, 300, 400 코드에서 호출되므로 onResponse에서는 response.isSuccesful()로 200인지 확인해야 함
package com.example.usinggithubapisampleapp

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        println(getUser("leesoeun98"))
        getUserList(20)
    }

    private fun getUser(userName: String){
        RetrofitService.userAPI.getUser(username = userName).enqueue(
            object : Callback<User> {
                override fun onResponse(call: Call<User>, response: Response<User>) {
                    if (response.isSuccessful) {
                        if (response.code() == 200) {
                            Log.d("GET_USER", response.body().toString())
                        }
                    } else { // response.code == 400 or 300
                        Log.d("CLIENT_ERR", response.errorBody().toString())
                    }
                }

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

    private fun getUserList(perPage: Int){
        RetrofitService.userAPI.getUserList(per_page = perPage).enqueue(
            object : Callback<List<User>> {
                override fun onResponse(call: Call<List<User>>, response: Response<List<User>>) {
                    if (response.isSuccessful) {
                        if (response.code() == 200) {
                            Log.d("GET_USER_LIST", response.body().toString())
                        }
                    } else { // response.code == 400 or 300
                        Log.d("CLIENT_ERR", response.errorBody().toString())
                    }
                }

                override fun onFailure(call: Call<List<User>>, t: Throwable) {
                    Log.d("RETROFIT_ERR", t.message.toString())
                }
            }
        )
    }
}

 

(8) 결과

  • MainActivity를 실행하며 Logcat - Debug 필터를 이용해 보면 API Response를 성공적으로 받아온 것을 확인할 수 있다.

 

Reference

https://ryan94.tistory.com/31

https://jslee-tech.tistory.com/18

https://velog.io/@sasy0113/Android-Kotlin-Retrofit2

https://jaejong.tistory.com/33

Comments