본문 바로가기
오늘은 뭘 배울까?/Android

레트로핏을 이용하여 서버와 통신하자!

by Kim Juhwan 2021. 4. 12.

1. 사용법
   1-1. Interface 정의
   1-2. Retrofit 객체 생성
   1-3. HTTP 요청과 응답
2. 예제 소스

 


 

이번 게시물에서 만들 거

 

이번 게시물에서는 Retrofit을 이용하여 버튼을 누를 때마다 다음 페이지의 공지사항을 불러와서 띄우는 기능을 구현할 것이다. 사이트는 크롤링을 허용해둔 배민 사장님 광장을 이용하였다.

 

Retrofit의 개념과 사용 전 알아야 할것들은 이미 이전 게시물에서 다루었다.
먼저 읽고 오는 것을 추천!

 

1. 사용법

1-1. Interface 정의

interface BaeminService {
    @GET("contents?typeCode=notice&size=10")
    fun loadNotice(@Query("page") page: String): Call<Baemin>
}

우선 위와 같은 API interface를 만들어야 한다.

이곳에 내가 어떠한 요청을 어떻게 보낼건지 메서드를 정의해주면 되는데

어떻게 정의하면 될지를 차근차근 알아보자.

 

interface BaeminService {
    @GET("contents?typeCode=notice&size=10")
    fun loadNotice(@Query("page") page: String): Call<Baemin>
}

이전 게시물에서 배민 공지사항은 GET 요청으로 받아올 수 있다는 사실을 알아냈다.

@GET, @POST 이런 식으로 어떤 작업을 어노테이션을 붙여주면 된다.

 

interface BaeminService {
    @GET("contents?typeCode=notice&size=10")
    fun loadNotice(@Query("page") page: String): Call<Baemin>
}

https://www.naver.com/user/profile

https://www.naver.com/blog?id=juhwan&password=1234

예를 들어, 위와 같은 주소가 있다고 치자.

파란색은 Base Url 빨간색은 End Point 초록색은 Parameter로 구분할 수 있다.

(Base Url과 End Point는 마지막 '/'를 기준으로 구분한다)

아무튼 어노테이션 안에 End Point를 넣어주면 되는데, 만약 필수 파라미터가 있다면 같이 넣어줘야 한다.

 

interface BaeminService {
    @GET("contents?typeCode=notice&size=10")
    fun loadNotice(@Query("page") page: String): Call<Baemin>
}

배민 공지사항 사이트에서 size와 같은 파라미터는 항상 고정되어 있는 값이다. (항상 한 페이지에 10개의 게시물을 보여주니까!) 하지만 page처럼 값을 동적으로 변경해야 하는 파라미터는 @Query 어노테이션을 이용해서 메서드를 호출할 때 값을 넘겨받아 주소에 포함시켜야 한다.

 

interface UnivService {

    @FormUrlEncoded
    @POST("boardList.do")
    fun loadNotice(@FieldMap fields: MutableMap<String, String>): Call<Result>
}

그리고 GET 요청과 달리 POST 요청은 주소에 파라미터가 노출되지 않는다는 특징이 있기 때문에

위와 같이 모든 파라미터를 @FieldMap 어노테이션을 이용해 보내준다.

이때 @FormUrlEncoded로 같이 사용해주어야 한다.

(이놈의 정체에 대해서는 좀 더 연구해봐야 할 듯... 아무튼 FieldMap 사용할 때 같이 사용 안 하면 에러가 뜬다.)

 

interface BaeminService {
    @GET("contents?typeCode=notice&size=10")
    fun loadNotice(@Query("page") page: String): Call<Baemin>
}

마지막으로 Call<> 안에 응답받을 Body 타입의 data class를 적어주면 된다.

이전 게시물에서 우리는 JSON 타입으로 된 공지사항을 받기 위해

'Baemin'이라는 이름으로 data class를 만들었다. 고로, <Baemin>이렇게 적어주면 끝!

만약 JSON이 아니라 HTML로 응답해주는 사이트라면 <ResponseBody> 이렇게 적어주면 된다.

 

1-2. Retrofit 객체 생성

object BaeminClient {
    private const val baseUrl = "https://ceo.baemin.com/cms/v1/"
    private val retrofit = Retrofit.Builder()
            .baseUrl(baseUrl)
            .addConverterFactory(GsonConverterFactory.create())
            .build()

    val service = retrofit.create(BaeminService::class.java)!!
}

만약 서버 호출이 필요할 때마다 인터페이스를 구현해야 한다면 너무 비효율적이기 때문에

Client 파일은 싱글톤(Object)으로 제작하는 것이 바람직하다.

 

아무튼.. 이 파일에서는 Retrofit 객체를 생성하면서 여러 옵션을 정하게 되는데 이를 알아보자.

 

object BaeminClient {
    private const val baseUrl = "https://ceo.baemin.com/cms/v1/"
    private val retrofit = Retrofit.Builder()
               .baseUrl(baseUrl)
               .addConverterFactory(GsonConverterFactory.create())
               .build()

    val service = retrofit.create(BaeminService::class.java)!!
}

우선 어떤 서버에 요청을 보낼 것인지 baseUrl() 메서드에 넘겨주어야 하는데

이때 주소의 끝은 항상 '/'로 끝나야 함을 유의해야 한다.

 

object BaeminClient {
    private const val baseUrl = "https://ceo.baemin.com/cms/v1/"
    private val retrofit = Retrofit.Builder()
               .baseUrl(baseUrl)
               .addConverterFactory(GsonConverterFactory.create())
               .build()

    val service = retrofit.create(BaeminService::class.java)!!
}

addConverterFactory()는 데이터를 파싱 할 converter를 추가하는 메서드이다.

JSON과 같은 데이터는 자바나 코틀린에서 바로 사용할 수 있는 데이터 형식이 아니기 때문에

이를 변환해주기 위해 이러한 converter를 사용해야 한다.

여기서 사용한 Gson은 구글에서 만든 라이브러리이다. 다른 컨버터를 원한다면 그걸 사용해도 되고, 여러 개를 추가할 수도 있다.

 

object BaeminClient {
    private const val baseUrl = "https://ceo.baemin.com/cms/v1/"
    private val retrofit = Retrofit.Builder()
               .baseUrl(baseUrl)
               .addConverterFactory(GsonConverterFactory.create())
               .build()

    val service = retrofit.create(BaeminService::class.java)!!
}

이렇게 해서 만들어진 Retrofit 객체를 이용해 Interface를 구현하면 된다.

 

1-3. HTTP 요청과 응답

class BaeminRepository {

    fun loadBaeminNotice(page: Int, mCallback: MainActivity) {
        val call = BaeminClient.service.loadNotice(page.toString())

        call.enqueue(object : Callback<Baemin> {
            override fun onResponse( // 통신에 성공한 경우
                call: Call<Baemin>,
                response: Response<Baemin>
            ) {
                if(response.isSuccessful()){ // 응답을 잘 받은 경우
                    mCallback.loadComplete(response.body()!!.data)
                } else {
                    // 통신은 성공했지만 응답에 문제가 있는 경우
                }
            }

            override fun onFailure(call: Call<Baemin>, t: Throwable) {
                // 통신에 실패한 경우
            }
        })
    }
}

이제 앞서 생성한 파일들을 이용해 HTTP 요청을 보내고 응답을 받는 작업을 해줄 차례이다.

 

class BaeminRepository {

    fun loadBaeminNotice(page: Int, mCallback: MainActivity) {
        val call = BaeminClient.service.loadNotice(page.toString())

        call.enqueue(object : Callback<Baemin> {
            override fun onResponse(
                call: Call<Baemin>,
                response: Response<Baemin>
            ) {
                if(response.isSuccessful()){
                    mCallback.loadComplete(response.body()!!.data)

                } else {
                    
                }
            }

            override fun onFailure(call: Call<Baemin>, t: Throwable) {
               
            }
        })
    }
}

 

  • enqueue : 비동기 방식
  • execute : 동기 방식

Retrofit에서는 2가지 통신 메서드를 지원한다.

솔직히 나는 이걸 동기로 실행해야 하는 경우가 있는지 (있다면 대체 왜 있는지) 모르겠다.

영문으로 검색해도 이유를 못 찾겠음...

아무튼 비동기로 통신을 요청하기 위해 enqueue를 사용하자.

 

비동기에 대해서는 코루틴 공부하기 게시물에서 잠시 언급한 적이 있다.
2022-01-06 추가)
Unit Test에 대해서 공부하고 적용해보다가 처음으로 execute(동기 방식)을 사용해보았다.

아직 Unit Test에 대한 이해가 부족해서인지 모르겠지만 내가 구현하고자 하는 로직은 비동기로 해결할 수 없었다.
비동기가 아닌 동기로 실행해야 하는 상황이 있긴 있는 것 같다.

 

class BaeminRepository {

    fun loadBaeminNotice(page: Int, mCallback: MainActivity) {
        val call = BaeminClient.service.loadNotice(page.toString())

        call.enqueue(object : Callback<Baemin> {
            override fun onResponse( // 통신에 성공한 경우
                call: Call<Baemin>,
                response: Response<Baemin>
            ) {
                if(response.isSuccessful()){
                    mCallback.loadComplete(response.body()!!.data)
                } else {
                    
                }
            }

            override fun onFailure(call: Call<Baemin>, t: Throwable) { // 통신에 실패한 경우
                
            }
        })
    }
}

요청을 보내고 난 다음 결과는 2가지로 나뉜다.

통신에 성공했을 경우는 onResponse

통신에 실패했을 경우는 onFailure

실패한 경우는 그에 따른 처리를 해주어야 나중에 에러가 발생하지 않는다.

ex) 응답에 성공할 것이라 생각하고 화면에 데이터를 띄우려고 했는데 아무런 데이터가 없다면...? -> 문제 발생

 

class BaeminRepository {

    fun loadBaeminNotice(page: Int, mCallback: MainActivity) {
        val call = BaeminClient.service.loadNotice(page.toString())

        call.enqueue(object : Callback<Baemin> {
            override fun onResponse(
                call: Call<Baemin>,
                response: Response<Baemin>
            ) {
                if(response.isSuccessful()){ // 응답을 잘 받은 경우
                    mCallback.loadComplete(response.body()!!.data)
                } else { // 통신은 성공했지만 응답에 문제가 있는 경우
                    
                }
            }

            override fun onFailure(call: Call<Baemin>, t: Throwable) {
                
            }
        })
    }
}

앞서 설명했던 onFailure의 경우 통신 자체가 되지 않는 상황에 속하는데

통신은 되는데 응답이 원하는 결과가 아닌 경우가 있다.

이건 response.isSuccessful을 통해 구분할 수 있다.

 

 

404 Not Found... ㅂㄷㅂㄷ

 

인터넷을 하면서 위와 같은 페이지를 접한 경험이 가끔씩 있을 것이다.

인터넷이 끊기는 등의 문제로 통신 자체를 실패했을 때 호출되는 메서드가 onFailure인 거고

통신은 성공했으나 (우리 집 컴퓨터는 멀쩡하고 네이버도 잘 들어가지지만)

돌아오는 응답에 문제가 있는 경우를 isSuccessful 메서드인 것이다.

 

응답에 문제가 생기는 이유와 종류는 되게 다양하다.

앞에 있는 저 '404'와 같은 숫자가 에러의 내용을 알려주는데 검색하면 다 나온다.

저 숫자 코드를 알아내고 싶다면 response.code() 메서드를 사용하면 된다.

 

class BaeminRepository {

    fun loadBaeminNotice(page: Int, mCallback: MainActivity) {
        val call = BaeminClient.service.loadNotice(page.toString())

        call.enqueue(object : Callback<Baemin> {
            override fun onResponse(
                call: Call<Baemin>,
                response: Response<Baemin>
            ) {
                if(response.isSuccessful()){
                    mCallback.loadComplete(response.body()!!.data)

                } else {
                    
                }
            }

            override fun onFailure(call: Call<Baemin>, t: Throwable) {
                
            }
        })
    }
}

모든 통신과 응답이 정상적이었다면 response.body에는 요청한 데이터가 들어있을 것이다.

우리는 'Baemin'으로 이름 지은 data class 형태로 요청을 했고 응답을 받았으므로

Baemin 형태의 데이터가 들어있을 것이다.

(뒤에 '.data'는.. Baemin안에 있는 data라는 이름의 데이터 클래스를 가져오기 위해 쓴 것이다)

이제 이 데이터를 지지고 볶고 사용하면 된다.

 

단, 비동기 실행이므로 LiveData나 콜백 함수 기능 등을 이용해서 받아온 응답을 처리해야 한다.

콜백 함수가 예제로 쓰기 더 쉬울 것 같아서 여기서는 콜백 함수를 사용했다.

 

class BaeminRepository {

    fun loadBaeminNotice(page: Int, mCallback: MainActivity) {
        val call = BaeminClient.service.loadNotice(page.toString())

        call.enqueue(object : Callback<ResponseBody> {
            override fun onResponse(
                call: Call<ResponseBody>,
                response: Response<ResponseBody>
            ) {
                if(response.isSuccessful()){
                    mCallback.loadComplete(response.body()!!.string())
                } else {

                }
            }

            override fun onFailure(call: Call<ResponseBody>, t: Throwable) {

            }
        })
    }
}

만약 응답받은 데이터 타입이 JSON이 아니라 HTML이라면

목차 [1-1]에서 말했듯이 <ResponseBody>로 데이터를 받아야 한다.

이렇게 받은 데이터는 생 날것의 데이터(?)이기 때문에 컴파일러에게 "야 이거 string형이야!"라고 알려주어야 한다.

주의하자. toString이 아니라 string이다.

아무튼 그렇게 받아온 데이터를 Jsoup 라이브러리 등을 통해 파싱 해서 사용하면 된다.

 

2. 예제 소스

 

juhwankim-dev/SelfStudy

코틀린으로 공부한 것들을 올리는 공간입니다. Contribute to juhwankim-dev/SelfStudy development by creating an account on GitHub.

github.com

 


💡 느낀 점

  • 드디어 Retrofit 정리를 끝냈다...! 공부하면서 정말 이것저것 시도해보고 실패해보고 한 듯 ㅠㅠ
  • Deprecated된 다른 방법들은... 대체 얼마나 더 어렵고 불편했던 걸까.. (아니야 알고 싶지 않다)
  • 부끄럽지만 string과 toString의 차이를 이제 알게 되었다.

반응형

댓글