Ktor를 사용하는 이유
지금까지 kotlin으로 안드로이드 앱 개발이나 python으로 웹 스크래핑 관련 프로그램을 개발을 해왔는데 직접 내 손으로 서버나 인프라를 구축해본 경험이 없었다.
주변을 보면 Nest.js, Springboot 프레임워크를 주로 사용을 하고 kotlin에 익숙한 필자는 kotlin + Springboot 조합으로 백엔드 개발을 하려했지만 러닝커브가 있었다.
Springboot와 Ktor를 모두 공부중이지만 개인적으로는 필자처럼 안드로이드 개발만 하고 서버 구축 경험이 없는 사람이라면 Ktor를 추천한다.
사람들이 가장 많이 사용하니까 "그냥" 해당 기술을 채택하는 것은 안된다는 것을 최근 들어 깨달았기에 거대한 규모의 서버가 아닌 보다 경량화된 서버로 구축하기에는 Ktor가 Springboot보다 더 적합하다고 판단했다.
또한, KMP 혹은 CMP에 관심이 있다면 Jetbrains에서 개발한 서버 프레임워크를 공부해서 옛날처럼 서버 사이드 렌더링 시대처럼 통합 환경으로 개발해보는 것도 나쁘지 않은 것 같다.
원래는 개발자 1명이 전체 기술스택을 다루고 프론트, 백 경계가 모호했었다가 2000년대 이후 "사용자 경험"을 중요시하고 Javascript 생태계가 폭발적으로 성장하면서 다양한 기술스택으로 서비스를 개발하는 것이 일반적으로 여겨졌다.
최근 들어서 Jetbrains의 행보를 보면 다시 옛날처럼 회귀하기보다는 Kotlin이라는 언어를 중심으로 자신들만의 왕국을 건설하려는 야망이 보여서 굉장히 흥미롭다.
(개발 과정을 전부 작성할 것이기 때문에 중간에 글을 지속적으로 수정할 예정)

https://start.ktor.io/settings
Ktor Generator로 손쉽게 프로젝트를 생성할 수 있지만 IntelliJ IDEA 2025 버전 기준으로 IDE 내부 프로젝트 생성기로도 플러그인을 취사 선택할 수 있다.

Ktor 서버 어플리케이션을 동작시키기 위해서 방식이 2가지가 있는데 Netty에서 제공해주는 EngineMain을 사용하거나 EmbeddedServer를 사용한다.
첫 프로젝트를 생성할 때 Add sample code를 하게 되면 EmbeddedServer를 기본 코드로 제공을 해주는데 필자는 EngineMain을 사용하기로 했다.
왜냐하면 Ktor의 기능 중 하나인 auto-reload가 잘 작동하지 않는다.
아직 원인 파악을 하지 못했다...
클라이언트 개발할 때 Hot-reload라고 코드 변경사항이 발생할 경우 즉각 자동 반영해주는 기능인데 로컬 서버에도 적용이 된다고 해서 신기했다.
터미널에 다음 명령어를 입력할 경우 동작한다.
./gradlew -t build
gradle.properties에 JDK 경로도 명시해야한다.
kotlin.code.style=official
kotlin_version=2.1.10
ktor_version=3.1.3
logback_version=1.4.14
org.gradle.java.home=C:\\Program Files\\Java\\jdk-21

EngineMain 방식
application.conf () -> HOCON
ktor {
development = true
deployment {
port = 8080
port = ${?PORT}
watch = [ classes, resources ]
}
application {
modules = [ com.example.ApplicationKt.module ]
}
}
EmbeddedServer 방식
application.yaml
ktor:
application:
modules:
- com.example.ApplicationKt.module
deployment:
port: 8080
watch: classes, resources
development: true
build.gradle
val kotlin_version: String by project
val logback_version: String by project
plugins {
kotlin("jvm") version "2.1.10"
id("io.ktor.plugin") version "3.1.3"
id("org.jetbrains.kotlin.plugin.serialization") version "2.1.10"
}
group = "com.example"
version = "0.0.1"
application {
mainClass = "com.example.Application.kt"
}
repositories {
mavenCentral()
}
dependencies {
implementation(project(":test-module"))
implementation(kotlin("stdlib"))
implementation("io.ktor:ktor-server-core")
implementation("io.ktor:ktor-server-host-common")
implementation("io.ktor:ktor-server-content-negotiation")
implementation("io.ktor:ktor-server-call-logging")
implementation("io.ktor:ktor-serialization-kotlinx-json")
implementation("io.ktor:ktor-server-netty")
implementation("ch.qos.logback:logback-classic:$logback_version")
implementation("io.ktor:ktor-server-config-yaml")
testImplementation("io.ktor:ktor-server-test-host")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
}
프로젝트 생성시 기본 패키지 구조

EmbeddedServer, EngineMain
fun main(args: Array<String>) {
embeddedServer(Netty, port = 8080, watchPaths = listOf("classes", "resources")) {
module()
}.start(wait = true)
// io.ktor.server.netty.EngineMain.main(args) // 서버 엔진 셋업
}
fun Application.module() {
configureSerialization()
configureMonitoring()
configureRouting()
}
다음은 Netty Engine을 8080포트로 띄운다.
fun Application.configureRouting() {
/* Ktor의 라우팅 DSL을 시작하는 블록
* HTTP 요청을 처리할 경로들을 정의
* */
routing {
get("/") {
call.respondText("Hello World!")
}
}
의존성 주입할 때 KOIN을 사용하는 점이 재밌었는데 다음 편에서 계속하겠다.