반응형
Intro
Ktor 프레임워크의 공식문서를 기반으로 학습하여 간단히 HTTP API를 만들어 실행까지 해보는 것을 목표로 한다. 본 내용은 실제 데이터베이스를 사용하여 정보를 관리하지 않고 간단히 리스트에 저장하여 실습한다.
- 백엔드 역할을 수행할 수 있는 HTTP API 만들기
- API 엔드포인트를 구성하고 정의하기
- 직렬화 플러그인을 통해 JSON 변환 단순화하기
- 고객(Customer)과 주문(Order)에 대한 정보 조회
- 고객(Customer) 정보 추가 및 제거
Ktor 프로젝트 생성하기
프로젝트 생성
Ktor Project Generator(https://start.ktor.io/) 또는 IntelliJ IDEA Ultimate를 통해 프로젝트를 생성한다.
Ktor 프로젝트 생성 방법은 아래 게시글에서 다룬다.
https://ittrue.tistory.com/439
플러그인
다음 세 가지 플러그인을 추가하여 프로젝트를 생성한다.
- Routing : 애플리케이션의 서버에서 들어오는 요청을 처리하기 위한 플러그인
- Content Negotiation : 코틀린 객체를 직렬화/역직렬화하기 위해 편리한 메커니즘 제공하는 플러그인
- kotlinx.serialization : ContentNegotiation을 사용하여 JSON, XML, CBOR 등으로 직렬화/역직렬 기능을 제공하는 플러그인
프로젝트 설정
Dependencies
build.gradle.kts 파일에서 추가된 의성을 확인한다.
dependencies {
implementation("io.ktor:ktor-server-core-jvm:$ktor_version")
implementation("io.ktor:ktor-server-netty-jvm:$ktor_version")
implementation("io.ktor:ktor-server-content-negotiation:$ktor_version")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version")
implementation("io.ktor:ktor-server-config-yaml:$ktor_version")
implementation("ch.qos.logback:logback-classic:$logback_version")
testImplementation("io.ktor:ktor-server-tests-jvm:$ktor_version")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
}
- ktor-server-core : Ktor 프레임워크의 핵심 구성 요소를 담당한다.
- ktor-server-netty : 프로젝트 생성 시 Engine을 Netty로 선택하면 생기는 의존성이다. 서버 엔진을 Netty를 사용하여 외부 애플리케이션 컨테이너에 의존하지 않고도 서버 기능을 사용할 수 있다.
- server-content-negotiation : 코틀린 객체를 직렬화/역직렬화하기 위한 플러그인이다.
- serialization-kotlinx-json : API 응답 형식과 사용자 요청 형식을 JSON으로 지정하고 직렬화/역직렬화 기능을 제공한다.
- ktor-server-config-yaml : 프로젝트 생성할 때 Configuration in을 YAML 파일로 설정한 경우 추가되는 의존성이다. 애플리케이션 구성 및 환경 변수 등을 YAML 파일로 관리할 수 있다.
- logback:logback-classic : SLF4J의 구현을 제공하여 콘솔에 형식을 갖춘 로그를 출력한다.
- kotlin-test-junit : JUnit4 라이브러리로 프로젝트의 테스트 코드 작성 및 테스트 기능을 제공한다.
application.yml
ktor:
application:
modules:
- com.example.ApplicationKt.module
deployment:
port: 8080
- 애플리케이션 실행 시 참조하게 되는 기본 모듈 경로와 로컬 서버에서 요청 및 응답을 하기 위해 8080으로 설정이 기본적으로 작성되어 있다.
Application.kt
fun main(args: Array<String>): Unit =
io.ktor.server.netty.EngineMain.main(args)
@Suppress("unused")
fun Application.module() {
configureRouting()
configureSerialization()
}
- Application.module 함수가 application.yml에서 설정된 모듈이다. 이를 확장하여 기능을 호출한다.
Routing.kt
fun Application.configureRouting() { // module()의 configureRounting() 확장 기능
routing {
customerRouter()
listOrdersRouter()
getOrderRouter()
totalizeOrderRouter()
}
}
- routing {} 블록 안에 추가된 함수들은 앞으로 구현해야 할 함수들로 API 엔드포인트 및 내부 구현 로직이 정의된다.
- 각각의 Router들은 MVC 패턴의 Controller 역할을 한다.
Serialization.kt
fun Application.configureSerialization() { // configureSerialization() 확장 기능
install(ContentNegotiation) {
json()
}
}
- 직렬화/역직렬화를 담당하는 기능을 구성한다.
- ContentNegotiation에 json 변환기를 활성화한다.
코드 작성
Customer
import kotlinx.serialization.Serializable
@Serializable // API 응답 시 객체 -> JOSN 변환
data class Customer(
val id: String,
val firstName: String,
val lastName: String,
val email: String
) {
fun hello(firstName: String): String {
return "hello $firstName"
}
}
val customerStorage = mutableListOf<Customer>()
- Customer는 id로 식별할 수 있으며, 이름과 성, 이메일 정보를 가지고 있다.
- Customer 추가 시 간단한 인사말을 응답하기 위한 hello 함수가 있다.
- customerStorage에는 Customer에 대한 정보가 리스트 형태로 담기며, 추가 및 삭제를 할 수 있도록 MutableList로 선언한다. (데이터베이스의 Customer 테이블을 대체하는 역할)
CustomerRouter
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun Route.customerRouter() {
route("/customer") {
get { // Customer 전체 조회
if (customerStorage.isNotEmpty()) { // 고객 리스트 검증
call.respond(customerStorage) // 전체 조회
} else { // 조회 실패
call.respondText(text = "No customers found", status = HttpStatusCode.OK)
}
}
get("{id?}") { // 특정 Customer 조회
// 요청 매개변수인 id가 null인 경우
val id = call.parameters["id"] ?: return@get call.respondText( // id가 null인 경우
text = "Missing id",
status = HttpStatusCode.BadRequest
)
// id가 일치하지 않는 경우
val customer = customerStorage.find { it.id == id } ?: return@get call.respondText(
text = "No customer with id $id",
status = HttpStatusCode.NotFound
)
call.respond(customer) // customer 조회 성공시 반환
}
post { // Customer 추가
// JSON 형식의 요청 내용을 Customer 객체로 변환
val customer = call.receive<Customer>()
customerStorage.add(customer) // Customer 추가
call.respondText(text = customer.hello(customer.firstName), status = HttpStatusCode.Created)
}
delete("{id?}") { // Customer 삭제
// 요청 매개변수인 id가 null인 경우
val id = call.parameters["id"] ?: return@delete call.respond(HttpStatusCode.BadRequest)
if (customerStorage.removeIf {it.id == id}) { // id가 일치하는 Customer가 있는 경우
call.respondText(text = "Customer removed correctly", status = HttpStatusCode.Accepted)
} else { // id가 일치하는 Customer가 없는 경우
call.respondText(text = "Not Found", status = HttpStatusCode.NotFound)
}
}
}
}
- Customer의 정보 조회, 추가, 삭제 등의 기능을 하는 API다.
- route(”customer”) : customerRouting 함수 내에 존재하는 항목에 대한 기본 엔드포인트
- get(”{id?}”)와 같이 각각의 HTTP 메서드에 경로를 지정할 수 있다. (?는 null 가능함을 의미)
Order
import kotlinx.serialization.Serializable
@Serializable
data class Order(
val number: String,
val contents: List<OrderItem>
)
@Serializable
data class OrderItem(
val item: String,
val amount: Int,
val price: Double
)
val orderStorage = listOf(
Order("2023-01-01-01", listOf(
OrderItem("Ham Sandwich", 2, 5.50),
OrderItem("Water", 1, 1.50),
OrderItem("Beer", 3, 2.50),
OrderItem("Cheesecake", 1, 3.75)
)),
Order("2023-07-01-01", listOf(
OrderItem("Cheeseburger", 1, 8.50),
OrderItem("Water", 2, 1.50),
OrderItem("Coke", 2, 1.76),
OrderItem("Ice Cream", 1, 2.35)
))
)
- Order는 name으로 식별 가능하며, contents 리스트 안에 주문한 아이템들이 담겨 있다.
- OrderItem은 주문한 아이템에 대한 정보로 이름, 개수, 가격 정보가 담겨있다.
- orderStorage는 주문한 아이템들의 내역을 담고있는 리스트다.
OrderRouter
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun Route.listOrdersRouter() {
get("/order") { // 모든 Order 및 OrderItem 조회
if (orderStorage.isNotEmpty()) {
call.respond(orderStorage)
}
}
}
fun Route.getOrderRouter() {
get("/order/{id?}") { // 개별 Order 및 OrderItem 조회
val id = call.parameters["id"] ?: return@get call.respondText(text = "Bad request", status = HttpStatusCode.BadRequest)
val order = orderStorage.find { it.number == id } ?: return@get call.respondText(
text = "Not Found",
status = HttpStatusCode.NotFound
)
call.respond(order)
}
}
fun Route.totalizeOrderRouter() {
get("/order/{id?}/total") { // 개별 Order에 대한 합산 가격 조회
val id = call.parameters["id"] ?: return@get call.respondText(text = "Bad request", status = HttpStatusCode.BadRequest)
val order = orderStorage.find { it.number == id } ?: return@get call.respondText(
text = "Not found",
status = HttpStatusCode.NotFound
)
val total = order.contents.sumOf { it.price * it.amount }
call.respond(total)
}
}
- 모든 Order와 OrderItem 조회, 개별 Order와 OrderItem 조회, 개별 Order에 대한 가격의 합 조회 기능을 하는 API이다.
테스트
애플리케이션을 실행하여 Postman을 통해 각각의 API를 호출하여 테스트를 진행한다.
Customer 테스트
고객이 없는 경우(전체 조회)
고객이 있는 경우(전체 조회)
특정 고객 조회
고객 추가
고객 삭제
Order 테스트
모든 Order 조회
특정 Order 조회
특정 Order의 OrderItem 합산 가격 조회
테스트 코드 작성
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.content.*
import io.ktor.http.*
import io.ktor.server.testing.*
import io.ktor.util.*
import org.junit.Test
import kotlin.test.assertEquals
class CustomerRouterTest {
@Test
fun getCustomerListTest() = testApplication{
val response = client.get("/customer")
val expected = """
[
{
"id":"100",
"firstName":"Jane",
"lastName":"Smith",
"email":"jane.smith@company.com"
},
{
"id":"200",
"firstName":"John",
"lastName":"Smith",
"email":"john.smith@company.com"
},
{
"id":"300",
"firstName":"Mary",
"lastName":"Smith",
"email":"mary.smith@company.com"
}
]""".trimIndent()
assertEquals(expected.replace("(\\\\s)".toRegex(), ""), response.bodyAsText())
assertEquals(HttpStatusCode.OK, response.status)
}
@Test
fun getCustomerTest() = testApplication {
val response = client.get("/customer/100")
val expected = """{"id":"100","firstName":"Jane","lastName":"Smith","email":"jane.smith@company.com"}"""
assertEquals(expected, response.bodyAsText())
assertEquals(HttpStatusCode.OK, response.status)
}
@Test
@OptIn(InternalAPI::class)
fun postCustomerTest() = testApplication {
val jsonString = """
{
"id" : "400",
"firstName" : "Gildong",
"lastName" : "Hong",
"email" : "mary.smith@company.com"
}
""".trimIndent()
val response = client.post {
url("/customer")
contentType(ContentType.Application.Json)
body = TextContent(jsonString, ContentType.Application.Json)
}
assertEquals(HttpStatusCode.Created, response.status)
}
@Test
fun deleteCustomerTest() = testApplication {
val response = client.delete("/customer/100")
assertEquals(HttpStatusCode.Accepted, response.status)
}
}
JUnit 라이브러리를 통해 애플리케이션을 실행하지 않고도 테스트를 진행할 수 있다.
반응형
'프레임워크(Framework) > Ktor' 카테고리의 다른 글
[Ktor] Kotlin + Ktor 환경에서 의존성 주입하는 방법 - Dependency Injection (0) | 2023.08.21 |
---|---|
[Ktor] Kotlin + Ktor 환경에서 Ktorm ORM 엔티티 작성 및 테이블 매핑하기 (0) | 2023.07.26 |
[Ktor] Kotlin + Ktor + Ktorm 환경에서 MySQL 연동하기 (0) | 2023.07.25 |
[Ktor] Kotlin 객체로 Yml 파일 읽어서 사용하기 - Jackson 라이브러리 활용 (0) | 2023.07.17 |
[Ktor] Ktor Framework란? - Ktor 소개 및 프로젝트 생성하기 (0) | 2023.06.28 |