질문 정리

Retrofit의 Call과 Response 공부하기

five2week 2024. 4. 7. 08:49

Retrofit 요청 및 응답 타입

구미 인사이더를 개발할 때, 작성한 게시판 조회 코드

요청 및 응답 타입을 Custom해서 사용한 경우

fun getBoardDetail(boardNo: String) {
    showProgress()
    viewModelScope.launch {
        val response = repository.getBoardDetail(boardNo)

        val type = "게시판 상세조회에"
        when (response) {
            is NetworkResponse.Success -> {
                _boardDetail.postValue(response.body)
                _commentCount.postValue(response.body.comments)
            }

            is NetworkResponse.ApiError -> {
                postValueEvent(0, type)
            }

            is NetworkResponse.NetworkError -> {
                postValueEvent(1, type)
            }

            is NetworkResponse.UnknownError -> {
                postValueEvent(2, type)
            }
        }

        hideProgress()
    }
}
@GET("/board/{boardNo}")
    suspend fun getBoardDetail(@Path("boardNo") boardNo: String): NetworkResponse<BoardDetailResponse, ErrorResponse>

다른 프로젝트의 게시판 조회 코드

Call의 enqueue를 사용한 경우

private fun getBoard() {
    val call = getBoardService.getBoard(TagRequest(getSelectedSubcategoryIds(categories)))

    call.enqueue(object : retrofit2.Callback<List<BillResponseItem>> {
        override fun onResponse(call: Call<List<BillResponseItem>>, response: Response<List<BillResponseItem>>) {
            Log.d("board", "onResponse: ${response.body()}")
            var list = response.body()
            if (response.body()?.isEmpty() == true) {
                binding.tvNoBill.isVisible = true
                binding.rvBillList.isGone = true
                binding.rvBillList.isVisible = false
            } else {
                binding.tvNoBill.isVisible = false
                binding.rvBillList.isGone = false
                binding.rvBillList.isVisible = true
            }

            if (response.body()?.size!! > 20) {
                list = response.body()?.subList(0, 20)
            }
            billAdapter.submitList(list)
        }

        override fun onFailure(call: Call<List<BillResponseItem>>, t: Throwable) {
            Log.e("onFailure", "error: ${t.message}")
            t.stackTrace
        }
    })
}
@POST("api/bill/tag")
    fun getBoard(
        @Body board: TagRequest
    ): Call<List<BillResponseItem>>

또 다른 프로젝트의 회원 비밀번호 초기화 코드

Call의 excute를 사용한 경우

override suspend fun resetPassword(email: String): Pair<RESULTRESPONSE, String?> {
    return withContext(Dispatchers.IO) {
        try {
            val response = apiService.resetPassword(email).execute()
            LogUtils.d("response : $email")
            LogUtils.d("response : ${response.toString()}")
            if (response.isSuccessful) {
                response.body()?.let { LogUtils.d("callFindPasswordAPI") }
                RESULTRESPONSE.SUCCESS to response.body().toString()
            } else {
                LogUtils.d("response : $response")
                val errorBody = response.errorBody()?.string()
                RESULTRESPONSE.FAILED to (getErrorFromBody(errorBody) ?: response.message()
                ?: "")
            }
        } catch (e: IOException) {
            RESULTRESPONSE.FAILED to e.message
        }
    }
}
@GET("user/reset-password/{email}")
fun resetPassword(
    @Path("email") email: String
): Call<SignUpResponseType>

Custom, Call, Response

Custom

sealed class NetworkResponse<out T: Any, out U: Any> {

    data class Success<T: Any>(val body: T): NetworkResponse<T, Nothing>()

    data class ApiError<U: Any>(val body: U, val code: Int): NetworkResponse<Nothing, U>()

    data class NetworkError(val error: IOException): NetworkResponse<Nothing, Nothing>()

    data class UnknownError(val error: Throwable?): NetworkResponse<Nothing, Nothing>()
}
class NetworkResponseAdapter<S: Any, E: Any>(
    private val successType: Type,
    private val errorBodyConverter: Converter<ResponseBody, E>
): CallAdapter<S, Call<NetworkResponse<S, E>>> {

Call

Call을 사용해 서버에 요청을 보낼 때, 각각의 Call은 자체적으로 HTTP 요청과 응답 쌍을 생성한다. Call은 execute()를 사용해 동기적으로, 또는 enqueue()를 사용해 비동기적으로 실행될 수 있다.

Call.execute()

execute() 메서드는 동기적으로 네트워크 요청을 처리한다. 즉, 해당 메서드를 호출하는 순간, 호출한 스레드(보통 현재 실행 중인 코드가 위치한 스레드)는 서버로부터 응답이 돌아올 때까지 차단(블록)된다.

동기 호출을 메인 스레드에서 사용하는 것은 안드로이드에서 네트워크 오퍼레이션을 수행할 때 UI를 차단하기 때문에 권장되지 않는다. 개발자는 동기 호출을 사용할 때, 항상 백그라운드 스레드나 코루틴 내에서 실행해야 하고, 이 기간동안 유저와 화면 간의 상호작용은 불가능하다.

응답을 받은 직후 그 결과를 바로 사용할 수 있다는 장점이 있다.

Call.enqueue()

enqueue()는 비동기적으로 요청을 보내고 응답을 반환한다. 따라서 execute()와 다르게 UI를 차단하지 않고, 서버와의 통신 중에도 유저와 화면 간의 상호작용이 가능하기 때문에 권장되는 방법이다.

Response

 @POST("/login")
 suspend fun login(@Body user: User): Response<Void>
suspend fun login(user: User) {
	try {
    	val response = RetrofitClient.userService.login(user)
        if (response.isSuccessful){
       		...
        }else {
        	...
        }
    }catch (e: Exception){
    	Log.e(TAG, e.toString())
    }
}

Response 객체는 특정 요청에 대한 서버의 응답을 나타냅니다. 코루틴이나 RxJava 같은 비동기 프로그래밍 패러다임을 사용할 때, Call 객체를 직접 다루기보다는 응답을 Response 객체로 처리하는 것이 더 일반적이다.

코루틴을 사용하는 경우, suspend 함수와 함께 서버 요청을 수행하면 Retrofit이 자동으로 비동기 처리를 해주며, 이때 서버로부터 받은 응답은 Response 객체로 처리된다. 이 방식은 코드를 간결하게 유지할 수 있으며, 동기적인 코드 흐름으로 비동기 작업을 처리할 수 있어 가독성이 높아진다.

Response는 성공일 수도, 실패일 수도 있기 때문에 각 케이스에 대한 핸들링을 해줘야 하는데, Response를 사용할 경우 Call.enqueue()를 사용할 때처럼 Callback methods(onResponse, onFailure)가 아닌 위 예제처럼 try ~ catch를 사용하거나 다른 방식을 사용해서 각 케이스에 대한 핸들링을 해줘야 한다.

Call vs Response: 어느 것을 사용해야 할까?

선택 기준

  • 비동기 처리 모델을 사용하는 경우, 즉 RxJava나 코루틴을 사용해서 서버와의 통신을 구현하는 경우, Retrofit의 자동 비동기 처리 기능을 활용하여 Response 객체로 응답을 처리하는 것이 더 효율적이다.
  • 반면, Retrofit의 기본적인 비동기 콜백 패턴에 익숙하거나, 특정 상황에서 동기적인 요청 처리가 필요한 경우 Call 객체를 사용할 수 있다.

결론적으로, 프로젝트의 요구 사항과 사용하는 비동기 처리 모델에 따라 **Call**과 Response 중 적합한 것을 선택하면 된다. 간결하고 선언적인 코드를 선호하며 비동기 프로그래밍 모델을 사용한다면 **Response**를, Retrofit의 기본 비동기 콜백 메커니즘에 익숙하다면 **Call**을 사용하는 것이 좋다.

생각해보면 나는 요청과 응답 결과에 대한 반환을 받는 것이 익숙하기 때문에 항상 Call을 위주로 사용해왔다. 다음에 기회가 된다면 간결한 코드를 위해서 Response를 사용해봐야겠다.