JVM (Apollo Kotlin)
Apollo Kotlin 을 사용하여 Delivery Tracker 를 사용하는 예시 입니다.
Step 1~4 의 초기 설정을 마치면, 아래와 같은 API를 손쉽게 사용하실 수 있습니다.
val response = apolloClient.query(
GetTrackLastEventQuery(
carrierId = "kr.cjlogistics",
trackingNumber = "1234567890",
)
).execute()
val notFoundError = response.findDeliveryTrackerError(code="NOT_FOUND")
if (notFoundError != null) {
println("NotFound : ${notFoundError.message}")
} else {
println(response.data?.track?.lastEvent?.status?.code)
}
아래 코드들은 의존성을 최소화 하기 위해 코드가 장황한 편 입니다. 사용하고 계신 HTTP Client 및 JSON Parser 라이브러리를 사용하시면 코드를 간소화 할 수 있습니다.
Step 1. 프로젝트 설정
Step 1.1. Apply the plugin
여러분의 프로젝트의 build.gradle.kts
파일의 plugins
에 Apollo Gradle Plugin(com.apollographql.apollo3
)을 추가합니다.
plugins {
// ...
id("com.apollographql.apollo3") version "3.8.2"
}
Apollo Plugin에는 프로젝트를 빌드할 때 쿼리에서 모델을 생성하는 컴파일러가 포함되어 있습니다.
Step 1.2. Configure the Apollo Gradle plugin
Apollo Plugin 을 구성하여 Codegen 이 될 위치 등을 지정합니다. 여러분의 프로젝트의 build.gradle.kts
파일에 아래 내용을 추가합니다.
import java.net.URL
import java.util.Base64
import java.io.IOException
// ...
apollo {
service("DeliveryTracker") {
packageName.set("com.demoproject.deliverytracker.api")
schemaFile.set(File("src/main/graphql/schema.graphqls"))
introspection {
endpointUrl.set("https://apis.tracker.delivery/graphql")
headers.put("Authorization", providers.provider {
var accessToken = providers.gradleProperty("deliveryTracker.introspection.accessToken").orNull
if (accessToken != null) {
return@provider "Bearer $accessToken"
}
val clientId = providers.gradleProperty("deliveryTracker.introspection.clientId").orNull
val clientSecret = providers.gradleProperty("deliveryTracker.introspection.clientSecret").orNull
if (clientId == null || clientSecret == null) {
throw InvalidUserDataException("Either the property 'deliveryTracker.introspection.accessToken' must exist, or both 'deliveryTracker.introspection.clientId' and 'deliveryTracker.introspection.clientSecret' properties must exist.")
}
val connection = URL("https://auth.tracker.delivery/oauth2/token").openConnection() as java.net.HttpURLConnection
connection.requestMethod = "POST"
connection.doOutput = true
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
connection.setRequestProperty("Authorization", "Basic " + Base64.getEncoder().encodeToString("$clientId:$clientSecret".toByteArray()))
val body = "grant_type=client_credentials".toByteArray()
connection.setRequestProperty("Content-Length", body.size.toString())
connection.outputStream.use { stream ->
stream.write(body, 0, body.size)
}
if (connection.responseCode >= 400) {
val response = connection.errorStream.bufferedReader().use { it.readText() }
throw IOException("Failed to authenticate with Delivery Tracker. Server responded with error: $response")
}
val response = connection.inputStream.bufferedReader().use { it.readText() }
val matchResult = """"access_token"\s*:\s*"(.+?)"""".toRegex().find(response)
accessToken = matchResult?.groups?.get(1)?.value
if (accessToken == null) {
throw IOException("Authentication failed: Unable to parse access token from the response. Response received: $response")
}
"Bearer $accessToken"
})
}
}
}
Step 1.3. Add dependencies
dependencies {
// ...
implementation("com.apollographql.apollo3:apollo-runtime:3.8.2")
}
Step 1.4. Configure the Gradle Properties
Schema Download 를 위해서는 인증 정보 설정이 필요합니다.
프로젝트 디렉토리의 gradle.properties
등을 수정하여 필요한 Gradle Properties 를 주입하여야 합니다.
Gradle Properties 설정 방법에 대해 더 자세히 알고 싶은 경우 이곳을 참고해주세요.
# ...
deliveryTracker.introspection.clientId=[YOUR_CLIENT_ID]
deliveryTracker.introspection.clientSecret=[YOUR_CLIENT_SECRET]
clientId, clientSecret 에 대해 알고 싶은 경우 이곳을 참고 해주세요.
Step 1.4. Reload Gradle project
마지막으로 변경 사항을 반영하기 위해 Gradle build script 를 reload 합니다.
이때 schema 파일이 없어 에러가 발생하는 경우 임시 schema 파일을 생성합니다.
echo "type Query { dummy: Boolean }" > src/main/graphql/schema.graphqls
Step 2. Download a schema
Gradle 의 downloadDeliveryTrackerApolloSchemaFromIntrospection task 를 실행하여 Schema를 다운로드 합니다.
./gradlew :downloadDeliveryTrackerApolloSchemaFromIntrospection
Step 3. Write your first query
Codegen에 필요한 GraphQL Query를 작성합니다. 아래 예시는 운송장의 마지막 이벤트를 가져오는 API 입니다.
query GetTrackLastEvent($carrierId: ID!, $trackingNumber: String!) {
track(carrierId: $carrierId, trackingNumber: $trackingNumber) {
lastEvent {
time
status {
code
}
}
}
}
Gradle의 generateDeliveryTrackerApolloSources
Task를 실행하여 위에서 만든 API(Query)를 CodeGen 합니다.
./gradlew :generateDeliveryTrackerApolloSources
Step 4. Execute your first query
아래 예시와 같이 API 를 호출하여 Delivery Tracker를 사용하실 수 있습니다.
Step 4.1. API Client 생성 및 Request 로직 작성
package com.demoproject
import com.apollographql.apollo3.ApolloClient
import com.apollographql.apollo3.network.http.DefaultHttpEngine
import com.demoproject.deliverytracker.api.DeliveryTrackerAuthorizationInterceptor
import com.demoproject.deliverytracker.api.GetTrackLastEventQuery
import com.demoproject.deliverytracker.api.findDeliveryTrackerError
suspend fun main() {
val apolloClient = ApolloClient.Builder()
.serverUrl("https://apis.tracker.delivery/graphql")
.httpEngine(DefaultHttpEngine(timeoutMillis = 20_000))
.addHttpInterceptor(
DeliveryTrackerAuthorizationInterceptor(
clientId = "[YOUR_CLIENT_ID]",
clientSecret = "[YOUR_CLIENT_SECRET]"
)
)
.build()
val response = apolloClient.query(
GetTrackLastEventQuery(
carrierId = "kr.cjlogistics",
trackingNumber = "1234567890",
)
).execute()
val notFoundError = response.findDeliveryTrackerError(code="NOT_FOUND")
if (notFoundError != null) {
println("NotFound : ${notFoundError.message}")
} else {
println(response.data?.track?.lastEvent?.status?.code)
}
}
Step 4.2. Authorization 처리 코드 작성
package com.demoproject.deliverytracker.api
import com.apollographql.apollo3.api.http.HttpRequest
import com.apollographql.apollo3.api.http.HttpResponse
import com.apollographql.apollo3.api.json.BufferedSourceJsonReader
import com.apollographql.apollo3.api.json.JsonReader
import com.apollographql.apollo3.network.http.HttpInterceptor
import com.apollographql.apollo3.network.http.HttpInterceptorChain
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import okhttp3.*
import okio.Buffer
import java.io.IOException
import java.util.concurrent.TimeUnit
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class DeliveryTrackerAuthorizationInterceptor(
clientId: String,
clientSecret: String,
private val okHttpClient: OkHttpClient = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.build(),
) : HttpInterceptor {
private val credentials = Credentials.basic(clientId, clientSecret)
private val requestBody = FormBody.Builder()
.add("grant_type", "client_credentials")
.build()
private val mutex = Mutex()
private var accessToken: String? = null
private suspend fun fetchNewAccessToken(): String? = suspendCoroutine { continuation ->
val call = okHttpClient.newCall(
Request.Builder()
.url("https://auth.tracker.delivery/oauth2/token")
.addHeader("Authorization", credentials)
.post(requestBody)
.build()
)
call.enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
continuation.resume(null)
}
override fun onResponse(call: Call, response: Response) {
val body = response.body
if (body == null) {
continuation.resume(null)
return
}
val jsonReader = BufferedSourceJsonReader(Buffer().write(body.bytes()))
jsonReader.beginObject()
while (jsonReader.hasNext()) {
if (jsonReader.nextName() != "access_token") {
jsonReader.skipValue()
continue
}
continuation.resume(jsonReader.nextString())
return
}
jsonReader.endObject()
continuation.resume(null)
}
})
}
/**
* errors[*].extensions.code == "UNAUTHENTICATED" 이 존재하는지 확인 합니다.
*
* 의존성 최소화를 위해 BufferedSourceJsonReader 를 사용하고 있습니다.
*/
private fun hasUnauthenticatedError(buffer: Buffer): Boolean {
val jsonReader = BufferedSourceJsonReader(buffer)
jsonReader.beginObject()
while (jsonReader.hasNext()) {
if (jsonReader.nextName() != "errors") {
jsonReader.skipValue()
continue
}
if (jsonReader.peek() == JsonReader.Token.NULL) {
jsonReader.nextNull()
return false
}
jsonReader.beginArray()
while (jsonReader.hasNext()) {
jsonReader.beginObject()
while (jsonReader.hasNext()) {
val name = jsonReader.nextName()
if (name != "extensions" && jsonReader.peek() != JsonReader.Token.BEGIN_OBJECT) {
jsonReader.skipValue()
continue
}
jsonReader.beginObject()
while (jsonReader.hasNext()) {
val extensionName = jsonReader.nextName()
if (extensionName != "code") {
jsonReader.skipValue()
continue
}
if (jsonReader.peek() != JsonReader.Token.STRING) {
jsonReader.skipValue()
continue
}
if (jsonReader.nextString() == "UNAUTHENTICATED") {
return true
}
}
jsonReader.endObject()
}
jsonReader.endObject()
}
jsonReader.endArray()
}
return false
}
private suspend fun getAccessToken(forceFetchNewAccessToken: Boolean = false) = mutex.withLock {
if (accessToken == null || forceFetchNewAccessToken) {
accessToken = fetchNewAccessToken()
}
accessToken
}
override suspend fun intercept(request: HttpRequest, chain: HttpInterceptorChain): HttpResponse {
var token = getAccessToken()
val response = if (token != null) {
chain.proceed(request.newBuilder().addHeader("Authorization", "Bearer $token").build())
} else {
chain.proceed(request)
}
val body = response.body?.readByteArray() ?: return response
if (hasUnauthenticatedError(Buffer().write(body))) {
token = getAccessToken(forceFetchNewAccessToken = true)
if (token != null) {
return chain.proceed(request.newBuilder().addHeader("Authorization", "Bearer $token").build())
}
}
return HttpResponse.Builder(
statusCode=response.statusCode,
)
.headers(response.headers)
.body(Buffer().write(body))
.build()
}
}
Step 4.2. Error 처리 유틸 작성 (Optional)
package com.demoproject.deliverytracker.api
import com.apollographql.apollo3.api.ApolloResponse
import com.apollographql.apollo3.api.Error
import com.apollographql.apollo3.api.Operation
fun <D : Operation.Data> ApolloResponse<D>.findDeliveryTrackerError(code: String): Error? {
return this.errors?.find {
it.extensions?.get("code") == code
}
}