[Android / Kotlin] Retrofit2로 서버 통신 구현하기

선주·2022년 3월 10일
3

Android

목록 보기
9/11
post-thumbnail

🔨 build.gradle 의존성 추가

Module단위 build.gradle 파일을 열어 dependencies에 아래 라인을 추가해주자!

dependencies {
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
    implementation 'com.squareup.okhttp3:okhttp:4.8.0'
    implementation 'com.squareup.okhttp3:logging-interceptor:4.8.0'
} 

각각 버전 2.9.0과 4.8.0으로 추가했지만 Retrofit 공식문서, OKHttp 공식문서에서 확인한 후 안정된 버전으로 작성하는 것이 좋다.


📂 PolicyResponse.kt

API가 다음과 같이 정의되어 있다고 하면

{
    "status": 201,
    "success": true,
    "message": "Created",
    "data": {
        "id": 1,
        "author": "String",
        "title": "String",
        "location": "Seoul",
        "content": "String",
        "category": [
            "law",
            "English"
        ],
        "thumbnailImageUrl": null
    }
}

이에 맞춰 클래스를 만든다.

data class PolicyResponse(
    @SerializedName("status") val status: Int,
    @SerializedName("success") val success: Boolean,
    @SerializedName("message") val message: String,
    @SerializedName("data") val data: List<Data>
) {
    data class Data(
        @SerializedName("id") val id: Int,
        @SerializedName("author") val author: String,
        @SerializedName("title") val title: String,
        @SerializedName("location") val location: String,
        @SerializedName("content") val content: String,
        @SerializedName("category") val category: List<String>,
        @SerializedName("thumbNailImageUrl") val thumbNailImageUrl: String?
    )
}

코드상 변수명과 API에 정의된 이름을 달리하고 싶다면 @SerealizedName 어노테이션을 사용한다. 또, 추후 Expected BEGIN_OBJECT but was BEGIN_ARRAY at line 1 column 38 path $.data과 같은 에러가 뜬다면 서버로부터 전달받은 타입과 내가 예상한 타입이 맞지 않아서 발생하는 문제이므로 이 PolicyResponse에서 타입을 잘못 작성하지는 않았는지 살펴보는 것이 좋다.


💻 RetrofitInterface.kt

interface RetrofitInterface {

    @GET("policy")
    fun getPolicy(
        @Query("location") location: String,
        @Header("Authorization") authToken: String
    ): Call<PolicyResponse>
}

서버주소를 baseUrl이라 하고,
baseUrl/policy?location=서울로부터 데이터를 전달받아야 한다고 하자.

  • @Get("policy"): baseUrl 뒤에 /policy가 붙는다.
  • @Query("location"): 주소에 들어가는 파라미터인 location을 @Query로 지정한다.
  • @Header("Authorization"): 서버가 토큰 인증을 사용중이고 헤더에 토큰을 담아 넘겨줘야 하는 경우 @Header("Authorization")을 추가한다.
  • Call<PolicyResponse>: 서버로부터 데이터를 PolicyResponse 타입으로 받아온다.

👀 RetrofitClient.kt

RetrofitClient는 싱글톤으로 구현하기 위해 object 키워드를 사용한다. (코틀린에서의 object는 클래스를 싱글톤으로 구현한다.)

* 싱글톤: 객체의 인스턴스를 1개만 생성하고 계속 재사용하는 패턴

object RetrofitClient {
    private var instance: Retrofit? = null
    private const val CONNECT_TIMEOUT_SEC = 20000L

    fun getInstance() : Retrofit {
        if(instance == null){

			// 로깅인터셉터 세팅
            val interceptor = HttpLoggingInterceptor()
            interceptor.setLevel(HttpLoggingInterceptor.Level.BODY)

			// OKHttpClient에 로깅인터셉터 등록
            val client = OkHttpClient.Builder()
                .addInterceptor(interceptor)
                .connectTimeout(CONNECT_TIMEOUT_SEC, TimeUnit.SECONDS)
                .build()

            instance = Retrofit.Builder()
                .baseUrl("baseUrl을 여기 작성")
                .addConverterFactory(GsonConverterFactory.create())
                .client(client) // Retrofit 객체에 OkHttpClient 적용
                .build()
        }
        return instance!!
    }
}

나의 경우 구현 과정에서 자꾸 에러가 나서 무슨 에러인지 정확히 보기 위해 LoggingInterceptor를 추가했는데, 굳이 에러가 나는 상황이 아니더라도 Response와 Request에 대한 정보를 다음과 같이 한눈에 볼 수 있기 때문에 추후 디버깅을 위해서라도 추가해두는 편이 좋다.


🎨 PolicyFragment.kt

API 11부터는 네트워크 통신은 무조건 작업 스레드에서 해야 한다. 그렇지 않으면 Exception이 발생한다고 한다. 즉, onCreate()에 모든 코드를 때려박았다가는 문제가 된다. 따라서 스레드에서 통신 작업을 돌렸다.

class PolicyFragment : Fragment() {

    private val retrofit: Retrofit = RetrofitClient.getInstance() // RetrofitClient의 instance 불러오기
    private val api: RetrofitInterface = retrofit.create(RetrofitInterface::class.java) // retrofit이 interface 구현
    private val authToken = "토큰값을 여기 작성"

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)

        // retrofit setting
        Runnable {
          api.getPolicy("서울", "Bearer $authToken").enqueue(object : Callback<PolicyResponse>{
              // 전송 실패
              override fun onFailure(call: Call<PolicyResponse>, t: Throwable) {
                  Log.d("태그", t.message!!)
              }
              // 전송 성공
              override fun onResponse(call: Call<PolicyResponse>, response: Response<PolicyResponse>) {
                  Log.d("태그", "response : ${response.body()?.data}") // 정상출력

                  // 전송은 성공 but 서버 4xx 에러
                  Log.d("태그: 에러바디", "response : ${response.errorBody()}")
                  Log.d("태그: 메시지", "response : ${response.message()}")
                  Log.d("태그: 코드", "response : ${response.code()}")
              }
          })
        }.run()
    }
}

response로 받은 PolicyResponse 객체에서 data를 출력해보면 이렇게 정상적으로 들어왔음을 확인할 수 있다!

참고
Tistory | 괴발개발 개발새발
Tistory | 현치비
Medium | Joyce Hong

profile
기록하는 개발자 👀

0개의 댓글