[Android, Kotlin] Retrofit2 잘 사용하기 (Response, Request, Callback Method)

설렁탕S.L.T·2021년 5월 28일
4

Retrofit

목록 보기
1/1

안드로이드에서 API를 활용하여 서버와 통신하는 가장 대중적이고, 효율적인 방법은 'Retrofit'이라는 라이브러리를 사용하는 것이다.

혼자서 개발할 때는 Firebase를 통해 데이터를 통신했지만, 서버 개발자와 협력하여 Rest API(POST, GET, PUT, DELETE)를 통해 서버와 통신하려면 Retrofit에 대한 이해는 필수인 것 같다.

물론 'Socket'을 통해 통신하는 방법도 있지만, 대다수의 회사에서는 효율적이고 간단한 Retrofit을 많이 사용하는 것 같다.

1. Retrofit이란?

Retrofit은 공식 홈페이지에서 다음과 같이 정의하고 있다.
(https://square.github.io/retrofit/)

'A type-safe HTTP client for Android and Java'

이를 해석하자면, Retrofit은 안드로이드와 자바를 위한 'type-safe'한 'HTTP client'이다.

여기서 'type-safe'란 말 그대로 타입에 안정적이라는 뜻으로, 어떠한 경우든 안정적으로 타입을 판별할 수 있어 그 결과를 예측할 수 있는 범위에 있게한다라고 해석할 수 있다.

결론적으로, Retrofit이란 안드로이드와 자바에서 안정적으로 HTTP 통신을 할 수 있게 해주는 라이브러리다.

2. Retrofit 적용

Android 프로젝트에 Retrofit을 적용시키기 위해선, 우선 Gradle dependency에 Retrofit을 추가해주어야 한다.

1) build.gradle (Module: app)에 dependency 적용

build.gradle (Module: app)

dependencies {
	implementation 'com.squareup.retrofit2:retrofit:(insert latest version)'
	implementation 'com.squareup.retrofit2:converter-gson:(insert latest version)'
}

위 첫번째 요소는 쓰여진 내용에서 알 수 있듯 Retrofit 라이브러리이고,
밑에 있는 converter-gson은 Retrofit 통신을 할때 Json을 자바에서 활용 가능한 Gson 형태로 바꿔주는 라이브러리이다. 안드로이드에서 사용할 땐 Json을 Gson으로 바꾸어 사용해야하기 때문에 같이 사용하면 큰 도움이 되는 라이브러리이다.

2) Manifest에 인터넷 사용 권한 허용하기

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

당연한 얘기지만 인터넷 권한이 없다면 HTTP 통신을 할 수 없으니, Retrofit을 사용하고자 한다면 꼭 인터넷 권한 허용을 Manifest에 추가하자

3. Retrofit 활용

1) Retrofit 객체 만들어주기

//RetrofitService.kt

class RetrofitService {

    companion object {
    	//통신할 서버 url
        private const val baseUrl = "http://12.345.678.910"

	//Retrofit 객체 초기화
        val retrofit: Retrofit = Retrofit.Builder()
                .baseUrl(this.baseUrl)
                .addConverterFactory(GsonConverterFactory.create())
                .build()

        val test: TestService = retrofit.create(TestService::class.java)
    }
}

통신할 서버 url을 'baseUrl'이라는 변수에 넣어주었으며, 이 서버 url은 Retrofit 객체를 만들 때 바로 사용된다.

addConverterFactory(GsonConverterFactory.create())는 위에서 얘기한 Json을 Gson형태로 변환해주는 요소이다.

마지막에 작성된 TestService는 바로 밑에서 추가로 설명하겠다.

2) Retrofit을 통해 통신할 Interface 만들어주기

//TestService.kt

interface TestService {
    @Headers("accept: application/json", "content-type: application/json")
    @POST("/api/signup")
    fun signUp(@Body params: HashMap<String, Any>): Call<SignUpOkResponse>

    @GET("/api/signup/check")
    fun signUpCheck(
            @Query("email") email: String,
            @Query("password") password: String,
    ): Call<SignUpCheckOkResponse>

    @Multipart
    @POST("/api/file")
    fun uploadImage(
            @Part file: MultipartBody.Part
    ): Call<UploadImageOkResponse>

    @GET("/api/info/me")
    fun myInfo(@Header("access_token") accessToken: String): Call<MyInfoOkResponse>

    @GET("/api/info/other")
    fun otherUserInfo(
            @Header("access_token") accessToken: String,
            @Query("user_uid") userUid: Int
    ): Call<OtherUserInfoOkResponse>
}

위 Interface는 통신 대상이 되는 서버 url과 통신하려는 내용을 담아놓은 꾸러미라고 생각하면 된다. 해당 코드에서 알 수 있듯이 예시로는 POST(@POST Annotation), GET(@GET Annotation)를 사용하고 있다.

가장 일반적으로는 GET 방식을 많이 사용하는데, 만약 서버에서 데이터를 받아올 때 Access-Token이 필요하다면 @Header에 Access-Token을 넣고 필요한 @Query 내용을 적어 보내 원하는 데이터를 받아올 수 있다.

POST의 경우에는 서버에 데이터를 넣어주는데 사용되기 때문에, @Header로 json임을 명시해준 뒤 HashMap으로 Body를 만들어 보낸다.

@GET, @POST 옆 괄호 안에 있는 url은 서버에 의해 정해진 API url로, 서버쪽으로부터 우선적으로 확인해야한다.

만약 서버에 json이 아닌 mp4, jpg등 파일을 보내야 하는 경우 MultipartBody라는 형식으로 만들어준 뒤 서버에 보내야 정상적으로 데이터가 전송될 수 있다.

위 예시는 회원가입(signUP)과 로그인을 위한 확인(signUpCheck), 내 정보 확인(myInfo), 타 유저 정보 확인(otherUserInfo), 이미지 업로드(uploadImage) 내용이다.

각 function 끝에 있는 Call은 서버에 요청 후 되돌아오는 대답을 받아주기 위한 요소로, 바로 밑에 추가로 얘기하겠다.

3) 응답받은 내용을 담을 그릇 DTO 만들기

위에서 Call 괄호 안에 있는 SignUpCheckOkResponse와 같이 응답받은 데이터를 저장해놓는 객체를 DTO 객체라고 한다. 다른 로직을 갖지 않는 순수한 데이터 객체이기 때문에 Kotlin에서는 data class로 작성한다.

// SignUpCheckOkResponse.kt
data class SignUpCheckOkResponse(
        val uid: Int,
        val is_deleted: Int,
        val created_time: String,
        val updated_time: String,
        val signup_type: String,
        val social_id: String,
        val nickname: String,
        val about: String,
        val gender: String,
        val interests: Int,
        val age: Int,
        val address: String,
        val filename: String,
        val access_token: String
): SignUpCheckResponse

// SignUpCheckErrorResponse.kt
data class SignUpCheckErrorResponse(
        val code: Int,
        val message: String,
        val method: String,
        val url: String
) : SignUpCheckResponse

// SignUpCheckResponse.kt
interface SignUpCheckResponse {
}

위 SignUpCheckOkResponse 안에 있는 요소들(uid, is_deleted 등)은 모두 서버에 요청을 한 후 응답받아 각 변수에 값이 저장되어 활용 가능하게 된다.

정상적으로 응답받는 경우(SignUpCheckOkResponse)와 에러가 발생할 경우 받는 응답(SignUpCheckErrorResponse)에 모두 implement되는 SignUpCheckResponse는 다음 내용을 얘기하면서 함께 다루겠다.

4) MethodCallback, ErrorUtils 만들기(도움을 주신 안선재 개발자님 감사드립니다!)

위에서 작성한 TestService Interface 내 각각의 function에 대해 일일이 콜백을 작성하는 것이 기본적인 방법이지만, 간단히 MethodCallback, ErrorUtils를 만들어 효율적으로 확인이 가능하다.
(이를 통해 깔끔하고 효율적으로 Restrofit response를 관리할 수 있게 되었다.)

먼저 아래는 기존 방식이다.

RetrofitService.test.signUpCheck("abcde","12345").enqueue(object : Callback<SignUpCheckOkResponse>{
    override fun onFailure(call: Call<SignUpCheckOkResponse>?, t: Throwable?) {
        Log.e("retrofit", t.toString())
    }

    override fun onResponse(call: Call<SignUpCheckOkResponse>?, response: Response<SignUpCheckOkResponse>?) {
        Log.d("retrofit", response?.body().toString())
    }
})

위와 같이 사용시, API를 사용할 때마다 onFailure, onResponse를 호출해야하며, onFailure 시 에러를 확인하는 것에 한계가 있다 이를 해결하기 위해 MethodCallback을 만든다.

//MethodCallback.kt
class MethodCallback {

    companion object {

        inline fun <G, O : G, reified E : G> generalCallback(crossinline callback: (response: G?) -> Unit): Callback<O> {

            return object : Callback<O> {
                override fun onFailure(call: Call<O>, t: Throwable) {
                    Log.d("TestAPI", "Failed API call with call: $call exception: $t")
                }

                override fun onResponse(call: Call<O>, response: Response<O>) {

                    if (response.isSuccessful) {
                        callback(response.body())
                    } else {
                        if (response.errorBody() != null) {

                            when (response.code()) {
                                400 -> {
                                    val errorBody: E? =
                                            ErrorUtils.getErrorResponse<E>(response.errorBody()!!)

                                    if (errorBody != null) {
                                        callback(errorBody)
                                    } else {
                                        callback(null)
                                    }
                                }
                                else -> {
                                    callback(null)
                                }
                            }
                        } else {
                            callback(null)
                        }
                    }
                }
            }
        }
    }

}

위와 같이 onFailure과 onResponse를 한번에 정리할 수 있다. 위에서 G는 GeneralResponse Interface로 O인 OkResponse, E인 ErrorResponse에 Implement 된다. 예시와 함께 얘기하자면, G는 SignUpCheckResponse(Interface)이고 O는 SignUpCheckOkResponse, E는 SignUpCheckErrorResponse다. MethodCallback은 다음과 같이 사용된다.

//UserAPIMethods.kt
class UserAPIMethods {

    companion object {

        fun signUpCheck(
                email: String,
                password: String,
                callback: (response: SignUpCheckResponse?) -> Unit
        ) {

            RetrofitService.test.signUpCheck(
                    email,
                    password
            ).enqueue(
                    MethodCallback.generalCallback<SignUpCheckResponse, SignUpCheckOkResponse, SignUpCheckErrorResponse>(
                            callback
                    )
            )
        }
    }

}

한편 MethodCallback에서 Error를 처리하고 확인하기 위해 response.code()가 400일 때 ErrorUtils를 통해 errorBody를 생성한 뒤 반환하고 있다. 아래는 ErrorUtils 내용이다.

//ErrorUtils.kt
class ErrorUtils {
    companion object {
        inline fun <reified T> getErrorResponse(errorBody: ResponseBody): T? {
            return RetrofitService.retrofit.responseBodyConverter<T>(
                    T::class.java,
                    T::class.java.annotations
            ).convert(errorBody)
        }
    }
}

ErrorUtils에서는 ErrorResponse를 받아 이를 retrofit responseBodyConverter를 통해 확인이 가능한 errorBody로 만들어준다.

일련의 과정을 거치면 하나의 API에 대해

  1. generalResponse(interface)
  2. okResponse(data class)
  3. errorResponse(data class)

3가지 Response만 준비된다면 간단히 통신이 가능해진다.

ex)
MyInfoResponse(G) - MyInfoOkResponse(O) - MyInfoErrorResponse(E)
FollowResponse(G) - FollowOkResponse(O) - FollowErrorResponse(E)

5) 간편하게 사용하기

마지막으로 MainActivity에서 간단하게 사용하는 예시로 마무리 하겠다.

//MainActivity.kt
private fun signUpCheck(
            email: String,
            password: String
    ) {
        UserAPIMethods.signUpCheck(
                email,
                password
        ) { response ->

            when (response) {
                is SignUpCheckOkResponse -> {
                    Log.d("MainActivity", "SignUpCheckOk")
                }
                is SignUpCheckErrorResponse -> {

                    Log.d("tag", "" + response.code)
                    Log.d("tag", response.message)
                    Log.d("tag", response.method)
                }
                else -> {
                    Toast.makeText(
                            applicationContext,
                            "에러가 발생하여 가입 여부 확인에 실패",
                            Toast.LENGTH_SHORT
                    ).show()
                }
            }
        }
    }
profile
안드로이드 개발!

3개의 댓글

comment-user-thumbnail
2021년 8월 11일

안녕하세요 너무 참고를 잘 하였습니다!! 혹시 리스트 형태로 2개 이상의 json데이터를 가져오고 싶은데 이것을 받으려면 어느 부분을 고쳐야하는지 알 수 있을까요???

1개의 답글
comment-user-thumbnail
2022년 3월 18일

유용한 정보 잘 얻고 갑니다!!

답글 달기