Kotlin Ktor에 Logger 연결하기

|

Ktor에 Logger 연결하기

Ktor에 Logger를 연결하지 않으면, 실행할 때 마다 다음 경고 메시지를 볼 수 있습니다.

SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.

경고 메시지라 실행하는데 큰 문제는 없지만, 걸리적거리기도 하고 원할한 디버깅을 위해 Logger를 설정하는 것을 추천합니다.

기본적으로 제공하는 Logback provider를 사용해보도록 하겠습니다.


build.gradle

먼저 build.gradle에 다음 항목을 추가합니다.

implementation "ch.qos.logback:logback-classic:1.2.3"


logback.xml

<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="trace">
        <appender-ref ref="STDOUT"/>
    </root>

    <logger name="org.eclipse.jetty" level="INFO"/>
    <logger name="io.netty" level="INFO"/>
</configuration>


그 이후 프로그램을 종료했다가 재실행을 하면 다음과 같은 메시지가 출력될 것입니다.

2020-01-06 18:47:51.160 [main] TRACE Application - {
    # application.conf @ file:/Users/snowdeer/Workspace/HelloKtor/build/resources/main/application.conf: 6
    "application" : {
        # application.conf @ file:/Users/snowdeer/Workspace/HelloKtor/build/resources/main/application.conf: 7
        "modules" : [
            # application.conf @ file:/Users/snowdeer/Workspace/HelloKtor/build/resources/main/application.conf: 7
            "com.snowdeer.BlogAppKt.main"
        ]
    },
    # application.conf @ file:/Users/snowdeer/Workspace/HelloKtor/build/resources/main/application.conf: 2
    "deployment" : {
        # application.conf @ file:/Users/snowdeer/Workspace/HelloKtor/build/resources/main/application.conf: 3
        "port" : 8080
    },
    # Content hidden
    "security" : "***"
}

2020-01-06 18:47:51.279 [main] INFO  Application - No ktor.deployment.watch patterns specified, automatic reload is not active
2020-01-06 18:47:51.598 [main] INFO  Application - Responding at http://0.0.0.0:8080

이 상태에서 GET, POST 등의 메소드를 이용해서 서버에 접속해보면 다양한 로그 메시지들이 출력되는 것을 확인할 수 있습니다.

Kotlin Ktor를 활용한 HTTP API 서버 만들기

|

Ktor를 활용한 HTTP API 서버 구현

간단한 라우팅 구현

fun Application.main() {

    routing {
        static("/static") {
            resources("static")
        }

        routing {
            get("/apis") {
                call.respondText("OK")
            }
        }
    }
}

위 코드로 http://localhost:8080/apis 경로로 접속하면 OK라는 텍스트를 리턴하는 웹서버를 만들 수 있습니다.


Jakson 라이브러리 추가

HTTP API들은 보통 JSON 형태의 데이터를 많이 사용합니다. jakson 라이브러리를 사용하면 보다 쉽게 JSON 데이터를 전송할 수 있습니다.

build.gradle에 다음 종속성을 추가합니다.

implementation "io.ktor:ktor-jackson:$ktor_version"

그리고 위의 Application.main() 코드를 다음과 같이 변경합니다.

package com.snowdeer

import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.features.ContentNegotiation
import io.ktor.http.content.resources
import io.ktor.http.content.static
import io.ktor.jackson.jackson
import io.ktor.response.respond
import io.ktor.routing.get
import io.ktor.routing.routing

fun Application.main() {

    install(ContentNegotiation) {
        jackson {
        }
    }

    routing {
        static("/static") {
            resources("static")
        }

        routing {
            get("/apis") {
                call.respond(mapOf("id" to "snowdeer"))
            }
        }
    }
}

만약 복수의 데이터를 Map 형태로 리턴하고 싶으면 다음과 같이 작성할 수 있습니다.

package com.snowdeer

import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.features.ContentNegotiation
import io.ktor.http.content.resources
import io.ktor.http.content.static
import io.ktor.jackson.jackson
import io.ktor.response.respond
import io.ktor.routing.get
import io.ktor.routing.routing
import java.util.*

fun Application.main() {

    install(ContentNegotiation) {
        jackson {
        }
    }

    routing {
        static("/static") {
            resources("static")
        }

        routing {
            get("/apis") {

                val map = HashMap<String, Any>()
                map["id"] = "snowdeer"
                map["age"] = 40
                map["email"] = "snowdeer0314@gmail.com"
                map["boolean"] = true
                map["float"] = 3.14F
                map["long"] = 30L

                call.respond(mapOf("data" to map))
            }
        }
    }
}

이 때 http://localhost:8080/apis로 리퀘스트를 날리면 응답은 다음과 같습니다.

{
    "data": {
        "boolean": true,
        "id": "snowdeer",
        "float": 3.14,
        "age": 40,
        "email": "snowdeer0314@gmail.com",
        "long": 30
    }
}


HTTP POST를 이용한 데이터 수신

POST를 이용해서 JSON 데이터를 수신받을 때는 call.receive<Type>() 메소드를 이용해서 전달 받을 수 있습니다.

package com.snowdeer

import com.fasterxml.jackson.databind.SerializationFeature
import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.features.ContentNegotiation
import io.ktor.http.content.resources
import io.ktor.http.content.static
import io.ktor.jackson.jackson
import io.ktor.request.receive
import io.ktor.response.respond
import io.ktor.routing.get
import io.ktor.routing.post
import io.ktor.routing.routing
import java.util.*
import kotlin.collections.ArrayList

data class PostItem(val id: String, val email: String)

fun Application.main() {

    val list = ArrayList<PostItem>()

    install(ContentNegotiation) {
        jackson {
            enable(SerializationFeature.INDENT_OUTPUT)
        }
    }

    routing {
        static("/static") {
            resources("static")
        }

        routing {
            get("/apis") {

                val map = HashMap<String, Any>()
                map["id"] = "snowdeer"
                map["age"] = 40
                map["email"] = "snowdeer0314@gmail.com"
                map["boolean"] = true
                map["float"] = 3.14F
                map["long"] = 30L

                call.respond(mapOf("data" to map))
            }

            post("/apis/add") {
                val data = call.receive<PostItem>()
                println("[snowdeer] receive: ${data}")

                list.add(data)

                call.respond(mapOf("result" to true))
            }

            get("/apis/list") {
                call.respond(mapOf("data" to list))
            }
        }
    }
}

POST로 데이터를 전송할 때 주의할 점은, Request의 Key/Value로 값을 전달하는게 아니라 HTTP Body에 데이터를 문자열 형태로 전송해야 한다는 점입니다.

예를 들면 다음과 같은 형태로 HTTP Request를 전송해야 합니다.

POST http://127.0.0.1:8080/apis/add
Content-Type: application/json

{
	"id": "snowdeer",
	"email" : "hello"
}

만약 JSON 포맷이 다르거나 잘못된 내용을 전송할 경우 500Internal Server Error를 리턴합니다.

Kotlin Ktor의 DSL을 이용한 웹페이지 생성

|

DSL을 이용한 웹페이지 만들기

FreeMaker 모듈 대신 HTML DSL(Domain specific language)를 이용해서 웹페이지를 응답하는 코드 예제입니다.

보다 자세한 내용은 여기에서 볼 수 있습니다.

build.gradle

Ktor용 App을 위한 기본 build.gradle의 종속성에 FreeMaker 종속성 대신에 implementation "io.ktor:ktor-html-builder:$ktor_version" 항목이 추가되었습니다.

repositories 항목에 jcenter()를 추가해야 됩니다.

group 'com.snowdeer'
version '1.0-SNAPSHOT'

buildscript {
    ext.kotlin_version = '1.3.61'
    ext.ktor_version = '1.2.6'

    repositories {
        mavenCentral()
    }
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

apply plugin: 'java'
apply plugin: 'kotlin'
apply plugin: 'application'

sourceCompatibility = 1.8
application {
    mainClassName = "com.snowdeer.BlogAppKt"
}

repositories {
    jcenter()
    mavenCentral()
}

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"

    implementation "io.ktor:ktor-html-builder:$ktor_version"
    compile "io.ktor:ktor-server-netty:$ktor_version"

    testCompile group: 'junit', name: 'junit', version: '4.12'
}

compileKotlin {
    kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
    kotlinOptions.jvmTarget = "1.8"
}

kotlin {
    experimental {
        coroutines "enable"
    }
}


BlogApp.kt

package com.snowdeer

import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.html.respondHtml
import io.ktor.http.content.resources
import io.ktor.http.content.static
import io.ktor.routing.get
import io.ktor.routing.routing
import kotlinx.html.*

data class IndexData(val items: List<Int>)

fun Application.main() {
    routing {
        static("/static") {
            resources("static")
        }

        get("/") {
            val data = IndexData(listOf(1, 2, 3))
            call.respondHtml {
                head {
                    link(rel = "stylesheet", href = "/static/styles.css")
                }
                body {
                    ul {
                        for (item in data.items) {
                            li { +"$item" }
                        }
                    }
                }
            }
        }
    }
}

Kotlin Ktor 활용한 간단한 Login 페이지 만들기

|

Login 기능 구현하기

login.ftl

resources/templates/login.ftl 파일을 생성합니다.

<html>
<head>
    <link rel="stylesheet" href="/static/styles.css">
</head>
<body>
<#if error??>
    <p style="color:red;">${error}</p>
</#if>
<form action="/login" method="post" enctype="application/x-www-form-urlencoded">
    <div>User:</div>
    <div><input type="text" name="username" /></div>
    <div>Password:</div>
    <div><input type="password" name="password" /></div>
    <div><input type="submit" value="Login" /></div>
</form>
</body>
</html>


Main.kt

package com.snowdeer

import freemarker.cache.ClassTemplateLoader
import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.freemarker.FreeMarker
import io.ktor.freemarker.FreeMarkerContent
import io.ktor.http.content.resources
import io.ktor.http.content.static
import io.ktor.request.receiveParameters
import io.ktor.response.respond
import io.ktor.response.respondText
import io.ktor.routing.get
import io.ktor.routing.post
import io.ktor.routing.route
import io.ktor.routing.routing

fun Application.main() {
    install(FreeMarker) {
        templateLoader = ClassTemplateLoader(this::class.java.classLoader, "templates")
    }

    routing {
        static("/static") {
            resources("static")
        }

        route("/login") {
            get {
                call.respond(FreeMarkerContent("login.ftl", null))
            }
            post {
                val post = call.receiveParameters()

                val username = post["username"]
                println("[snowdeer] username: $username")
                if (username != null && post["password"] != null) {
                    call.respondText("OK")
                } else {
                    call.respond(FreeMarkerContent("login.ftl", mapOf("error" to "Invalid login")))
                }
            }
        }
    }

}

만약 리다이렉션(Redirection)을 하고 싶으면 다음과 같은 코드를 사용하면 됩니다.

call.respondRedirect("/", permanent = false)

Kotlin Ktor 활용한 간단한 웹페이지(index.html)에 CSS 적용하기

|

CSS 파일 적용하기

앞서 만들었던 FreeMaker 템플릿 기반 샘플 예제에서, css 파일을 적용하려면 다음과 같이 수정합니다.

BlogApp.kt

먼저 routing 정보에 static 정보를 포함하도록 선언합니다.

package com.snowdeer

import freemarker.cache.ClassTemplateLoader
import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.freemarker.FreeMarker
import io.ktor.freemarker.FreeMarkerContent
import io.ktor.response.respond
import io.ktor.routing.get
import io.ktor.routing.routing

data class IndexData(val items: List<Int>)

fun Application.main() {
    install(FreeMarker) {
        templateLoader = ClassTemplateLoader(this::class.java.classLoader, "templates")
    }

    routing {
        static("/static") {
            resources("static")
        }

        get("/html-freemarker") {
            call.respond(FreeMarkerContent("index.ftl", mapOf("data" to IndexData(listOf(1, 2, 3))), ""))
        }
    }
}


css 파일 추가

resources/static/styles.css 파일을 추가합니다.

body {
    background: #B9D8FF;
}


index.ftl 수정

resources/templates/index.ftlcss 파일을 불러오는 코드를 추가합니다.

<html>
    <head>
        <link rel="stylesheet" href="/static/styles.css">
    </head>

	<body>
		<ul>
		<#list data.items as item>
			<li>${item}</li>
		</#list>
		</ul>
	</body>
</html>



그 외

위에서 사용했던 static 코드는 텍스트 파일만 추가하는 것이 아니라 이미지 파일이나 기타 다른 파일들도 불러올 수 있습니다. <img src="..."> 태그를 이용해서 자유롭게 이미지를 삽입할 수 있습니다.