본문 바로가기
안드로이드

[안드로이드 #4] Retrofit2

by algosketch 2021. 5. 18.
  • permission, dependency 설정
  • api key 와 .gitignore
  • Rectofit (Service Creator)
  • Entity
  • Service interface
  • Service 사용
  • 중요 과제

 이번 시간에는 서버와 통신을 하기 위해 Retrofit2 라는 라이브러리를 이용합니다. 서버와 HTTP 통신하기 위해 사용되는 대표적인 라이브러리는 Volley 와 Retrofit2 이 있습니다. Volley 가 진입장벽은 낮지만 Retrofit2 가 성능이 더 좋고 확장성에도 좋기 때문에 대부분 Retrofit2 을 이용합니다.
 보통 JSON을 가장 많이 이용하기 때문에 JSON 포맷을 사용하도록 하겠습니다. 옛날 API 나 공공데이터 API 의 경우 XML 을 사용하는 경우도 많이 있습니다. JSON 은 JavaScript Object 이지만 js 가 아니더라도 HTTP 통신할 때 흔히 쓰입니다.

 Retrofit 은 위와 같은 의존 관계를 가집니다. OOP 에 경험이 있다면 친숙한 모델입니다. 사실 MainActivity 도 나중에 ViewModel 로 분리할 수 있습니다. (키워드 : MVVM 패턴) 위와같이 객체지향 설계를 따라가기 때문에 우리는 Service 객체만 이용하면 됩니다. 이 구조의 장점은 Service 객체의 사용 방법만 알면 서버와 통신하는 다른 코드가 어떻게 동작하는 지 알 필요가 없습니다.

시작하기에 앞서 3가지 설정을 해 줘야 합니다.

  • permission
  • dependency
  • HTTP 사용 가능하게 설정 (서버가 https 라면 생략)

 먼저 dependency 입니다. Module 단위의 build.gradle 파일의 dependencies 에 다음 내용을 추가해 줍니다. 만약 아래 버전이 최신 버전이 아닐 경우 (JetBrains IDE 에서는)Alt + Enter 를 누르면 최신버전으로 업데이트할 수 있습니다.

// retrofit2
implementation 'com.squareup.retrofit2:converter-gson:2.9.0' 
implementation 'com.squareup.retrofit2:retrofit:2.9.0'

manifests/AndroidManifest.xml

<uses-permission android:name="android.permission.INTERNET"/> // <manifest> 이 위치에 삽입하면 됩니다. </manifest>

android:usesCleartextTraffic="true" // application 의 속성으로 넣을 수 있습니다. http 관련 설정입니다.

 

 코드가 들어가는 폴더에 새로운 폴더(패키지) model 을 추가합니다. 그리고 네 개의 파일을 만듭니다.

Configs : 전역변수처럼 이용하기 위한 kotlin class 파일, class 대신 object 키워드를 사용하여 싱글턴으로 이용
PapagoEntity : 서버로부터 전송받을 데이터의 형식을 정의한 Entity 클래스
PapagoServie : 여기서 정의한 메소드들로 서버와 통신이 가능하다. 인터페이스이다.
PapagoServiceCreator : Service 객체를 만들어주는 클래스, 다른 방식으로 구현하는 사람도 있다.

 예상 했겠지만 파파고 API 를 이용할 예정입니다. 파파고 API 는 네이버 개발자센터에 가입 후 API 이용 신청할 수 있습니다. 가입하는 방법, API 이용 신청하는 방법은 생략하겠습니다. API 이용 신청이 완료되면 다음과 같은 화면을 볼 수 있습니다.

 Configs 파일을 먼저 살펴보겠습니다.

object Configs {
    val clientID = "buhGiXpppTfUsDU7KEiY"
    val apiKey = "Client Secret"
}

 네이버로부터 발급받은 ID 와 token(api key, 여기서는 Client Secret) 을 Configs 에 넣어줍니다. 이 파일은 외부로 유출되면 안 되는 token 을 포함하고 있습니다. 따라서 이대로 github 에 push 하면 github 에서 보안 관련 이메일이 날아옵니다. github 에 push 되지 않도록 관리해야합니다. .gitignore 파일을 이용하면 git 으로 추적하지 않을 파일을 추가할 수 있습니다. .gitignore 파일은 Android 로는 보이지 않기 때문에 잠시 Project 로 바꿔줘야 합니다.

 프로젝트 최상위 디렉터리에 있는 .gitignore 파일 기준으로 다음과 같은 코드를 삽입해주면 됩니다. 폴더 이름은 사람마다 다르기 때문에 아래 코드를 참고하여 추가하시면 됩니다.

<module name>/src/main/java/<your pakage>/model/Configs.kt

 이 내용을 추가하기 전과 후의 git status 명령어 결과가 다르게 나오는 것을 확인할 수 있습니다. git status 는 터미널에서 입력할 수 있습니다.

 앞에서 언급했다시피 MainActivity 에서는 Service 객체를 통해 간접적으로 서버와 통신할 수 있습니다.

// PapagoServiceCreator.kt
class PapagoServiceCreator {
    val BASE_URL = "https://openapi.naver.com/v1/papago/" // JSON 출력

    fun create() : PapagoService {
        return Retrofit.Builder()
                .baseUrl(BASE_URL)
                .addConverterFactory(GsonConverterFactory.create())
                .build()
                .create(PapagoService::class.java)
    }
}

 이 클래스의 역할은 retrofit 의 빌더를 이용해 우리가 사용한 Service 객체를 생성해주는 역할을 합니다. builder 패턴을 이용하는 경우 메서드 단위로 개행하는 게 일반적인 코드 컨벤션입니다. 이 클래스에서 서버의 url 을 설정합니다. 서버의 url 은 API 문서에서 확인할 수 있습니다.

API 문서

 API 문서를 통해 JSON 에 포함되는 필드들을 알아낼 수 있습니다. 여기서 필요한 필드의 이름과 똑같은 변수 이름을 사용하여 Entity class 를 구현합니다. 우리에게는 translatedText 만 필요합니다. 이 경로에 해당하는 객체나 배열들을 모두 정의해야 합니다. 전체 > message > result > translatedText 구조입니다. 아래 코드로 위 translatedText 를 받을 수 있습니다.

// PapagoEntity
class PapagoEntity {
    var message: ResultMessage? = null

    inner class ResultMessage {
        var result: Result? = null

        inner class Result {
            var translatedText: String? = null
        }
    }
}

 마지막으로 Service interface 를 정의하면 서버와의 통신 준비는 끝납니다.

// PapagoService.kt
interface PapagoService {
    @FormUrlEncoded
    @POST("n2mt")
    fun requestTranslation(@Header("X-Naver-Client-Id")clientID: String = Configs.clientID,
                           @Header("X-Naver-Client-Secret")apiKey: String = Configs.apiKey,
                           @Field("source")fromLang: String = "en",
                           @Field("target")toLang: String = "ko",
                           @Field("text")text: String? = "this is android") : Call<PapagoEntity>
}

 네이버에서 발급받은 id 와 secret 을 request header 에 넣어주고 나머지 옵션들은 body 에 넣어줍니다. http 통신은 header 와 body 로 나누고 @Field 애너테이션으로 지정한 내용이 body 에 들어갑니다. POST 방식으로 요청을 보내고 @Field 애너테이션을 사용했기 때문에 @FormUrlEncoded 를 추가합니다. @Header 나 @Field 애너테이션의 (괄호) 안의 문자열은 서버에서 요구하는 이름과 같아야 합니다. API 문서를 참고하시면 됩니다. Header 에 들어갈 인증 정보 이름은 안 적혀있었지만 예제 코드를 통해 확인할 수 있습니다.

java 예제 코드

 이로써 서버와 통신한 준비가 끝났습니다. MainActivity 코드는 RecyclerView 내용과 이어집니다.

class MainActivity : AppCompatActivity() {
    private val LOG_TAG = "MainActivity Request"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)

        val papagoService = PapagoServiceCreator().create()
        val call = papagoService.requestTranslation()
        call.enqueue(object : retrofit2.Callback<PapagoEntity> {
            override fun onResponse(call: Call<PapagoEntity>, response: Response<PapagoEntity>) {
                if(response.isSuccessful) { 
                    // 성공
                    Log.d(LOG_TAG, "Successful!")

                    val result = response.body()
                    val chattingList = arrayListOf( ChattingAdapter.Message(result?.message?.result?.translatedText!!) )

                    recyclerView.adapter = ChattingAdapter(chattingList)
                    Log.e(LOG_TAG, response.raw().toString());
                }
                else { 
                    // 서버에 연결은 됐으나 결과 받기 실패
                    Log.e(LOG_TAG, "fail!")
                    Log.e(LOG_TAG, "error code : " + response.code())
                    Log.e(LOG_TAG, "error message : " + response.message())
                }
            }
            // 서버 연결 실패
            override fun onFailure(call: Call<PapagoEntity>, t: Throwable) {
                Log.d(LOG_TAG, "onFailure!")
                t.printStackTrace()
            }
        })
    }
}

 지난번에는 고정된 문자열을 RecyclerView 에 출력해줬지만 이번에는 파파고에 번역을 요청하고 그 요청 결과를 받아 결과를 RecyclerView 에 출력해줬습니다. papageService.requestTranslation() 에는 인자를 넣어줄 수 있지만 Service 를 정의한 코드에 default 값을 넣어 줬기 때문에 넣어주지 않아도 됩니다.
 기본적으로 서버나 데이터베이스에 요청하는 작업은 시간이 오래 걸리는 작업입니다. 사용자는 그 작업으로 인해 지연되면 불편함을 느낍니다. 그래서 이런 작업들은 백그라운드에서 처리해야 합니다. 선택이 아니라 강제입니다. call.endqueue() 부분이 서버 통신을 비동기로 처리하는 부분입니다. 통신을 요청했을 때 호출될 콜백 메소드를 구현하여 enqueue 의 인자로 넘겨줘야 합니다. 성공 혹은 실패하는 경우 어디로 이동하게 되는 지는 주석으로 달았습니다. 결과는 response.body 로 받을 수 있으며 우리가 만든 Entity 클래스의 객체로 나옵니다.

 위 코드에서 콜백 메소드에 대한 인자를 넘겨주는 부분을 분리시키면 아래 코드로도 작성할 수 있습니다. 두 코드의 차이를 분석하면 kotlin 문법을 이해하는 데 도움이 됩니다. object 는 싱글턴 객체를 생성한다는 의미이고, retrofit2.Callback<> { ... } 처럼 클래스 이름을 지어주지 않고 상속받을 클래스명만 명시할 수도 있습니다.

class MainActivity : AppCompatActivity() {
    private val LOG_TAG = "MainActivity Request"
    lateinit var recyclerView: RecyclerView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        recyclerView = findViewById(R.id.recyclerview)

        val papagoService = PapagoServiceCreator().create()
        val call = papagoService.requestTranslation()
        call.enqueue(SimpleClass())
    }

    inner class SimpleClass : retrofit2.Callback<PapagoEntity> {
        override fun onResponse(call: Call<PapagoEntity>, response: Response<PapagoEntity>) {
            if(response.isSuccessful) {
                // 성공
                Log.d(LOG_TAG, "Successful!")

                val result = response.body()
                val chattingList = arrayListOf( ChattingAdapter.Message(result?.message?.result?.translatedText!!) )

                recyclerView.adapter = ChattingAdapter(chattingList)
                Log.e(LOG_TAG, response.raw().toString());
            }
            else {
                // 서버에 연결은 됐으나 결과 받기 실패
                Log.e(LOG_TAG, "fail!")
                Log.e(LOG_TAG, "error code : " + response.code())
                Log.e(LOG_TAG, "error message : " + response.message())
            }
        }
        // 서버 연결 실패
        override fun onFailure(call: Call<PapagoEntity>, t: Throwable) {
            Log.d(LOG_TAG, "onFailure!")
            t.printStackTrace()
        }
    }
}

실행 결과

 

 이번 과제는 중요 과제입니다. 중요 과제 2개를 통과하셔야 프로젝트에 참여하실 수 있습니다. 중요 과제를 하나도 통과하지 못 한다면 앱센터와 이별하게될 수도 있습니다. 과제를 제출하시려면 본인의 깃허브에 과제를 올린 뒤 제게 말씀해주시면 됩니다. 기간은 종강하기 전까지이며 여러 번 피드백 받으셔도 됩니다.

 과제 : 서버로부터 여러 개의 데이터를 받아서 결과를 RecyclerView 에 출력하는 안드로이드 앱을 만들어주세요. 어떤 API 나 서버를 이용하든, 어떤 형식으로 출력하든 상관 없습니다. 원하는 UI 로 꾸미고 원하는 API 를 이용하여 만드시면 됩니다.

 이번 과제는 처음 하기에 어려울 수 있습니다. 그래서 기간을 넉넉하게 드렸고 미리 과제를 시도해 보시고 방법을 찾아보시기 바랍니다. 스터디에서 배우지 않은 내용을 찾아서 사용하셔도 좋습니다. 

 과제 예시

  • 5일간 각 요일마다 최저, 최고 기온을 알려주는 앱
  • 한글로 채팅을 치면 영어로 번역해 주는 번역 앱