Android/Study

[HTTP 통신] Volley와 Retrofit2 라이브러리

화요밍 2022. 5. 17. 17:58
728x90
반응형

Volley와 Retrofit2는 안드로이드에서 네트워킹을 돕는 라이브러리입니다.

기본적으로 HTTP 통신을 위해 매니페스트에 인터넷 권한을 추가해줘야 합니다.

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

1. Volley 라이브러리

Volley는 2013년 구글 IO 행사에서 공개된 라이브러리로, 안드로이드 앱의 네트워킹을 더 쉽고 빠르게 해주는 라이브러리입니다.

UI를 채우기 위해 필요한 RPC(Remote Procedure call) 유형의 작업을 할 때 유용합니다.

String, 이미지, JSON 타입을 사용할 수 있습니다.

기본적으로 HTTP Client는 HttpURLConnection입니다.

 

* RPC(Remote Procedure call)

별도의 원격 제어를 위한 코딩 없이 다른 주소 공간서 함수나 프로시저를 실행할 수 있게하는 프로세스 간 통신 기술입니다.

 

Volley는 파싱하는 동안 모든 Response를 메모리에 유지하기 때문에 대규모 다운로드나 스트리밍 작업에는 적합하지 않습니다.(대규모 다운로드 작업은 DownloadManager를 사용해야 합니다.)

 

  • 장점
  1. 네트워크 요청 자동 예약
  2. 여러 개의 동시 네트워크 연결
  3. 표준 HTTP 캐시 일관성을 갖춘 투명한 디스크 및 메모리 응답 캐싱
  4. 요청 우선순위 지정 가능
  5. 특정 요청 취소 및 취소할 요청 범위 설정 가능
  6. 강력한 정렬 기능을 이용해 비동기식으로 가져온 데이터로 UI를 올바르게 채울 수 있음
  7. 디버깅 및 추적 도구 존재

 

  • 라이브러리 등록
dependencies {
    ...
    implementation 'com.android.volley:volley:1.2.1'
}

 

  • Volley의 구성요소
  1. RequestQueue: 서버에 요청을 보내 네트워크 작업을 실행하고, 캐시를 읽고 쓰거나 Response를 파싱하기 위해 작업 스레드를 관리합니다.
  2. Request: 타입에 맞춰 결과를 받는 Request 정보를 담는 객체입니다. Raw Response를 파싱하며 Volley는 파싱된 응답을 메인 쓰레드에 전달합니다. StringRequest, ImageRequest, JsonObjectRequest, JsonArrayRequest 등이 있습니다.

 

  • Volley 구현하기
  • Request 객체 생성하기
val url = "http://www.google.com"
val stringRequest = StringRequest(
	Request.Method.GET,
    url,
    Response.Listener<String> { response ->
    	textView.text = response
    },
    Response.ErrorListener {
    	Log.d("Request", "Error!!)
    })

StringRequest의 생성자에는 HTTP 메서드, 서버 URL, 서버로부터 응답을 받으면 호출할 콜백, 서버 연동에 실패하면 호출할 콜백을 지정합니다.

세 번째 매개변수를 통해 서버로부터 Response를 받은 순간 Response.Listener 객체의 onResponse() 함수가 호출됩니다.

onResponse() 함수 내에서 UI 컨트롤을 하는 로직을 구현하면 됩니다.

 

  • RequestQueue 객체 생성 및 요청
val requestQueue = Volley.newRequestQueue(this)
requestQueue.add(stringRequest)

Volley.newRequestQueue()를 통해 RequestQueue 객체를 얻고, add() 함수에 Request 객체를 전달해서 서버에 요청을 보냅니다.

네트워킹을 한 번만 요청하면 되고 쓰레드 풀을 남겨두고 싶지 않다면 필요할 때마다 RequestQueue를 만들고 Volley.newRequestQueue() 메서드를 사용하며 RequestQueue에서 stop()을 호출하는 방식으로 사용하면 됩니다.

 

하지만 일반적인 상황에서는 RequestQueue를 싱글톤으로 만들어 앱 전체 기간 동안 계속 실행해야 합니다.

Application Context로 RequestQueue를 인스턴스화하면 앱의 전체 기간동안 유지됩니다.

class MySingleton constructor(context: Context) {
	companion object {
    	@Volatile
        private var INSTANCE: MySingleton? = null
        fun getInstance(context: Context) = 
        	INSTANCE ?: synchronized(this) {
            	INSTANCE ?: MySingleton(context).also {
                	INSTANCE = it
                }
            }
    }
    
    val requestQueue: RequestQueue by lazy {
    	Volley.newRequestQueue(context.applicationContext)
    }
    
    fun <T> addToRequestQueue(req: Request<T>) {
		requestQueue.add(req)
    }
}

* 싱글톤 패턴 참고: https://cliearl.github.io/posts/android/understanding-singleton-pattern/

 

알기쉬운 Singleton Pattern

이번 포스팅에서는 싱글톤 패턴에 대해 알아보도록 하겠습니다. Singleton 이란 싱글톤(Singleton)은 소프트웨어 디자인패턴의 한 종류로, 프로그램 안에서 클래스의 인스턴스가 단 하나만 존재해야

cliearl.github.io

 

  • Volley 내부 동작

요청 처리 과정

Request 객체를 실어 add()를 호출하여 RequestQueue에 요청을 보내면, 요청이 파이프라인을 통해 이동하고 처리됩니다.

Volley는 하나의 캐시 처리 쓰레드와 네트워크 전달 쓰레드 풀을 실행합니다.

RequestQueue에 추가된 요청은 캐시 쓰레드에 의해 분류됩니다.

캐시 Hit인 경우, 캐싱된 Response가 캐시 쓰레드에서 파싱되어 기본 쓰레드에 전달됩니다.

캐시 Miss인 경우, 캐시에서 요청을 처리할 수 없으므로 해당 요청은 네트워크 대기열에 추가됩니다.

네트워크 쓰레드가 대기열에서 요청을 가져와 HTTP 트랜잭션을 실행하고 작업 쓰레드에서 Response를 파싱하여 캐시에 저장한 후에 기본 쓰레드에 전달합니다.

 

 

  • 요청 취소하기

Request 객체의 cancel()을 호출해서 요청을 취소할 수 있습니다.

cancel()을 통해 액티비티의 onStop() 생명주기에서 대기 중인 요청을 모두 취소할 수 있습니다.

진행 중인 요청을 적절한 시기에 취소하려면, 각 요청에 태그 객체를 연결해서 요청을 추적할 수 있습니다.

Activity로 모든 요청을 태그하고 onStop()에서 requestQueue.cancelAll(this)를 호출하면 됩니다.

또한, ViewPager의 한 탭에서 보낸 요청을 처리하던 중에 다른 탭으로 전환될 때 이전 탭의 요청을 취소함으로써 새 탭이 다른 탭의 요청으로 인해 지연되지 않도록 할 수 있습니다.

 

  • TAG 설정하기
val TAG = "MyTag"
val stringRequest: StringRequest
val requestQueue: RequestQueue?

stringRequest.tag = TAG
requestQueue?.add(stringRequest)

 

  • 태그가 포함된 모든 요청 취소하기
protected fun onStop() {
	super.onStop()
    requestQueue?.cancelAll(TAG)
}

 

 

  • Request 객체
  1. StringRequest: URL을 지정하고 String 타입의 Response를 수신합니다.
  2. JsonObjectRequest: URL을 지정하고 JSON 객체를 응답으로 수신합니다. JsonRequest의 서브클래스입니다.
  3. JsonArrayRequest: URL을 지정하고 JSON 배열을 응답으로 수신합니다. JsonRequest의 서브클래스입니다.

 


2. Retrofit2 라이브러리

Square에서 만든 RESTful한 HTTP 통신을 간편하게 만들어 주는 라이브러리입니다.

Retrofit은 네트워크 통신 정보만 제공하면 네트워크 프로그래밍을 대신 구현해 줍니다.

 

  • 장점
  1. REST 통신뿐만 아니라 파일 업로드/다운로드 등의 통신을 위한 코드를 짧고 간결하게 작성할 수 있습니다.
  2. Annotation을 사용하므로 코드 가독성이 좋습니다.
  3. JsonConverterFactory를 지정해 Json 응답 객체를 개발자가 정의한 모델 클래스에 직렬화하므로 파싱이 효율적입니다.
  4. OkHttp 기반이라 Volley보다 빠른 성능을 가지고 있습니다.

 

  • Annotation을 사용한 HTTP 요청
  1. URL 파라미터 대체 및 Query 파라미터 지원
  2. 요청의 Body에 담을 객체를 JSON으로 직렬화
  3. Multipart request의 Body 및 파일 업로드

 

  • 라이브러리 등록
dependencies {
    ...
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
}

 

 

  • Retrofit2 구조

Retrofit 구조

  1. 인터페이스: 인터페이스에 정의한 통신용 함수를 호출해서 통신이 이뤄집니다.
  2. Retrofit: 통신용 서비스 객체를 생성하기 위한 Retrofit 객체를 생성합니다. Base URL과 ConverterFactory를 지정합니다.
  3. 서비스 객체: 서비스 객체를 통해 인터페이스에 선언한 함수를 호출하면 Call 객체가 반환되며, 이 Call 객체의 enqueue()함수를 호출해 통신을 수행합니다.

 

 

  • Retrofit2 구성요소
  1. 모델 클래스: 서버와 주고받는 데이터를 표현하는 클래스입니다.
  2. 인터페이스: 네트워크 통신을 위해 호출할 함수를 선언합니다.
  3. Retrofit 서비스 객체: Retrofit을 빌드하여 서비스 인터페이스를 구현한 Retrofit 서비스 객체를 생성합니다.

 

  • Retrofit2 구현하기
  • 모델 클래스 선언하기 (DTO, Data Transfer Object)

JSON, XML 등의 응답 데이터를 파싱해 모델 클래스 객체에 직렬화하기 위해 모델 클래스를 선언합니다.

응답 데이터를 담을 모델 클래스 정보만 Retrofit에 알려주면 모델 클래스의 객체를 알아서 생성해서 데이터를 담아줍니다.

 

모델 클래스의 프로퍼티에 데이터가 자동으로 저장되기 위해서는 JSON 데이터의 key와 모델 클래스의 프로퍼티 이름을 @SerializedName이라는 Annotation을 사용하여 매칭해야 합니다.

기본적으로 JSON 데이터의 key의 밑줄 다음에 오는 첫 글자를 대문자로 바꾼 프로퍼티에 자동으로 저장됩니다.

따라서 이러한 경우에는 Annotation을 생략해도 됩니다.

예를 들어 JSON 데이터의 key가 last_name이라면, lastName 프로퍼티에 자동으로 저장됩니다.

val lastName: String
//아래와 같습니다.
@SerializedName("last_name") 
val lastName: String

key와 프로퍼티 명을 다르게 정의하고 싶다면 @SerializedName Annotation을 명시해주면 됩니다.

data class Book(
    @SerializedName("itemId")
    val id: Long,
    
    @SerializedName("title")
    val title: String,
    
    @SerializedName("description")
    val description: String,
    
    @SerializedName("coverSmallUrl")
    val coverSmallUrl: String
)

@SerializedName("itemId") val id: Long은 서버에서 받아온 JSON 객체의 itemId라는 key의 데이터가 id라는 property에 저장된다는 의미입니다.

 

서버의 데이터가 복잡할 때는 모든 데이터를 하나의 모델 클래스로 표현하지 않고 여러 클래스로 분리하고 조합해서 사용할 수 있습니다.

data class BestSellerDto(
    @SerializedName("title") val title: String,
    @SerializedName("item") val books: List<Book>
)

Retrofit을 이용할 때 BestSellerDto를 알려주면 JSON 데이터를 파싱해 프로퍼티에 저장하고 item이라는 key값은 books 프로퍼티에 선언된 Book 클래스의 객체에 담아줍니다.

 

 

  • 인터페이스 작성하기

Retrofit을 이용할 때 네트워크 통신이 필요한 순간에 호출할 함수를 포함하는 서비스 인터페이스를 작성합니다.

작성한 인터페이스를 구현해서 실제로 통신하는 클래스는 Retrofit이 만들어 줍니다.

Retrofit은 인터페이스에 명시한 Annotation을 보고 그 정보대로 네트워크 통신을 할 수 있는 코드를 만듭니다.

 

인터페이스에서 사용하는 Annotation

1. @GET, @POST, @PUT, @DELETE, @HEAD

HTTP Method를 지정합니다.

메서드 명 뒤에 URL 경로를 지정하면 baseURL 뒤에 추가되어 최종 서버 요청 URL이 됩니다.

 

2. @Query

GET방식일 때 함수의 매개변수를 서버에 전달합니다.

매개변수 값은 URL에 덧붙여져 서버에 전송됩니다.

interface BookService {
    @GET("/api/search")
    fun getBooksByName(
        @Query("key") apiKey: String,
        @Query("query") keyword: String
    ): retrofit2.Call<SearchBookDto>

    @GET("/api/bestSeller")
    fun getBestSellerBooks(
        @Query("key") apiKey: String
    ): retrofit2.Call<BestSellerDto>
}

getBooksByName("abc", "Choi")를 호출하면 https://book.interpark.com/api/bestSeller?key=abc&key=Choi를 호출하는 것과 같습니다.

getBestSellerBooks("abc")를 호출하면 https://book.interpark.com/api/bestSeller?key=abc를 호출하는 것과 같습니다.

 

3. @QueryMap

GET 방식일 때 다중 쿼리들을 Map 객체에 담아서 전송하기 위해 사용합니다.

interface BookService {
    @GET("/api/search")
    fun getBooksByName(
        @QueryMap Map<String, String> queries
    ): retrofit2.Call<SearchBookDto>
}
val queries = mapOf<String, String>("apiKey" to "abc", "keyword" to "Choi")

getBooksByName(queries)를 호출하면 https://book.interpark.com/api/bestSeller?key=abc&key=Choi를 호출하는 것과 같습니다.

 

4. @Field

POST 방식일 때 @FormUrlEncoded와 함께 사용하며, 파라미터를 form-urlencoded로 보낼 때 사용합니다.

모델 객체에는 사용할 수 없으며 여러 데이터를 한 번에 보내고 싶다면 배열이나 List 객체를 이용해야 합니다.

 

 * @FormUrlEncoded

데이터를 URL 인코딩 형태로 만들어 전송합니다.

전송 데이터를 "키=값" 형태의 URL 인코딩으로 전송합니다.

파라미터가 여러 개일 때 &를 구분자로 사용합니다.

@Field Annotation이 추가된 데이터를 인코딩합니다.

interface BookService {
    @FormUrlEncoded
    @POST("/api/bestSeller")
    fun getBestSellerBooks(
        @Field("key") apiKey: String
        @Field("title") title: String
    ): retrofit2.Call<BestSellerDto>
}

getBestSellerBooks("abc")를 호출하면 https://book.interpark.com/api/bestSeller에 Body에 "key=apiKey을 인코딩한 값&title=title을 인코딩한 값"이 담겨서 전송됩니다.

 

5. @FieldMap

POST 방식일 때 여러 파라미터를 Map 객체에 담아서 전송하기 위해 사용합니다.

interface BookService {
    @FormUrlEncoded
    @POST("/api/bestSeller")
    fun getBestSellerBooks(
    	@FieldMap Map<String, String> queries
    ): retrofit2.Call<BestSellerDto>
}
val queries = mapOf("key" to "abc", "title" to "Choi")

getBestSellerBooks("abc")를 호출하면 https://book.interpark.com/api/bestSeller에 Body에 key="abc"&title="Choi"이 담겨서 전송됩니다.

 

6. @Body

@POST 방식에서 전송할 데이터를 모델 객체로 지정해야 할 때 사용합니다.

모델 객체 타입의 매개변수의 프로퍼티명이 키로, 프로퍼티의 데이터를 값으로 해서 JSON 문자열을 만들어 서버에 전송합니다.

이렇게 만들어진 JSON 문자열은 URL이 아닌 데이터 스트림으로 Request Body에 담겨 전송됩니다.

interface BookService {
    @POST("/api/bestSeller")
    fun createBookInfo(
    	@Body book: Book
    ): retrofit2.Call<Book>
}

 

7. @Header

서버 요청에서 헤더값을 조정해야 할 때 사용합니다.

interface BookService {
    @Headers("Cache-Control: max-age=640000")
    @GET("/api/search")
    fun getBooksByName(
        @QueryMap Map<String, String> queries
    ): retrofit2.Call<SearchBookDto>
}

 

8. @Url

baseUrl을 무시하고 전혀 다른 URL을 지정하고 싶을 때 사용합니다.

interface BookService {
    @GET
    fun getBooks(
        @Url url: String,
        @Query("key") key: String
    ): retrofit2.Call<SearchBookDto>
}

 

 

 

  • Retrofit 객체 생성하기

Retrofit 객체를 생성하는 코드는 초기 설정을 하므로 한 번만 생성하면 됩니다.

val retrofit = Retrofit.Builder()
    .baseUrl("https://book.interpark.com")
    .addConverterFactory(GsonConverterFactory.create())
    .build()

baseUrl() 함수로 URL을 설정하면 이후에는 이 URL 뒤에 올 경로만 지정해서 서버와 연동할 수 있습니다.

addConverterFactory() 함수로 데이터를 파싱해 모델 객체에 담는 역할자를 지정합니다.

 

 

 

  • 인터페이스 타입의 서비스 객체를 생성하고 네트워크 통신하기

생성된 Retrofit 객체에 서비스 인터페이스를 담아 네트워크 통신을 수행할 서비스 객체를 생성합니다.

서비스 객체에 인터페이스에서 작성한 함수를 호출하면 Call 객체가 반환됩니다.

이 Call 객체의 enqueue()함수를 호출해서 비동기로 네트워크 통신을 수행합니다.

enqueue() 함수의 매개변수인 Callback 객체의 onResponse(), onFailure() 함수를 통해 통신에 성공/실패했을 때 수행할 로직을 구현합니다.

bookService = retrofit.create(BookService::class.java)

bookService.getBestSellerBooks(getString(R.string.interParkAPIKey))
    .enqueue(object: Callback<BestSellerDto> {
        override fun onResponse(
            call: Call<BestSellerDto>,
            response: Response<BestSellerDto>
        ) {
            if (response.isSuccessful.not()) {
                Log.e(TAG, "NOT!! SUCCESS")
                return
            }
            adapter.submitList(response.body()?.books.orEmpty())
        }

        override fun onFailure(call: Call<BestSellerDto>, t: Throwable) {
            Log.e(TAG, t.toString())
        }

    })
  1. enqueue(): 매개변수인 Callback 객체의 onResponse(), onFailure() 함수가 자동으로 호출됩니다.
  2. onResponse(): 통신에 성공하면 서버에서 넘어온 데이터가 Response 객체로 전달됩니다. 이 Response 객체의 body() 함수를 호출해서 서버에서 받아온 데이터를 받아올 수 있습니다.
  3. onFailure(): 통신에 실패하면 호출됩니다. Throwable 매개변수를 통해 로그를 찍어 오류를 확인할 수 있습니다.

3. Volley / Retrofit2 / AsyncTask 성능 비교

 

 

Retrofit은 Volley와 AsyncTask보다 빠른 성능을 보입니다.

다수의 Discussion을 수행할 때 그 성능 차이가 점점 더 커져서 3배 이상 더 빠른 성능을 보입니다.

 

 


참고

  • 참고 도서: Do it! 깡샘의 안드로이드 앱 프로그래밍 with 코틀린 - 강성윤 지음
 

GitHub - google/volley

Contribute to google/volley development by creating an account on GitHub.

github.com

 

Volley 개요  |  Android 개발자  |  Android Developers

Volley 개요 Volley는 Android 앱의 네트워킹을 더 쉽고, 무엇보다도 더 빠르게 하는 HTTP 라이브러리입니다. Volley는 GitHub에서 사용할 수 있습니다. Volley를 사용하면 다음과 같은 이점이 있습니다. 네트

developer.android.com

 

Retrofit

A type-safe HTTP client for Android and Java

square.github.io

 

728x90
반응형

'Android > Study' 카테고리의 다른 글

[View binding]  (0) 2022.08.12
[이미지 처리] Glide 라이브러리  (0) 2022.05.18
[HTTP 통신] REST(Representational State Transfer)  (0) 2022.05.18