게임으로 넘어가기전에 정리할건 정리하고 가야지. Ktor Client 사용법 공부하다가 요로결석 걸려서 1주일 넘게 무력화 되어 있었다. 암튼… 앞에서 다뤘던 SQLDelight, Koin과 함께 Ktor까지 사실 전부 Kotlin Multi-Platform을 위한 것들이기도 하다. 안드로이드에서 Room, Hilt, Retrofit이 각각 DB, DI, Network를 위한 라이브러리로 주로 사용되는데, 자바기반이라서 KMP에서는 1:1로 대체 가능한 순수 Kotlin 기반 라이브러리들을 사용 하는 것이다. 하지만, KMP가 아니더라도 충분히 사용할만 하기 때문에, 좋은 선택지라고 생각한다. 앞에서 SQLDelight, Koin을 다뤘으니 마지막으로 Ktor만 정리하면 기본은 어쨌든 하는거.
먼저, IntelliJ Idea에서 코틀린을 이용한 방법을 알아보자. build.gradle.kts에 다음과 같이 ktor를 추가한다.
val ktorVersion = "3.0.2"
...
dependencies {
...
implementation("io.ktor:ktor-client-core:$ktorVersion")
implementation("io.ktor:ktor-client-cio:$ktorVersion")
ktor-client-core 는 기본적인 ktor core로 반드시 포함되어야 하고, ktor-client-cio는 네트워크 리퀘스트를 처리하는 엔진으로 플랫폼에 따라 원하는 것을 사용할 수 있다. 각 엔진들이 지원하는 플랫폼과 특징은 Engines 에서 확인 가능하다.
기본적인 사용은 다음과 같다.
suspend fun main() {
val client = HttpClient(CIO)
val response: HttpResponse = client.get("https://ktor.io/")
println(response.status)
client.close()
}
일단, main()함수가 suspend로 선언되어 있다. 이는 KTor Client가 coroutine을 이용하고 있기 때문인데, 코루틴 사용을 위해 그렇게 해준다. 다음 첫 줄에 HttpClient 를 만들어주고 있다. 인자로 CIO를 명시했는데, 사용할 엔진을 인자로 넘겨주게 된다. 초기화에 대한 내용이 꽤 있는데 일단, 이렇게 사용이 가능하다는걸 알고 넘어가자. 다음에 client.get()을 이용하고 있는데, HTTP의 get을 이용해 인자의 URL로 HTTPRequest를 보내게 된다. 그 응답을 위에선 출력해주고 client를 close()하여 닫아주고 있다.
위 코드의 결과물은 정상이라면 200 OK가 떨어질 것이다.
위 코드를 보면 예상하겠지만, 안드로이드에서도 위 내용을 참고하여 추가해주면 된다. 따로 설명할까 하다가, 그냥 라이브러리 추가가 끝인데 굳이 불필요해 보인다.
Config KTor
Ktor는 앞에서 플랫폼에 맞는 엔진이 필요하다고 했다. KTor의 구성은 이 Engine에 더해서 Plugin 시스템으로 구성된다. 플러그인은 말 그대로, 추가 기능들을 라이브러리에 추가하고 HttpClient()를 정의할 때, install()을 이용해서 추가하고 설정 할 수 있다. 예를들어 Logging이나 JSON, XML 등을 파싱해서 Serialization 시켜주는 기능들을 이런식으로 추가가 가능하다.
Engines
먼저 Engine의 설정을 살펴보자. 엔진마다 설정방법이 좀 다르다. 공식 사이트를 참조하여 몇 개 예를들어보자. 다음은 안드로이드 엔진의 예이다.
implementation("io.ktor:ktor-client-android:$ktor_version")
먼저 위와같이 해당 엔진 라이브러리의 추가가 필요하다.
val client = HttpClient(Android) {
engine {
// this: AndroidEngineConfig
connectTimeout = 100_000
socketTimeout = 100_000
proxy = Proxy(Proxy.Type.HTTP, InetSocketAddress("localhost", 8080))
}
}
엔진도 그렇고 나중에 나오는 플러그인도 그렇고 HttpClient()의 함수 파라미터를 이용하여 설정한다. 위 안드로이드의 예를 보면, connect timeout과 socket timeout, proxy등을 설정하고 있다. 설정값들은 각 엔진에 대한 문서를 보면 알 수 있다.
Plugins
플러그인은 로깅에 대해 살펴보자. KTor의 로깅에 대해 먼저 말해보자면, JVM위에서 SLF4J(Simple Logging Facade for Java) 라는 추상 레이어를 사용한다고 한다. 이건 일종의 인터페이스로 작동해서 실제 구현과 API를 분리시켜준다. 그렇다는건 여기에 맞는 로깅 프레임워크라면 어떤 것이든 사용이 가능해진다는 얘기. 여기서는 Logback을 한 번 적용해보자.
먼저 라이브러리를 추가해준다.
implementation("ch.qos.logback:logback-classic:$logback_version")
만약 안드로이드라면, SLF4J 안드로이드 라이브러리를 사용하길 권장한다고 한다.
로깅 플러그인의 설치는 다음과 같다.
val client = HttpClient(CIO) {
install(Logging)
}
바로, HttpClient의 함수 파라미터에서 install()을 이용하면 된다. 로깅의 config 는 LoggingConfig 문서를 찾아보면 알 수 있다. 사용 예제를 살펴보자면 다음과 같다.
val client = HttpClient(CIO) {
install(Logging) {
logger = Logger.DEFAULT
level = LogLevel.HEADERS
filter { request ->
request.url.host.contains("ktor.io")
}
sanitizeHeader { header -> header == HttpHeaders.Authorization }
}
}
HttpClient와 유사하게 install()에서도 함수 파라미터를 이용하고 있다. logger는 SLF4J를 사용할지, Native를 사용할지등을 정한다. Logger.DEFAULT 는 SLF4J를 사용한다는 의미이다. level은 로깅할 level을 지정하는 것인데, LogLevel.HEADERS는 request/response header만 표시한다는 의미이다. filter는 로그자체의 필터링 설정인데, 위 예제는 request에서 “ktor.io”가 포함된 호스트에 대해서만 표시한다는 의미이다. 마지막으로, sanitizeHeader는 표시되면 안되는 내용을 로그에서 ‘***’로 표시하는 부분이다. 예제에선 Authorization 부분을 그렇게 표시했다.
Content negotiation and serialization ( REST API 사용예 )
Rest API를 사용하고 싶다면, Content negotiation 플러그인을 필요로 한다. 이 플러그인의 역할은 서버와 클라이언트간 주고받는 미디어 타입에 대해 처리해준다. 이 Content negotiation 플러그인 안에서 컨텐츠가 JSON이나 XML등과 같은 경우, 코틀린에서 사용하는 Serialization 플러그인등을 따로 추가해준다.
Rest API 예제로 내용을 확인해보자. 먼저 Content negotiaion과 Kotlin에서 제공하는 Serializer를 추가해보자.
implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
다음으로, Logging에서 했듯이 Content Negotiation플러그인을 HttpClient에 추가한다. 다음은 openweathermap을 사용하는 예제이다.
val client = HttpClient(Apache5) {
install(ContentNegotiation) {
json(Json {
isLenient = false
})
}
install(Logging)
defaultRequest {
url {
protocol = URLProtocol.HTTPS
host = "api.openweathermap.org/data/2.5"
}
}
}
defaultRequest는 차후 request를 간편하게 하기 위해 default값을 설정하는 부분이다. 이걸 이용해 request를 보내는 함수를 만들면 다음과 같다.
data class CurrentWeatherReqeust(
val latitude: Double,
val longitude: Double,
val mode: ResponseMode? = null,
val units: ResponseUnits? = null,
val language: String? = null
)
//----
suspend fun getCurrentWeather(client: HttpClient, currentWeatherRequest: CurrentWeatherReqeust) : CurrentWeather? {
return try {
val response: HttpResponse = client.get("/weather") {
url {
parameters.append("lat", currentWeatherRequest.latitude.toString())
parameters.append("lon", currentWeatherRequest.longitude.toString())
parameters.append("appid", APP_ID)
if(currentWeatherRequest.mode != null) parameters.append(
"mode",
currentWeatherRequest.mode.name
)
if(currentWeatherRequest.units != null) parameters.append(
"units",
currentWeatherRequest.units.name
)
if(currentWeatherRequest.language != null) parameters.append(
"lang",
currentWeatherRequest.language
)
}
}
if (response.status == HttpStatusCode.OK) {
response.body<CurrentWeather>()
} else {
println("Failed to retrieve current weather. Status: ${response.status}")
null
}
} catch (e: Exception) {
println("Error retrieving current weather: ${e.message}")
null
}
}
CurrentWeatherRequest는 request편의상 만든 클래스이다. 위와같이 default값에 값을 추가해서 get()을 이용해 request를 보낸다. 이렇게 보낸 response는 serialization을 이용해서 data class로 받아볼 수 있다. response를 받는 data class를 일부만 표시해보면 다음과 같다.
@OptIn(ExperimentalSerializationApi::class)
@Serializable
data class CurrentWeather(
@SerialName("coord") val coordinates: Coordinates,
@SerialName("weather") val weather: List<Weather>,
@SerialName("base") val base: String,
@SerialName("main") val main: Main,
@SerialName("visibility") val visibility: Int,
@SerialName("wind") val wind: Wind,
@SerialName("rain")
@EncodeDefault
val rain: Rain? = null,
@SerialName("snow")
@EncodeDefault
val snow: Snow? = null,
@SerialName("clouds") val clouds: Clouds,
...
여기서 중요한건 바로 @Serializable 과 @SerialName 어노테이션인데, 먼저 @Serializable 로 명시해야 Serialization으로 사용이 가능하며, @SerialName은 필드이름 대신 Serialization에서 사용할 다른 이름을 설정해준다.
이와같이 사용해서 REST API같은 요청을 할 수 있다.
이 외에도 http관련된 많은 작업이 있지만, 요정도만 다루겠다. 문서화가 꽤 잘되어 있으므로 공식 사이트인 Ktor.io 에 방문해서 필요한 정보들을 얻기 바란다.