[연재] 코틀린 프로젝트 - 02 - 5단계: Retrofit의 REST API의 처리

이제 실제 서버의 데이터를 JSON으로 제공해 주는 사이트를 이용해 REST(Representational State Transfer) API를 사용할 것입니다. 이것을 코드에서 객체로 담고 다시 뷰에 표시할 수 있도록 데이터 클래스에 저장해 활용하는 방법에 대해 살펴봅시다. 보통 다양한 정보를 JSON으로 제공하는 사이트는 유료 서비스와 무료 서비스가 있으며 무료 서비스는 Open API라는 형태로 여러가지 데이터를 활용할 수 있도록 제공하고 있습니다. 

Open API 제공 사이트

몇 가지 소개하자면 먼저 한국 정부에서 제공하는 공공데이터포털 사이트인 www.data.go.kr에서는 각종 공공 데이터를 XML이나 JSON으로 제공하고 있습니다. 예를 들면, 국내 관광정보 서비스를 위해 다양한 언어로 제공하고 있는데 API 활용 신청 후 API 키와 요청 URL을 받아서 활용하면 훌륭한 관광 정보 앱을 만들 수 있게 됩니다. 


www.data.go.kr 사이트의 오픈 API의 예 - 관광정보 서비스

몇 가지 유용한 오픈 API를 제공하는 서비스를 소개 하겠습니다. 

국내는 공공데이터포털에서 많은 정보를 제공하고 있으며 공공 인공지능의 하나인 언어분석 API를 활용하면 한국어 어휘관계 등을 분석할 수 있습니다. 외국 사이트로는 뉴욕타임즈가 뉴스 기사의 형식을 다양하게 선택해 제공 받을 수 있고, TMDB가 제공하는 오픈 API를 사용하면 영화의 기본정보를 제공받을 수 있습니다. 더 많은 정보를 원하면 다음 사이트에 공개된 API 서비스들을 발견할 수 있을 것입니다. 

https://github.com/toddmotto/public-apis

이런 사이트의 공개된 데이터들을 이용해 자신만의 서비스로 다시 설계할 수 있을 것입니다. 대부분의 사이트는 오픈 API를 이용하기전에 API 키를 받아서 인증해야 합니다. 한국 사이트의 경우 API 인증을 위해 공인 인증이나 전화 인증을 해야하는 경우가 많습니다. 우리 프로젝트는 영화 정보를 제공하기 위해 TMDB사이트를 이용할 것입니다. 

먼저 API키를 받기 위해 https://www.themoviedb.org/account/signup를 통해 사이트에 가입합니다. 가입 후 계정 설정을 통해 기본 언어를 한국어로 지정할 수 있습니다. 그리고 다음 그림과 같이 API 메뉴에서 [Request an API Key]의 [click here]를 누르면 API키를 받을 수 있습니다. API키의 내용을 앱에서 사용할 것이므로 따로 잘 복사해 둡니다. 


API Key v3를 앱에서 사용하기 위해 저장해 둔다.

이 키는 REST API에서 인증 시 사용할 것입니다. 

JSON 데이터와 웹 접근

이제 https://developers.themoviedb.org/3으로 개발자 문서 사이트로 이동해서 API를 테스트해 볼 차례입니다. 문서의 API 카테고리가 많습니다. 여기서 DISCOVER를 통해 인기 영화 목록을 가져올 것입니다. DISCOVER를 사용하는 예제는 다음 링크에서 확인할 수 있습니다. 

https://www.themoviedb.org/documentation/api/discover

이제 이런 API를 테스트 하기 위해서는 파이어폭스와 같은 브라우저를 사용할 수도 있지만 REST API를 전문적으로 테스트하는 PostMan과 같은 프로그램을 이용해 보겠습니다.

PostMan의 설치와 이용

먼저 다음 사이트에서 PostMan을 다운로드 및 설치합니다.

https://www.getpostman.com/

이제 앞서 살펴본 themoviedb.org에서 제공하는 문서에 나와있는 예제를 API키와 함께 구성해 테스트합니다. 

postman을 이용해 API 테스트 해보기

새로운 요청(Request)을 만들고 GET으로 지정한 뒤 (2)번에서 URL을 붙여 넣습니다. 그러면 3번과 같이 전달할 변수가 자동으로 구성됩니다. 이때 사이트로부터 복사해 두었던 API키를 api_key 항목에 값으로 지정합니다. GET 방식의 URL의 예는 다음과 같습니다. 

URL?변수명1=값1&변수명2=값2&변수명3=값3&…

그러면 관련 필드를 사용하면 URL에 다음과 같이 표현할 수 있습니다.

https://api.themoviedb.org/3/discover/movie?sort_by=popularity.desc&api_key={받은 API키 붙여넣기} &language=ko&page=1

사용된 변수로는 sort_by는 인기순으로 정렬하기 위한 popularity.desc를 사용했고 language를 통해 한국어인 ko로 서비스 받을 수 있게 지정했습니다. page는 결과가 여러 페이지로 나뉘어서 제공되는데 첫번째 페이지를 나타내도록 했습니다. 이제 (4)번을 눌렀을 때 아무 문제가 없다면 정상 상태 코드인 200을 받고 REST API의 GET요청에 의해 (5)번과 같이 JSON으로 응답할 것입니다. 

{
    "page": 1,
    "total_results": 387662,
    "total_pages": 19384,
    "results": [
        {
            "vote_count": 2141,
            "id": 335983,
            "video": false,
            "vote_average": 6.6,
            "title": "베놈",
            "popularity": 228.355,
            "poster_path": "/lc8TJW5z261JqSz3oSy5GES2ZXj.jpg",
            "original_language": "en",
            "original_title": "Venom",
            "genre_ids": [
                878
            ],
            "backdrop_path": "/VuukZLgaCrho2Ar8Scl9HtV3yD.jpg",
            "adult": false,
            "overview": "진실을 위해서라면 몸을 사리지 않는 정의로운 열혈 기자 '에디 브록'...",
            "release_date": "2018-10-03"
        },
...

이제 응답 받은 JSON을 분석해 봅시다. 여기서 우리가 데이터 클래스에서 프로퍼티로 정의했던 정보들을 사용할 것입니다. 각 results의 결과는 20개씩 구성되어 있으므로 List 구조에 넣도록 합니다. 지속적으로 정보를 불러들이기 위해 page를 사용해 다음 페이지의 results를 탐색할 것입니다. 

JSON을 위한 클래스

서버에서 제공하는 JSON 데이터를 클래스에 담기 위해 Retrofit라이브러리와 함께 JSON변환기가 필요합니다. 여기서는 https://github.com/square/moshi 에서 제공하는 Moshi를 사용할 것입니다. 다음과 같은 라이브러리를 build.gradle에 지정합니다. 

모듈의 빌드 스크립트 build.gradle
...
dependencies {
...
    // Retrofit
    implementation 'com.squareup.retrofit2:retrofit:2.4.0'
    implementation 'com.squareup.retrofit2:converter-moshi:2.0.0'
...

이번에는 패키지 data/ 에 ApiModels.kt 파일을 만들과 JSON 데이터를 담을 클래스들을 생성해 봅시다. 

REST API용 클래스 정의하기 data/ApiModels.kt
...
/**
 * REST API 응답에 사용할 클래스들
 */
class MovieListResponse(
        var page: Int,
        val results: List<MovieDetailResponse>
)

class MovieDetailResponse(
        val vote_count: Int,
        val vote_average: Float,
        val title: String,
        val release_date: String,
        val poster_path: String,
        val overview: String?
)

앞서 분석했던 JSON 데이터들을 담을 클래스를 지정했습니다. 각각의 영화 상세 정보는 다시 List에 의해 results에서 20개씩 지정될 것입니다. 이것은 page를 통해 다음 내용을 새롭게 가져올 수 있게 합니다. 

REST 요청을 위한 인터페이스와 빌더

1. 이제 Retrofit의 @GET 어노테이션을 사용해 REST API을 요청하도록 구성할 것입니다. 계속 data/ 하위에 TheMovieService.kt 파일을 생성합니다. 

REST 서비스를 위한 인터페이스 data/TheMovieService.kt
...

// The Movie API 서비스르 위한 키
const val API_KEY = "f2-------------------------cb1"  // 부여받은 API 키를 지정한다.
const val BASE_URL = "https://api.themoviedb.org/3/"

/**
 * Retrofit의 @GET 어노테이션으로 HTTP의 GET요청으로 JSON을 읽어온다
 */
interface  TheMovieService {
    @GET("discover/movie")
    /**
     * REST 요청을 처리하기 위한 메서드
     * @param par QueryMap을 통해 질의한 쿼리문을 Map으로 부터 받는다.
     * @return Call<T> 콜백 인터페이스 반환, T는 주고 받을 데이터 구조
     * @QueryMap 어노테이션은 위치가 바뀌어도 동적으로 값을 받아올 수 있게 한다.
     */
    fun getTop(@QueryMap par: Map<String, String>): Call<MovieListResponse>

}

Retrofit2에서 사용하는 @GET 어노테이션을 통해 BASE_URL과 함께 사용되는 URL이 지정됩니다. page와 같이 보낼 변수들은 @QueryMap 어노테이션에 의해 Map<>정보로 사용될 것입니다. 필요한 경우 @Headers 어노테이션으로 받을 형식을 지정할 수 있습니다. 

@Headers({"Accept: application/json"})

반환 자료형은 Call<MovieListResponse>을 사용했는데 이 Call 클래스는 Retrofit이 제공하는 클래스로 각 요청 처리가 성공인지 실패인지 에 따라 실행 하도록 구성할 수 있습니다. 또, 제네릭 자료형으로 선언된 MovieListResponse 은 서버로부터 응답 받은 데이터를 저장할 클래스가 됩니다. 

2. 초기화는 다음과 같이 data/에 RestApi.kt 파일을 만들고 Retrofit의 빌더를 사용합니다. 

Retrofit의 빌더 생성 data/RestApi.kt
package com.acaroom.themovieapp.data

import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory

class RestApi {
    private val theMovieService: TheMovieService

    init {
        val retrofit = Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(MoshiConverterFactory.create())
            .build()
        theMovieService = retrofit.create(TheMovieService::class.java)
    }

    fun getMovieListRetrofit(param: Map<String, String>): Call<MovieListResponse> {
        return theMovieService.getTop(param)
    }
}

Retrofit의 Builder를 통해 URL을 초기화하고 JSON을 위한 컨버터를 Moshi로 지정하고 있습니다. retrofit.create()의 TheMovieService::class.java와 같은 형식은 자바 클래스의 인스턴스를 코틀린의 KClass 인스턴스로 사용할 수 있게 해 줍니다. 이렇게 해서 최종적으로 요청을 보내고 영화 정보를 가져오는 getMovieListRetrofit()함수가 구성되었습니다. 

MovieManager를 통한 서비스 호출

3. 이제 요청을 보낼 수 있도록 MovieManager의 프로퍼티에 NewsAPI 클래스를 사용해 처리하도록 정의해 줍니다. 이제 MovieManager를 다음과 같이 변경합니다. 

MovieManager를 통한 서비스 호출 ui/MovieManager.kt
class MovieManager(private val api: RestApi = RestApi()) {
    fun getMovieList(): Observable<List<MovieItem>> {
        return Observable.create { subscriber ->
            val param = mapOf( // (1) GET 요청용 변수를 mapOf()를 사용해 지정
                "page" to "1",
                "api_key" to API_KEY,
                "sort_by" to "popularity.desc",
                "language" to "ko"
            )
            val call = api.getMovieListRetrofit(param) // (2) REST API의 요청
            val response = call.execute() // (3) 요청의 실행

            if (response.isSuccessful) {
                val movieList = response.body()?.results?.map { // (4) 응답받은 데이터에서 results읽기
                    MovieItem( // (5) 각각의 목록 요소를 데이터 클래스로 초기화
                        it.vote_count,
                        it.vote_average,
                        it.title,
                        it.release_date,
                        it.poster_path,
                        it.overview
                        )
                }
                if (movieList != null) {
                    subscriber.onNext(movieList) // (6) 구독자에게 데이터 변경 이벤트 알림
                }
                subscriber.onComplete()
            } else {
                subscriber.onError(Throwable(response.message()))
            }
        }
    }
}

먼저 (1)번에서 URL에 같이 보낼 변수들을 mapOf()를 사용해 정의합니다. 이렇게 정의된 param은 (2)번에 의해 요청을 만들어내게 됩니다. (3)번에서 실행 후 서버로부터 정상적인 응답을 하면 response.isSuccessful을 통해 관련 내용을 처리할 수 있습니다. 여기서는 (4)번에 의해 응답 받은 데이터의 results를 map을 통해서 데이터 클래스인 MovieItem에 영화 정보를 각각 지정합니다. 이것을 다시 movieList로 반환 받으면 (6)번에 의해 구독자에게 데이터 변경 이벤트를 알려 표시할 준비를 합니다.

4. 이제 poster_path 형식을 TMDB 사이트의 다음 문서를 읽어보면 이미지를 읽어오는 경로는 다른 형태로 추가 되어야 합니다.

https://developers.themoviedb.org/3/getting-started/images

이 문서에 의하면 다음과 같이 poster_path의 이미지를 사용할 수 있습니다. 

https://image.tmdb.org/t/p/w500/{poster_path}

이제 MovieItemAdapter.kt 파일의 이미지 부분을 다음과 같이 수정합시다. 

이미지 경로의 수정 ui/adapter/MovieItemAdapter.kt
...
class MovieItemAdapter() : ItemAdapter {
...
    inner class MovieViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder(
            parent.inflate(R.layout.item_movie)) {

        private val imgPoster = itemView.img_poster
...

        fun bind(item: MovieItem) {
            imgPoster.loadImg("https://image.tmdb.org/t/p/w500/${item.poster_path}") // (1)
            overview.text = item.overview
   ...

(1)번과 같이 poster_path 앞에 추가 경로를 넣고 수정합니다. 

이제 모든 수정사항이 반영되었다면 실행하고 서버로부터 정보를 제대로 읽어오는지 확인합니다.

 

정보를 읽어오는데 문제는 없으나 최대 20개의 영화정보를 표현할 뿐 더 이상의 페이지가 나타나지 않습니다. 또한 각 아이템의 클릭 이벤트 처리도 빠져 있습니다. 또 다른 문제로 앱이 가로로 변경 되면 기존에 보던 데이터가 사라지고 화면이 다시 그려지며 데이터가 초기화 됩니다. 오리엔테이션에 대한 고려가 필요합니다.

압축하면 다음과 같은 3가지 추가 기능을 제공해야 합니다. 

  • 20개이상의 영화 정보를 위한 무한 스크롤의 제공
  • 가로 혹은 세로 화면으로 변경되도 기존 데이터의 유지
  • Back키가 눌리게 되면 앱의 완전 종료 Home키가 사용되면 기존 정보 유지
  • 각 아이템의 클릭 이벤트 처리

이제 다음 단계에서 오리엔테이션의 고려와 스크롤 시 지속적으로 데이터를 보여주기 위해 Infinite Scroll의 적용, UI 개선등을 살펴보겠습니다.

 

Note:'Do it! 코틀린 프로그래밍'의 연속 과정입니다. 

 

youngdeok's picture

Language

Get in touch with us

"If you would thoroughly know anything, teach it to other."
- Tryon Edwards -