이번 영상에서는 Retrofit 라이브러리를 써서 카카오의 책 검색 API를 다루는 법에 대해 알아보도록 하겠습니다.
카카오의 책 검색 API는 다음 책 서비스에서 질의어로 도서 정보를 검색하는데 이 때 원하는 검색어와 함께 결과 형식 파라미터를 선택적으로 추가할 수 있습니다. API의 REQUEST 형식은 다음과 같으며, 파라미터로는 query, sort, page, size, target이 있습니다.
GET /v3/search/book HTTP/1.1
Host: dapi.kakao.com
Authorization: KakaoAK {REST_API_KEY}
API를 사용하기 위해서 Kakao Developers Console에서 앱을 추가하고 REST API 키를 획득합니다.
그럼 얻어진 키를 써서 실제 응답을 얻어보도록 하겠습니다. 무료로 REST API 테스트를 할 수 있는 REST test test... 사이트를 이용하겠습니다. REQUEST 형식을 참고해 파라미터를 넣어주면 JSON 응답을 확인할 수 있습니다.
Endpoint: https://dapi.kakao.com/v3/search/book
Header Name: Authorization
Header Value: KakaoAK 720435404bfff76560fb550b1e267fac
Parameter Name: query
Parameter Value: android
그럼 Retrofit으로 책 검색 API를 이용해 보도록 하겠습니다. 우선은 앱에 인터넷 접근 권한을 부여합니다.
<uses-permission android:name="android.permission.INTERNET" />
다음은 build.gradle에 Retrofit과 Moshi, Logging Interceptor, 그리고 어노테이션 처리를 위한 kapt Dependency를 추가합니다.
plugins {
id 'kotlin-kapt'
}
// Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
// Moshi
implementation 'com.squareup.moshi:moshi:1.13.0'
kapt 'com.squareup.moshi:moshi-kotlin-codegen:1.13.0'
// Okhttp
implementation 'com.squareup.okhttp3:okhttp:4.9.3'
implementation 'com.squareup.okhttp3:logging-interceptor:4.9.3'
JSON 데이터는 앱이 다룰 수 있는 데이터 객체로 변환하게 되는데 이 때 그 객체의 틀이 되는것이 Data Transfer Object (DTO)입니다.
그럼 북 API의 JSON 응답에 대응하는 DTO를 작성합니다. JSON To Kotlin Class 플러그인을 설치한 뒤 data/model 폴더 아래에서 New > Kotlin data class File from JSON을 선택해 위의 JSON을 붙여넣고 SearchResponse라는 이름으로 변환합니다. 그리고 Advanced > Annotation에서 DTO 변환에 사용할 컨버터를 정해주어야 하는데 여기서는 Moshi 를 사용하도록 하겠습니다.
메뉴에서 고를 수 있는 Moshi는 Reflect와 Codegen 두 가지가 있는데요, 여기서는 MoShi (Codegen)을 사용하도록 하겠습니다. 네트워크에서 받은 데이터를 DTO로 변환하는 작업은 컴파일 타임이 아니라 앱이 동작중인 런타임에 실행됩니다. 동작중인 앱은 DTO의 클래스 정보를 모르기 때문에 리플렉션이라는 기능을 써서 클래스 정보에 접근하게 됩니다. 리플렉션 작업은 java.lang.reflect라는 패키지에 의해 수행되는데 다음과 같은 프로퍼티에 대해 접근할 수 있습니다.

리플렉션을 이용하면 이렇게 런타임에 클래스 정보를 다룰 수 있지만 다음과 같은 단점도 있기 때문에 필요한 상황에만 제한적으로 사용하는 것이 좋습니다.
Moshi의 Codegen은 이러한 자바 리플렉션을 이용하지 않고 컴파일 타임에 Kotlin 코드를 생성하는 기능으로, 빌드시간은 약간 증가할 수 있으나 앱 성능을 더 높일 수 있기 때문에 사용되었습니다.

JSON은 객체를 중괄호{} 로 감싸서 표시하는데 이걸 DTO로 변환할 때에는 객체 하나를 데이터 클래스 하나로 대응시키기 때문에 3개의 데이터 클래스가 생성됩니다. 이 때 @Json만으로는 Moshi가 코틀린에서 변환에 실패하기 때문에 @field:Json으로 바꾸어주고 Document 클래스의 이름은 좀 더 직관적으로 알아보기 위해 Book으로 변경합니다. translator는 값이 없어서 타입이 Any로 지정되었는데 String으로 수정합니다. 데이터 클래스에 대한 보다 구체적인 설명은 보강이론의 Data class 기초를 참고하시기 바랍니다.
@JsonClass(generateAdapter = true)
data class SearchResponse(
@field:Json(name = "documents")
val documents: List<Book>,
@field:Json(name = "meta")
val meta: Meta,
)
@JsonClass(generateAdapter = true)
data class Meta(
@field:Json(name = "is_end")
val isEnd: Boolean,
@field:Json(name = "pageable_count")
val pageableCount: Int,
@field:Json(name = "total_count")
val totalCount: Int,
)
@JsonClass(generateAdapter = true)
data class Book(
@field:Json(name = "authors")
val authors: List<String>,
@field:Json(name = "contents")
val contents: String,
@field:Json(name = "datetime")
val datetime: String,
@field:Json(name = "isbn")
val isbn: String,
@field:Json(name = "price")
val price: Int,
@field:Json(name = "publisher")
val publisher: String,
@field:Json(name = "sale_price")
val salePrice: Int,
@field:Json(name = "status")
val status: String,
@field:Json(name = "thumbnail")
val thumbnail: String,
@field:Json(name = "title")
val title: String,
@field:Json(name = "translators")
val translators: List<String>,
@field:Json(name = "url")
val url: String
)
우선은 REQUEST 요청에 사용할 URL과 API 키 정보를 util 패키지 아래 Constants 클래스에 모아줍니다.
object Constants {
const val BASE_URL = "https://dapi.kakao.com/"
const val API_KEY = "720435404bfff76560fb550b1e267fac"
}
API_KEY를 이렇게 사용하면 편하긴 하지만 모든 사람이 볼 수 있게 노출되어 버리지요. Android Gradle plugin 7.0.2 이상을 사용하는 프로젝트에서는 Secrets Gradle Plugin for Android를 사용해서 더 안전하게 키를 주입할 수 있습니다. 그럴려면 우선 gradle에 Dependency를 추가합니다.
// project 레벨 build.gradle
plugins {
+ id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin' version '2.0.1' apply false
}
// app 레벨 build.gradle
plugins {
+ id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'
}
그리고 API Key 값을 다음과 같이 local.properties파일에 기입합니다. local.properties는 로컬 컴퓨터의 환경을 저장하는 파일이라 버전 관리에서 제외되기 때문에 다른 사람이 이 파일을 볼 수 없습니다.
bookApiKey=720435404bfff76560fb550b1e267fac
그리고 프로젝트를 다시 빌드한 뒤 Constants의 API_KEY를 다음과 같이 수정하면 키를 감출 수 있습니다.
const val API_KEY = BuildConfig.bookApiKey
Retrofit은 HTTP API의 request를 인터페이스로 정의하여 사용합니다. 그러면 data/api 패키지 아래에 API_KEY와 인자를 전달받아 북 API에 GET 요청을 수행하는 서비스를 만들어보겠습니다.
interface BookSearchApi {
@Headers("Authorization: KakaoAK $API_KEY")
@GET("v3/search/book")
suspend fun searchBooks(
@Query("query") query: String,
@Query("sort") sort: String,
@Query("page") page: Int,
@Query("size") size: Int
) : Response<SearchResponse>
}
@Get 요청과, 인증에 필요한 @Headers를 정의해 줍니다. 그리고 나머지 파라미터는 @Query 어노테이션을 써서 전달합니다. 인터페이스의 메소드인 searchBooks는 SearchResponse 타입을 가지는 Response 클래스를 반환하도록 정의하면 됩니다.
다음은 서비스를 사용하기 위한 Retrofit 인스턴스를 만들어줍니다.
object RetrofitInstance {
private val okHttpClient: OkHttpClient by lazy {
val httpLoggingInterceptor = HttpLoggingInterceptor()
.setLevel(HttpLoggingInterceptor.Level.BODY)
OkHttpClient.Builder()
.addInterceptor(httpLoggingInterceptor)
.build()
}
private val retrofit: Retrofit by lazy {
Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create())
.client(okHttpClient)
.baseUrl(BASE_URL)
.build()
}
val api: BookSearchApi by lazy {
retrofit.create(BookSearchApi::class.java)
}
}
여러개의 retrofit 객체가 만들어지면 자원도 낭비되고 통신에 혼선이 발생할 수도 있습니다. 그래서 여기서는 object와 lazy 키워드를 조합함으로써 실제 사용되는 순간이 와야 비로소 만들어지게 되고, 단 하나의 인스턴스만이 만들어지도록 싱글톤으로 구현하였습니다. 싱글톤에 대한 더 구체적인 설명은 보강이론 섹션의 Singleton Pattern 기초 영상을 참고하시기 바랍니다.
그리고 빌더 패턴을 통해 retrofit 객체를 만들어줍니다. addConverterFactory에서 DTO 변환에 사용할 JSON 컨버터를 Moshi로 지정하고, baseUrl 을 전달한 뒤 build()로 객체를 생성합니다.
retrofit 객체를 생성할 때 client 속성에 OkHttp interceptor를 넘겨주어 로그캣에서 패킷내용을 모니터링하도록 하겠습니다. OkHttp interceptor는 다음과 같이 서버와 어플리케이션 사이에서 데이터를 수집하는 작업을 수행합니다.

마지막으로 retrofit 인스턴스의 create 명령을 이용해서 BookSearchApi의 인스턴스를 만들어주면 API 사용준비가 완료됩니다.