07
11

https://github.com/kimmandoo/android-drill/tree/main/TmapWithNaverMap

 

android-drill/TmapWithNaverMap at main · kimmandoo/android-drill

기술 연습용 repo. Contribute to kimmandoo/android-drill development by creating an account on GitHub.

github.com

 

필요한 기능 학습할 때 API를 써야되는 경우가 있다. 이때 retrofit을 사용해도 되지만 내 귀찮음이 Ktor로 이끌었다. 물론 제대로 하려면 Ktor도 모듈 분리하고 다 해야될 것이지만 간단하게 테스트 용도로 사용할 것이므로 큰 문제 없다고 생각한다.

 

이번에 해볼 건 TMAP 대중교통 API 불러오기다.

https://openapi.sk.com/products/detail?svcSeq=59&menuSeq=492

 

SK open API

통계성 열차 혼잡도 진입 역 기준 열차 혼잡도 진입 역 기준 칸 혼잡도 진입 역 기준 칸별 하차 비율

openapi.sk.com

# Ktor

Ktor는 이름에서도 알수 있듯이 코틀린으로만 작성된 프레임워크다. 그리고 retrofit에 비해 좀 더 가볍다고 하는데 아직 제대로 안써봐서 잘 모르겠다.

 

gradle dependency는 아래와 같이 추가해주면 된다.

[versions]
ktorClientCore = "2.3.12"

[libraries]
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktorClientCore" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktorClientCore" }
// 여기까지는 toml에 작성
// app 수준 build.gradle.kts에 이걸 추가하면된다.
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.cio)

이러면 Ktor를 사용할 준비가 끝이 났다. 근데 안드로이드 특화 엔진을 사용하기 위해서 하나를 더 추가해보자. 

ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktorClientCore" }

implementation(libs.ktor.client.android)

이제 가장 간단한 코드를 작성해보겠다. 예시는 그냥 ktor 홈페이지로 해봤다.

private fun ktorBasic(){
    val client = HttpClient(Android)
    lifecycleScope.launch {
        val response: String = client.get("https://ktor.io/").bodyAsText()
        Log.d(TAG, "ktorBasic: $response")
    }
}

 

비동기 웹 프레임워크라서 코루틴안에 넣어줘야된다.

이제 진짜 API를 호출해보자.

 

Json으로 내려올 거니까 이걸 처리해줄 dependency도 추가해준다. 직렬화에 사용되는 `kotlinx-serialization-json`도 같이 넣어줘야된다. ktor가 내부적으로 사용하기 때문이다. 그리고 자동 직렬화를 도와주는 ContentNegotiation도 추가해주겠다. 레트로핏으로 생각하면 일종의 converter factory라고 생각하면 될 것 같다.

 

결과적으로 아래와 같이 됐다.

implementation(libs.ktor.client.core)
implementation(libs.ktor.client.cio)
implementation(libs.ktor.client.android)
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.kotlinx.serialization.json)

이제 진짜 Tmap API를 불러보자. 근데 아직 테스트 단계라서 데이터클래스 만들어서 자동 파싱까지 하는 건 건너뛰겠다.

api키 숨기는 건 다른 블로그를 찾아보거나 아래 링크를 읽어보면된다. 

API 숨기기 1

API 숨기기 2 (최신 수정사항)

 

클라이언트 먼저 만들면 아래와 같다.

val client = HttpClient(Android) {
    install(ContentNegotiation) {
        json()
        headers {
            append("Accept", "application/json")
            append("Content-Type", "application/json")
        }
    }
}

헤더를 붙여준다. 이때 client는 범용이라  appKey는 api 호출 부에서 넣어준다. BuildConfig가 안나온다면 프로젝트를 rebuild해보자. content-type은 기본헤더로 설정해도 될 것 같은데 아직 방법을 몰라서 넘어갔다.

바디를 이렇게 넘겨줘야한다. 바디 정도는 나중에도 쓸거니까 데이터클래스를 만들어줬다.

@Serializable
data class TmapRouteRequest(
    val count: Int = 1,
    val endX: String,
    val endY: String,
    val format: String = "json",
    val lang: Int = 0,
    val startX: String,
    val startY: String
)

이제 client에 post요청을 하면서 setBody에 이 객체를 넣어주면 끝난다. 아까 직렬화, 컨버터 다 넣어놨기 때문에 우리는 그냥 넣어주기만 하면 된다. 

private fun requestTmapAPI() {
    val client = HttpClient(Android) {
        install(ContentNegotiation) {
            json()
        }
    }
    lifecycleScope.launch {
        val response = client.post("https://apis.openapi.sk.com/transit/routes") {
            headers {
                append("Content-Type", "application/json")
                append("appKey", BuildConfig.TMAP)
            }
            setBody(
                TmapRouteRequest(
                    endX = "127.030406594109",
                    endY = "37.609094989686",
                    startX = "127.02550910860451",
                    startY = "37.63788539420793"
                )
            )
        }

        Log.d(TAG, "ktorPostAPI: ${response.body<TmapRouteResponse>()}")
    }
}

위에서는 Json 만들기 귀찮다고 안만들었었는데 그냥 플러그인 써서 만들었다... 예시로 주어진 API 응답에서 빠진 게 좀 있어 직접 api를 호출해서 파라미터를 생성했다.

잘 나오는 걸 볼 수 있다. 이제 이 정보를 갖고 네이버 지도 위에 오버레이를 그리면 된다...

 

혹시 response data class 가 필요한 사람도 있을 테니 추가로 올려두겠다. 한 곳만 호출해본거라서 빠진 파라미터가 있을 수 있을지도 모르겠다.

package com.kimmandoo.tmapwithnavermap.model


import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class TmapRouteResponse(
    @SerialName("metaData")
    val metaData: MetaData
) {
    @Serializable
    data class MetaData(
        @SerialName("plan")
        val plan: Plan,
        @SerialName("requestParameters")
        val requestParameters: RequestParameters
    ) {
        @Serializable
        data class Plan(
            @SerialName("itineraries")
            val itineraries: List<Itinerary>
        ) {
            @Serializable
            data class Itinerary(
                @SerialName("fare")
                val fare: Fare,
                @SerialName("legs")
                val legs: List<Leg>,
                @SerialName("pathType")
                val pathType: Int,
                @SerialName("totalDistance")
                val totalDistance: Int,
                @SerialName("totalTime")
                val totalTime: Int,
                @SerialName("totalWalkDistance")
                val totalWalkDistance: Int,
                @SerialName("totalWalkTime")
                val totalWalkTime: Int,
                @SerialName("transferCount")
                val transferCount: Int
            ) {
                @Serializable
                data class Fare(
                    @SerialName("regular")
                    val regular: Regular
                ) {
                    @Serializable
                    data class Regular(
                        @SerialName("currency")
                        val currency: Currency,
                        @SerialName("totalFare")
                        val totalFare: Int
                    ) {
                        @Serializable
                        data class Currency(
                            @SerialName("currency")
                            val currency: String,
                            @SerialName("currencyCode")
                            val currencyCode: String,
                            @SerialName("symbol")
                            val symbol: String
                        )
                    }
                }

                @Serializable
                data class Leg(
                    @SerialName("distance")
                    val distance: Int,
                    @SerialName("end")
                    val end: End,
                    @SerialName("Lane")
                    val lane: List<Lane>? = null,
                    @SerialName("mode")
                    val mode: String,
                    @SerialName("passShape")
                    val passShape: PassShape? = null,
                    @SerialName("passStopList")
                    val passStopList: PassStopList? = null,
                    @SerialName("route")
                    val route: String? = null,
                    @SerialName("routeColor")
                    val routeColor: String? = null,
                    @SerialName("routeId")
                    val routeId: String? = null,
                    @SerialName("sectionTime")
                    val sectionTime: Int,
                    @SerialName("service")
                    val service: Int? = null,
                    @SerialName("start")
                    val start: Start,
                    @SerialName("steps")
                    val steps: List<Step>? = null,
                    @SerialName("type")
                    val type: Int? = null,
                ) {
                    @Serializable
                    data class End(
                        @SerialName("lat")
                        val lat: Double,
                        @SerialName("lon")
                        val lon: Double,
                        @SerialName("name")
                        val name: String
                    )

                    @Serializable
                    data class Lane(
                        @SerialName("route")
                        val route: String,
                        @SerialName("routeColor")
                        val routeColor: String,
                        @SerialName("routeId")
                        val routeId: String,
                        @SerialName("service")
                        val service: Int,
                        @SerialName("type")
                        val type: Int
                    )

                    @Serializable
                    data class PassShape(
                        @SerialName("linestring")
                        val linestring: String
                    )

                    @Serializable
                    data class PassStopList(
                        @SerialName("stationList")
                        val stationList: List<Station>
                    ) {
                        @Serializable
                        data class Station(
                            @SerialName("index")
                            val index: Int,
                            @SerialName("lat")
                            val lat: String,
                            @SerialName("lon")
                            val lon: String,
                            @SerialName("stationID")
                            val stationID: String,
                            @SerialName("stationName")
                            val stationName: String
                        )
                    }

                    @Serializable
                    data class Start(
                        @SerialName("lat")
                        val lat: Double,
                        @SerialName("lon")
                        val lon: Double,
                        @SerialName("name")
                        val name: String
                    )

                    @Serializable
                    data class Step(
                        @SerialName("description")
                        val description: String,
                        @SerialName("distance")
                        val distance: Int,
                        @SerialName("linestring")
                        val linestring: String,
                        @SerialName("streetName")
                        val streetName: String
                    )
                }
            }
        }

        @Serializable
        data class RequestParameters(
            @SerialName("airplaneCount")
            val airplaneCount: Int,
            @SerialName("busCount")
            val busCount: Int,
            @SerialName("endX")
            val endX: String,
            @SerialName("endY")
            val endY: String,
            @SerialName("expressbusCount")
            val expressbusCount: Int,
            @SerialName("ferryCount")
            val ferryCount: Int,
            @SerialName("locale")
            val locale: String,
            @SerialName("reqDttm")
            val reqDttm: String,
            @SerialName("startX")
            val startX: String,
            @SerialName("startY")
            val startY: String,
            @SerialName("subwayBusCount")
            val subwayBusCount: Int,
            @SerialName("subwayCount")
            val subwayCount: Int,
            @SerialName("trainCount")
            val trainCount: Int,
            @SerialName("wideareaRouteCount")
            val wideareaRouteCount: Int
        )
    }
}

 

도움이 됐다면 댓글이나 공감 버튼 한 번씩 누르고 가주세요!

 

반응형
COMMENT