News Feed

케이터 입문 : 서버 사이드 스택

컨텐츠 정보

  • 조회 758

본문

케이터(Ktor), 그리고 웹 애플리케이션 빌드를 위한 케이터의 기본 기능 몇 가지를 소개했는데, 이번에는 이전 기사에서 개발한 예제 애플리케이션을 확장해서 지속성 데이터와 HTMX를 추가하고 더 인터랙티브한 뷰를 만들어 보자. 비교적 간단한 스택으로 많은 기능을 얻을 수 있다.

케이터-HTMX 애플리케이션에 지속성 추가

예제 애플리케이션을 더 강력하게 만들기 위한 첫 번째 단계는 지속적인 데이터를 추가하는 것이다. 코틀린에서 SQL 데이터베이스와 상호작용하는 가장 일반적인 방법은 익스포즈드 ORM(Exposed ORM) 프레임워크다. 이 프레임워크는 데이터베이스와 상호작용하는 방법으로 DAO 매핑과 DSL, 두 가지를 제공한다. 코틀린 네이티브 구문은 ORM 매핑 계층을 사용하는 데 따르는 전반적인 오버헤드가 다른 방식보다 비교적 적다는 것을 의미한다.

이미 있는 것 외에, build.gradle.kt에 몇 가지 종속 항목을 더 추가해야 한다.

dependencies {  // existing deps...    implementation("org.jetbrains.exposed:exposed-core:0.41.1")  implementation("org.jetbrains.exposed:exposed-jdbc:0.41.1")   implementation("com.h2database:h2:2.2.224")}

노출된 코어와 JDBC 라이브러리, 인메모리 H2 데이터베이스를 위한 드라이버가 포함된 것을 볼 수 있다. 나중에 포스트그레스와 같은 외부 SQL 데이터베이스로 손쉽게 전환할 수 있는 간단한 지속성 메커니즘으로 H2를 사용한다.

서비스 추가

우선 데이터베이스와 통신하는 주요 서비스와 상호작용하는 두 가지 간단한 서비스를 만든다. 다음은 현재 상태의 QuoteSchema.kt 파일로, 데이터베이스 스키마를 설정하고 이와 상호작용하기 위한 서비스 기능을 제공한다.

// src/main/kotlin/com/example/plugins/QuoteSchema.ktpackage com.example.pluginsimport kotlinx.coroutines.*import org.jetbrains.exposed.sql.*import org.jetbrains.exposed.sql.transactions.transactionobject Quotes : Table() {    val id: Column = integer("id").autoIncrement()    val quote = text("quote")    val author = text("author")    override val primaryKey = PrimaryKey(id, name = "PK_Quotes_ID")}data class Quote(val id: Int? = null, val quote: String, val author: String)class QuoteService {    suspend fun create(quote: Quote): Int = withContext(Dispatchers.IO) {      transaction {        Quotes.insert {          it[this.quote] = quote.quote          it[this.author] = quote.author        } get Quotes.id      } ?: throw Exception("Unable to create quote")    }    suspend fun list(): List = withContext(Dispatchers.IO) {        transaction {            Quotes.selectAll().map {                Quote(                    id = it[Quotes.id],                    quote = it[Quotes.quote],                    author = it[Quotes.author]                )            }        }    }}

이 파일에서는 많은 작업이 실행된다. 단계별로 살펴보자. 가장 먼저 하는 일은 Table을 확장하는 Quotes 객체를 선언하는 것이다. Table은 익스포즈드 프레임워크의 일부이며, 데이터베이스에서 테이블을 정의할 수 있게 해준다. 이는 우리가 정의한 4개의 변수, 즉 id, quote, author, primary key를 기반으로 많은 작업을 수행한다. id요소는 자동 증분 primary key를 위해 자동으로 생성되고, 다른 두 요소는 각자 적절한 열 유형을 갖는다. (예를 들어 데이터베이스의 방언과 드라이버에 따라 textstring이 된다.)

익스포즈드는 테이블이 이미 존재하지 않는 경우에만 생성한다.

다음으로, 생성자 스타일을 사용해 Quote라는 데이터 클래스를 선언한다. id는 선택 사항으로 표시된다(자동으로 생성되기 때문).

그 다음 createlist라는 두 개의 일시 중단이 가능한 함수가 있는 QuoteService 클래스를 만든다. 둘 다 IO 디스패처를 사용해 코틀린의 동시성 지원과 상호작용한다. 이러한 메서드는 데이터베이스 액세스에 적합한 IO 바운드 동시성에 최적화된다.

각 서비스 메서드 내부에는 새 Quote를 삽입하거나 QuoteList를 반환하는 작업을 수행하는 데이터베이스 트랜잭션이 있다.

경로

이제 QuoteService를 가져오고 이와 상호작용하기 위한 엔드포인트를 노출하는 Database.kt 파일을 만든다. 인용문을 생성하기 위한 POST와 이를 나열하기 위한 GET이 필요하다.

//src/main/kotlin/com/example/plugins/Database.kt package com.example.pluginsimport io.ktor.http.*import io.ktor.server.application.*import io.ktor.server.request.*import io.ktor.server.response.*import io.ktor.server.routing.*import java.sql.*import kotlinx.coroutines.*import org.jetbrains.exposed.sql.*import org.jetbrains.exposed.sql.transactions.transactionfun Application.configureDatabases() {    val database = Database.connect(        url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1",        user = "root",        driver = "org.h2.Driver",        password = "",    )    transaction {        SchemaUtils.create(Quotes)    }    val quoteService = QuoteService()     routing {        post("/quotes") {          val parameters = call.receiveParameters()          val quote = parameters["quote"] ?: ""          val author = parameters["author"] ?: ""           val newQuote = Quote(quote = quote, author = author)            val id = quoteService.create(newQuote)          call.respond(HttpStatusCode.Created, id)        }        get("/quotes") {            val quotes = quoteService.list()            call.respond(HttpStatusCode.OK, quotes)        }    }}

먼저 익스포즈드 프레임워크의 Database.connect를 사용해서 표준 H2 매개변수로 데이터베이스 연결을 생성한다. 그런 다음 트랜잭션 내에서 QuoteSchema.kt에서 정의한 Quote 클래스를 사용해 Quotes 스키마를 만든다.

그 다음 예제의 첫 번째 단계에서 개발한 구문을 사용하고 createlist 함수, 그리고 QuoteSchemaQuote 클래스를 이용해 두 개의 경로를 만든다.

Application.kt에 새 함수를 포함하는 것을 잊으면 안 된다.

// src/main/kotlin/com/example/Application.kt package com.exampleimport com.example.plugins.*import io.ktor.server.application.*import io.ktor.server.response.*import io.ktor.server.routing.*fun main(args: Array) {    io.ktor.server.netty.EngineMain.main(args)}fun Application.module() {  configureTemplating()  //configureRouting()  install(RequestLoggingPlugin)  configureDatabases()}

새 경로와 충돌하지 않도록 이전의 configureRouting() 호출을 주석 처리했다.

이러한 경로를 신속하게 테스트하려면 curl 명령줄 툴을 사용하면 된다. 다음 코드는 행 하나를 삽입한다.

$ curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -H "Host: localhost:8080" -d "quote=FooBar.&author=William+Shakespeare" http://localhost:8080/quotes

다음 코드는 기존 행을 출력한다.

$ curl http://localhost:8080/quotes

인터랙티브 뷰를 위한 HTMX 사용

이제 바로 HTMX를 사용해 서비스와 상호작용하는 UI 만들어 보자. 우리가 원하는 것은 기존 인용문을 나열하는 페이지와 새 인용문을 제출하는 데 사용할 수 있는 양식이다. 페이지를 다시 로드하지 않아도 페이지의 목록에 동적으로 인용문이 삽입된다.

이러한 목표를 달성하려면 처음에 모든 것을 그리는 경로, 이후 양식 POST를 수락하고 새로 삽입된 인용문을 위한 마크업을 반환하는 또 다른 경로가 필요하다. 여기서는 단순하게 만들기 위해 이를 Database.kt 경로에 추가한다.

다음은 초기 목록과 양식을 제공하는 /quotes-htmx 페이지다.

get("/quotes-htmx") {        val quotes = quoteService.list()            call.respondHtml {          head {            script(src = "https://unpkg.com/htmx.org@1.9.6") {}           }        body {          h1 { +"Quotes (HTMX)" }          div {            id = "quotes-list"            quotes.forEach { quote ->              div {                p { +quote.quote }                p { +"― ${quote.author}" }              }            }          }          form(method = FormMethod.post, action = "/quotes", encType = FormEncType.applicationXWwwFormUrlEncoded) {            attributes["hx-post"] = "/quotes"            attributes["hx-target"] = "#quotes-list"            attributes["hx-swap"] = "beforeend"             div {              label { +"Quote:" }              textInput(name = "quote")            }            div {              label { +"Author:" }              textInput(name = "author")            }            button(type = ButtonType.submit) { +"Add Quote" }          }        }      }    }

먼저 서비스에서 인용문 목록을 가져온다. 그런 다음 CDN의 HTMX 라이브러리가 포함된 head 요소부터 HTML을 출력하기 시작한다. 그 다음 body 태그를 열고 title(H1), 이후 quotes-list id와 함께 div를 렌더링한다. 여기서 id div의 속성이 아니라 div 블록 내부의 호출로 처리되는 것을 볼 수 있다.

quotes-list 안에서 인용문 컬렉션을 반복 처리하고 각 인용문과 저자가 포함된 div를 출력한다. (이 애플리케이션의 익스프레스(Express) 버전에서는 UL을 사용하고 항목을 나열했다. 여기서도 똑같이 해도 된다.)

목록 다음에는 attributes 컬렉션에 여러 비표준 속성(hx-post, hx-target, hx-swap)을 설정하는 양식이 있다. 이는 출력 HTML 양식 요소에 설정된다.

이제 POST에서 오는 인용문을 수락하고 목록에 삽입될 새 인용문을 나타내는 HTML 조각으로 응답하는 /quotes 경로만 있으면 된다.

post("/quotes") {      val parameters = call.receiveParameters()      val quote = parameters["quote"] ?: ""      val author = parameters["author"] ?: ""      val newQuote = Quote(quote = quote, author = author)      val id = quoteService.create(newQuote)      val createdQuote = quoteService.read(id)       call.respondHtml(HttpStatusCode.Created) {         body{        div {          p { +createdQuote.quote }          p { +"― ${createdQuote.author}" }        }    }  }

아주 간단하다. 고민이 필요한 한 부분은 코틀린의 HTML DSL이 HTML 조각 보내기를 좋아하지 않으므로 인용문 마크업을 body 태그로 래핑해야 하는데, 이 태그가 여기 있으면 안 된다는 점이다. (여기서는 내용을 간소하게 유지하기 위해 생락하겠지만 이 프로젝트에서 respondHtmlFragment라는 간단한 우회 방법을 찾을 수 있다.)

그 외에는 그냥 양식을 파싱하고 서비스를 사용해 인용문을 만든 다음 새 Quote를 사용해서 응답을 생성하면 된다. HTMX는 이 응답을 사용해 동적으로 UI를 업데이트한다.

결론

이 예제를 통해 빠르고 간결하게 케이터의 기본을 살펴봤다. 간단하지만 많은 오버헤드 없이 성능이 우수한 동적인 스택의 모든 요소를 갖췄다. 코틀린은 JVM을 기반으로 구축되었으므로 자바가 하는 모든 것에 액세스할 수 있다. 여기에 객체 지향 및 함수형 프로그래밍, DSL 기능의 강력한 조합까지 결합된 코틀린은 매력적인 서버 사이드 언어다. 기존 RESTful JSON 엔드포인트, 또는 여기서 살펴본 것처럼 동적 HTMX 기반 UI를 사용해서 애플리케이션을 빌드하는 데 사용할 수 있다.

케이터-HTMX 애플리케이션 예제의 전체 소스 코드는 필자의 깃허브 리포지토리에서 확인할 수 있다.
dl-itworldkorea@foundryco.com

관련자료

댓글 0
등록된 댓글이 없습니다.
Member Rank