ABOUT ME

-

  • Retrofit을 사용한 이유를 '깊게' 얘기해보기
    안드로이드 2023. 4. 11. 15:10

    프로젝트를 하면서 HTTP 통신을 할때 고민없이 Retrofit2, Okhttp3 의존성을 추가해 사용했습니다. 그 이유를 명확히 하고 사용하지 못했다는 생각이 들어 Retrofit이 왜 가장 대중적인 통신 라이브러리가 되었는지의 과정을 이해하면서 Retrofit을 사용한 이유를 '깊게' 얘기해보도록 하겠습니다.

     

     


     

    네트워크 프로그램을 작성하는 이유

    네트워크 프로그램을 작성하는 이유는 클라이언트 프로그램과 서버 프로그램간에 데이터를 송수신하기 위해서입니다. 

    안드로이드 앱은 HTTP 네트워크의 클라이언트 프로그램으로, 통신하기 위해 서버 프로그램이 필요합니다.

     

    안드로이드 앱에서 HTTP 통신 프로그램을 구축하는 방법

    1. 플랫폼 API를 이용하는 방법

    2. 다양한 라이브러리를 활용하는 방법(Retrofit, OkHttp, Volley 등)

    두가지가 있습니다.

     

    방법에 대해 얘기하기 전에 안드로이드에서 네트워크 통신을 '왜' 위와 같이 구축하는지 알아야합니다.

     

     

     

    네트워크 통신을 하기 위해선 비동기 작업이 필요하다.

    안드로이드는 싱글 스레드 모델로 동작합니다. 싱글 스레드란 안드로이드 화면을 구성하는 뷰나 뷰그룹을 하나의 스레드에서만 담당하는 원칙을 말합니다. 

     

    싱글 스레드 모델에는 2가지 규칙이 있습니다.

    1. 메인 스레드(UI 스레드)를 블럭하지 말 것
    2. 안드로이드 UI 툴킷은 오직 메인 스레드(UI 스레드)에서만 접근할 수 있도록 할 것

    화면의 UI 그리기 담당을 하나의 스레드가 해야하는 이유는, 안드로이드 앱처럼 UI가 있는 프로세스는 UI를 제대로 표시하기 위해 각 요소를 그리는 순서가 절대적으로 중요하기 때문입니다. 만약 여러 스레드가 각각의 UI를 그리게 된다면 앱이 충돌하거나 예기치 않은 동작을 할 수 있습니다. 

     

    즉, 싱글 스레드로 동작하는 이유는 UI 작업에 있어 경합 상태(레이스 컨디션), 교착 상태를 방지하고자 함입니다.

     

    레이스 컨디션과 교착 상태란?

    레이스 컨디션(Race Condition)

    레이스 컨디션이란 두 개 이상의 프로세스 혹은 스레드가 공유 자원을 서로 사용하려고 경합(Race)하는 현상을 말합니다. 동시에 공유 자원에 접근할 수 있으면 자원의 일관성을 해치는 결과가 발생할 수 있습니다. 그래서 동시에 공유 자원에 접근할 수 없도록 상호 배제 조건을 만들어 놓습니다. 하지만 상호 배제를 한다고 하여도 교착상태가 발생할 수 있습니다.

     

    교착 상태(Deadlock)

    교착 상태란 공유 자원에 대한 요구가 엉켜서 자원 관리를 잘못하여 프로세스나 스레드가 자원의 락을 획득하기 위해 무한 대기 하는 것을 말합니다. 서로 자원을 사용하기 위해 대기하고 있으면 안드로이드에선 ANR 문제가 발생합니다.

     

    ANR

    Android 앱의 UI 스레드가 너무 오랫동안 차단되면 나타나는 애플리케이션 응답없음 오류입니다.

    • UI 스레드가 일정 시간 어떤 Task에 잡혀있으면 발생합니다.
    • input 이벤트에 5초안에 반응하지 않을 때 발생합니다.
    • Broadcast Receiver가 10초내로 실행을 하지 않을 때 발생합니다.
    • UI 가 없는 브로드캐스트 리시버와 서비스도 실행 주체가 메인스레드 임으로 긴 시간을 소모하는 작업인 경우 ANR발생합니다.

     

    ANR 예방하려면?

    1. 시간 소모가 많은 작업은 스레드를 통해 처리해야합니다.
    2. 사용자에게 progress bar 등을 이용해 작업의 진행 과정을 알려 기다리도록 합니다.

     

     

    결과적으로 네트워크 통신에는 비동기작업이 필요합니다.

    시간이 걸리는 작업을 하는 코드는 여분의 스레드를 사용하여 메인 스레드에서 분리해야 하고, 자연스럽게 메인 스레드와 다른 스레드가 통신하는 방법이 필요하게 됩니다. 즉 UI스레드가 원활하게 돌아가기 위해서는 비동기작업이 필수적입니다

     

     

     

     

    출처: https://server-talk.tistory.com/291

     

     

     

    Http 통신 라이브러리의 역사

    HttpClient

    • Http 통신을 용이하게 수행하기 위해 Apache에서 제작한 라이브러리입니다.
    • 안드로이드 초기에 주로 사용되었으며 실제로는 HttpClient를 래핑한 DefaultHttpClient나, 안드로이드에 맞게 개수한 AndroidHttpClient가 사용되었습니다.
    • 변경점을 안드로이드 SDK에 일괄적으로 즉시 반영할 수 없어 Android 5.1에서 Deprecated 되며 6.0에서는 완전히 삭제되었습니다.

    HttpUrlConnection

    Volley

    • HttpUrlConnection을 사용할 때는 Application Not Responding(ANR)을 피하기 위해 백그라운드 스레드도 만들어야하고, 버퍼를 통한 입출력도 준비해야 하고, 캐시나 예외처리도 한땀한땀 다 처리해 주어야 하는 불편함이 있었습니다.
    • 그래서 구글에서는 HTTP 연결을 만들때마다 비동기 처리를 감싸주고 있기 때문에 AsyncTask 등을 사용하지 않아도 되는 라이브러리인 Volley를 2013년 Google I/O에서 발표했습니다.
    • 사용법은 다음과 같습니다. HTTP 메소드와 url 정보를 가진 Request를 만들어서 RequestQueue에 넣어줍니다. 그러면 Volley가 알아서 스레드를 만들고 HttpUrlConnection으로 통신을 수행한 뒤 response를 반환해줍니다. 코드를 보시면 HttpUrlConnection을 직접 사용할 때보다 코드가 더 읽기 쉬워진 것을 알 수 있습니다.
    • 하지만 Volley는 반환받은 JSON 객체를 데이터클래스로 바로 변환해주지 못하기 때문에, 별도의 과정을 통해 직접 변환해서 사용해야합니다.
    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)

    OkHttp

    • 그런 와중에 2013년 5월 6일엔 Square에서 OkHttp라는 HTTP 클라이언트 라이브러리를 발표합니다. 이 라이브러리는 Okio와 코틀린을 활용해 쓰여졌고 다음과 같은 특징이 있습니다. Connection pooling과 Redirection을 도입해 접속을 더 안정적이게 하면서도, 속도를 개선시킬 수 있는 여러가지 기술이 적용된 것으로 보입니다
    • OkHttp는 통신을 동기화로할지 비동기 처리로할지 선택할 수 있습니다. 그러나 스레드를 넘나들 수 없으므로 Handler를 사용합니다.
    OkHttpClient client = new OkHttpClient();
    
    String run(String url) throws IOException {
      Request request = new Request.Builder()
          .url(url)
          .build();
    
      try (Response response = client.newCall(request).execute()) {
        return response.body().string();
      }
    }

    Retrofit

    • Retrofit은 OkHttp를 개발한 Square에서 2013/05/14 에 발표한 라이브러리입니다.
    • HttpURLConnection을 사용하기 편하도록 랩핑한게 Volley라면 Retrofit은 OkHttp를 랩핑한 것입니다.
    • Retrofit은 어노테이션을 사용하여 코드를 생성하기 때문에 이를 위한 인터페이스를 만듭니다
    • 사용법은 다음과 같습니다. 우선 REST API 콜을 인터페이스 형식으로 준비합니다. 그리고 Retrofit 객체를 만들어서 인터페이스의 인스턴스를 생성합니다. 마지막으로 인터페이스를 동기 혹은 비동기적으로 구동시켜 response를 반환받게 되어 있습니다.

    Ktor

    Jetbrains에서 개발한 Ktor는 코틀린을 이용해 비동기 서버와 클라이언트를 구축할수 있게 해주는 라이브러리입니다. 1.6.3 버전까지 발표되어 있으며 현재도 활발한 업데이트가 이루어지고 있습니다
    안드로이드에서의 사용법은 다음과 같습니다. 코루틴 스코프 안에서 Http 클라이언트를 만들어서 request를 보내고 response를 확인한 뒤, 클라이언트의 리소스를 close로 반환하면 됩니다.

    CoroutineScope(Dispatchers.IO).launch {
      val client = HttpClient()
      val response: HttpResponse = client.get("https://ktor.io/")
      val stringBody: String = response.receive()
      client.close()
    }

     

     

     

    Retrofit 더 자세히 알아보기

    안드로이드 앱에서 서버와 HTTP 통신을 도와주는 유명한 라이브러리는 Volley와 Retrofit입니다.

    이 둘에 대한 비교는 계속되고 있습니다.

    https://medium.com/@sudhakarprajapati7/retrofit-vs-volley-c6cf74b3c8e4

     

    Retrofit vs Volley

    Now a days , Almost every mobile app includes some sort of network hits to perform its functionality and there are many alternatives…

    medium.com

     

    Http 통신 프로그램을 작성 할 때 성능, 쉽게 작성할 수 있는지가 중요하다고 한다면, HTTP 통신 프로그램 중에 Retrofit이 가장 많은 선택을 받을 수 있는 라이브러리가 될 수도 있을 것 같습니다.

    출처: http://instructure.github.io/blog/2013/12/09/volley-vs-retrofit/

    retrofit vs volley vs asynctask 간의 성능을 비교한 표에서 Retrofit이 가장 빠른 성능을 자랑하고, Retrofit의 코드 작성이 가장 편하다는 평을 받고 있습니다. 또한, 안드로이드 권장 앱 아키텍처를 보면 Http 통신에 Volley가 아닌 Retrofit을 추천하고 있기도 합니다.

     

     

     

    Retrofit 구조

    Retrofit을 이용해 서버 연동을 하려면 기본 Call 객체가 필요합니다. Call 객체는 실제 서버 연동을 실행하는 객체로 생각하면 됩니다. 이 Call 객체는 개발자가 직접 만들지 않고 Retrofit이 자동으로 만들어줍니다.

    Retrofit의 동작의 흐름을 보면 어떤 방식으로 서버와 연동되는지 알 수 있습니다. 여기서 개발자가 네트워크를 위해 직접 작성하는 부분은 인터페이스 부분 뿐입니다.

    출처: https://velog.io/@changhee09/

     

    Retrofit2 사용 방법

     

    build.gradle 파일에 의존성 설정

    Retrofit를 이용하려면 Build.gradle 파일에 의존성 설정이 필요합니다. Retrofit만을 설정해서 사용할 수 있지만, 서버 연동을 할 때 주고 받는 데이터가 대부분 JSON 파일과 XML 파일이어서 이를 파싱하는 것이 불편합니다. 그래서 자동으로 파싱할 수 있는 컨버터를 추가해줍니다.

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

     

     

    Model을 정의

    model은 서버 연동을 위한 데이터 추상화 클래스로. JSON이나 XML 데이터를 개발자가 직접 파싱하지 않고 컨버터가 자동으로 생성해 객체 생성 후 변수에 파싱한 데이터를 담아줍니다.

    data class ResultSearchImage(
        val meta: ImageMeta,
        val documents: List<ImageDocuments>,
    )
    
    data class ImageMeta(
        val total_count: Int,
        val pageable_count: Int,
        val is_end: Boolean,
    )
    
    data class ImageDocuments(
        val collection: String,
        val thumbnail_url: String,
        val image_url: String,
        val width: Int,
        val height: Int,
        val display_sitename: String,
        val doc_url: String,
        val datetime: String,
    )

     

     

    Service 인터페이스

    Retrofit의 핵심은 서버 네트워킹을 위한 함수를 인터페이스와 추상 함수로 만들고 그 함수에 어노테이션으로 GET/POST 등의 HTTP method와 서버 전송 질의를 지정하면, 그 정보에 맞게 서버를 연동하는 Call 객체를 자동으로 만들어줍니다.

    interface KakaoAPI {
    
        @GET("/v2/search/image?")
        suspend fun getSearchImage(
            @Header("Authorization") key: String,
            @Query("query") query: String,
            @Query("sort") sort: String,
            @Query("page") page: Int,
            @Query("size") size: Int,
        ): ResultSearchImage
    }

    이렇게 작성한 인터페이스를 Retrofit에 전달하면, Retrofit에서 인터페이스의 함수를 구현해 Call 객체를 반환하는 Service객체를 생성합니다. Retrofit에서 코루틴을 사용하면, 별도의 스케줄러는 지정하지 않아도 동작하고, 알아서 UI에서 사용할 수 있도록 만들어줍니다.

     

     

    Retrofit 객체를 생성

    object KakaoClientImpl : KakaoClient {
        private const val BASE_URL = "https://dapi.kakao.com/"
    
        override fun create(): KakaoAPI {
            val logger =
                HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }
    
            val client = OkHttpClient.Builder()
                .addInterceptor(logger)
                .build()
    
            return Retrofit.Builder()
                .baseUrl(BASE_URL)
                .client(client)
                .addConverterFactory(GsonConverterFactory.create())
                .build()
                .create(KakaoAPI::class.java)
        }
    }

    Retrofit은 Builder에 의해 만들어지며 Builder의 세터 함수로 각종 정보를 설정합니다.

    addConverterFactory() 함수로 서버와 통신할 데이터 타입에 맞는 컨버터를 지정합니다.

    .create()로 call 객체까지 획득했습니다. 이제 실제 네트워킹이 이루어지게 일을 시키도록 네트워킹 시도를 한다.

     

     

    네트워킹 시도

    override suspend fun getSearchData(query: String, page: Int, size: Int): List<ViewData> {
        val data = kotlin.runCatching {
            remoteDataSource.getResultSearchImage(KAKAO_KEY, query, page, size)
        }.getOrDefault(listOf())
        return data
    }

    서버에서 정상적으로 결과를 받으면 결과 데이터를 전달하고, 실패할 경우 빈 리스트를 반환해줍니다.

     

     

    Retrofit에 OkHttp 사용하기

    OkHttp의 경우 OkHttp Client에 네트워크 Intercepter를 통해 API가 통신되는 모든 활동을 모니터링 할 수 있으며 서버 통신 시간 조절이 가능하다는 장점이 있습니다.

    https://modelmaker.tistory.com/entry/Android-Okhttp-Interceptor%EB%A1%9C-%EC%9B%90%ED%95%98%EB%8A%94-%EC%9D%91%EB%8B%B5%EC%9C%BC%EB%A1%9C-%EB%B3%80%ED%98%95%ED%95%98%EA%B8%B0%EC%99%80-Interceptor-Test

    인터셉터의 종류

    다음과 같은 두 가지 유형의 인터셉터가 있습니다.

    • Application Interceptors : Application Code와 OkHttp Core Library 사이에 추가되는 인터셉터입니다. addInterceptor()를 사용하여 추가합니다.
    • 네트워크 인터셉터 : OkHttp Core Library와 Server 사이에 추가되는 인터셉터입니다. addNetworkInterceptor()를 사용하여 OkHttpClient에 추가할 수 있습니다

    저는 통신의 result 값을 OkHttp와 logging intercepter를 사용하여 디버깅을 통해 통신이 정상적으로 이루어졌는지 확인하였습니다.

    object KakaoClientImpl : KakaoClient {
        private const val BASE_URL = "https://dapi.kakao.com/"
    
        override fun create(): KakaoAPI {
            val logger =
                HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }
    
            val client = OkHttpClient.Builder()
                .addInterceptor(logger)
                .build()
    
            return Retrofit.Builder()
                .baseUrl(BASE_URL)
                .client(client)
                .addConverterFactory(GsonConverterFactory.create())
                .build()
                .create(KakaoAPI::class.java)
        }
    }

     

     

     

     

    참고

    https://developer.android.com/topic/performance/vitals/

    https://amitshekhar.me/blog/okhttp-interceptor

    https://pluu.github.io/blog/android/2016/12/25/android-network/

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

Designed by Tistory.