커스텀 플러그인 - 기본 API
코드 예제: custom-plugin-base-api
Ktor는 v2.0.0부터 커스텀 플러그인 생성을 위한 새로운 간소화된 API를 제공합니다.
Ktor는 공통 기능을 구현하고 여러 애플리케이션에서 재사용할 수 있는 커스텀 플러그인 개발을 위한 API를 제공합니다. 이 API를 사용하면 다양한 파이프라인 페이즈를 가로채서 요청/응답 처리에 커스텀 로직을 추가할 수 있습니다. 예를 들어, Monitoring
페이즈를 가로채서 들어오는 요청을 로깅하거나 메트릭을 수집할 수 있습니다.
플러그인 생성
커스텀 플러그인을 생성하려면 다음 단계를 따르십시오:
- 플러그인 클래스를 생성하고 다음 인터페이스 중 하나를 구현하는 컴패니언 오브젝트를 선언합니다:
- 플러그인이 애플리케이션 수준에서 작동해야 하는 경우 BaseApplicationPlugin.
- 플러그인이 특정 라우트에 설치될 수 있는 경우 BaseRouteScopedPlugin.
- 이 컴패니언 오브젝트의
key
및install
멤버를 구현합니다. - 플러그인 설정을 제공합니다.
- 필요한 파이프라인 페이즈를 가로채서 호출을 처리합니다.
- 플러그인을 설치합니다.
컴패니언 오브젝트 생성
커스텀 플러그인 클래스에는 BaseApplicationPlugin
또는 BaseRouteScopedPlugin
인터페이스를 구현하는 컴패니언 오브젝트가 있어야 합니다. BaseApplicationPlugin
인터페이스는 세 가지 타입 파라미터를 받습니다:
- 이 플러그인과 호환되는 파이프라인의 타입.
- 이 플러그인에 대한 설정 객체 타입.
- 플러그인 객체의 인스턴스 타입.
class CustomHeader() {
companion object Plugin : BaseApplicationPlugin<ApplicationCallPipeline, Configuration, CustomHeader> {
// ...
}
}
'key' 및 'install' 멤버 구현
BaseApplicationPlugin
인터페이스의 하위 요소로서, 컴패니언 오브젝트는 두 멤버를 구현해야 합니다:
key
프로퍼티는 플러그인을 식별하는 데 사용됩니다. Ktor는 모든 속성의 맵을 가지고 있으며, 각 플러그인은 지정된 키를 사용하여 자신을 이 맵에 추가합니다.install
함수는 플러그인이 작동하는 방식을 설정할 수 있게 해줍니다. 여기에서 파이프라인을 가로채고 플러그인 인스턴스를 반환해야 합니다. 파이프라인을 가로채고 호출을 처리하는 방법은 다음 챕터에서 살펴보겠습니다.
class CustomHeader() {
companion object Plugin : BaseApplicationPlugin<ApplicationCallPipeline, Configuration, CustomHeader> {
override val key = AttributeKey<CustomHeader>("CustomHeader")
override fun install(pipeline: ApplicationCallPipeline, configure: Configuration.() -> Unit): CustomHeader {
val plugin = CustomHeader()
// Intercept a pipeline ...
return plugin
}
}
}
호출 처리
커스텀 플러그인에서 기존 파이프라인 페이즈 또는 새로 정의된 페이즈를 가로채서 요청과 응답을 처리할 수 있습니다. 예를 들어, Authentication 플러그인은 Authenticate
및 Challenge
커스텀 페이즈를 기본 파이프라인에 추가합니다. 따라서 특정 파이프라인을 가로채면 호출의 다양한 단계에 접근할 수 있습니다. 예를 들어:
ApplicationCallPipeline.Monitoring
: 이 페이즈를 가로채는 것은 요청 로깅 또는 메트릭 수집에 사용될 수 있습니다.ApplicationCallPipeline.Plugins
: 응답 파라미터를 수정하는 데 사용될 수 있으며, 예를 들어 커스텀 헤더를 추가할 수 있습니다.ApplicationReceivePipeline.Transform
및ApplicationSendPipeline.Transform
: 클라이언트로부터 수신된 데이터를 얻고 변환하며, 데이터를 다시 보내기 전에 변환할 수 있게 해줍니다.
아래 예시는 ApplicationCallPipeline.Plugins
페이즈를 가로채서 각 응답에 커스텀 헤더를 추가하는 방법을 보여줍니다:
class CustomHeader() {
companion object Plugin : BaseApplicationPlugin<ApplicationCallPipeline, Configuration, CustomHeader> {
override val key = AttributeKey<CustomHeader>("CustomHeader")
override fun install(pipeline: ApplicationCallPipeline, configure: Configuration.() -> Unit): CustomHeader {
val plugin = CustomHeader()
pipeline.intercept(ApplicationCallPipeline.Plugins) {
call.response.header("X-Custom-Header", "Hello, world!")
}
return plugin
}
}
}
이 플러그인에서 커스텀 헤더 이름과 값이 하드코딩되어 있음에 유의하십시오. 필요한 커스텀 헤더 이름/값을 전달하기 위한 설정을 제공하여 이 플러그인을 더 유연하게 만들 수 있습니다.
커스텀 플러그인을 사용하면 호출과 관련된 모든 값을 공유할 수 있으므로, 이 값을 해당 호출을 처리하는 모든 핸들러 내에서 접근할 수 있습니다. 호출 상태 공유에서 더 자세히 알아볼 수 있습니다.
플러그인 설정 제공
이전 챕터에서는 미리 정의된 커스텀 헤더를 각 응답에 추가하는 플러그인을 생성하는 방법을 보여줍니다. 이 플러그인을 더 유용하게 만들고 필요한 커스텀 헤더 이름/값을 전달하기 위한 설정을 제공해봅시다. 먼저, 플러그인 클래스 내에 설정 클래스를 정의해야 합니다:
class Configuration {
var headerName = "Custom-Header-Name"
var headerValue = "Default value"
}
플러그인 설정 필드는 변경 가능(mutable)하므로, 이를 지역 변수에 저장하는 것이 좋습니다:
class CustomHeader(configuration: Configuration) {
private val name = configuration.headerName
private val value = configuration.headerValue
class Configuration {
var headerName = "Custom-Header-Name"
var headerValue = "Default value"
}
}
마지막으로, install
함수에서 이 설정을 가져와서 그 속성을 사용할 수 있습니다.
class CustomHeader(configuration: Configuration) {
private val name = configuration.headerName
private val value = configuration.headerValue
class Configuration {
var headerName = "Custom-Header-Name"
var headerValue = "Default value"
}
companion object Plugin : BaseApplicationPlugin<ApplicationCallPipeline, Configuration, CustomHeader> {
override val key = AttributeKey<CustomHeader>("CustomHeader")
override fun install(pipeline: ApplicationCallPipeline, configure: Configuration.() -> Unit): CustomHeader {
val configuration = Configuration().apply(configure)
val plugin = CustomHeader(configuration)
pipeline.intercept(ApplicationCallPipeline.Plugins) {
call.response.header(plugin.name, plugin.value)
}
return plugin
}
}
}
플러그인 설치
애플리케이션에 커스텀 플러그인을 설치하려면 install
함수를 호출하고 원하는 설정 파라미터를 전달하십시오:
install(CustomHeader) {
headerName = "X-Custom-Header"
headerValue = "Hello, world!"
}
예시
아래 코드 스니펫은 커스텀 플러그인의 몇 가지 예시를 보여줍니다. 실행 가능한 프로젝트는 여기에서 찾을 수 있습니다: custom-plugin-base-api
요청 로깅
아래 예시는 들어오는 요청을 로깅하기 위한 커스텀 플러그인을 생성하는 방법을 보여줍니다:
package com.example.plugins
import io.ktor.serialization.*
import io.ktor.server.application.*
import io.ktor.server.plugins.*
import io.ktor.util.*
class RequestLogging {
companion object Plugin : BaseApplicationPlugin<ApplicationCallPipeline, Configuration, RequestLogging> {
override val key = AttributeKey<RequestLogging>("RequestLogging")
override fun install(pipeline: ApplicationCallPipeline, configure: Configuration.() -> Unit): RequestLogging {
val plugin = RequestLogging()
pipeline.intercept(ApplicationCallPipeline.Monitoring) {
call.request.origin.apply {
println("Request URL: $scheme://$localHost:$localPort$uri")
}
}
return plugin
}
}
}
커스텀 헤더
이 예시는 각 응답에 커스텀 헤더를 추가하는 플러그인을 생성하는 방법을 보여줍니다:
package com.example.plugins
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.util.*
class CustomHeader(configuration: Configuration) {
private val name = configuration.headerName
private val value = configuration.headerValue
class Configuration {
var headerName = "Custom-Header-Name"
var headerValue = "Default value"
}
companion object Plugin : BaseApplicationPlugin<ApplicationCallPipeline, Configuration, CustomHeader> {
override val key = AttributeKey<CustomHeader>("CustomHeader")
override fun install(pipeline: ApplicationCallPipeline, configure: Configuration.() -> Unit): CustomHeader {
val configuration = Configuration().apply(configure)
val plugin = CustomHeader(configuration)
pipeline.intercept(ApplicationCallPipeline.Plugins) {
call.response.header(plugin.name, plugin.value)
}
return plugin
}
}
}
본문 변환
아래 예시는 다음을 보여줍니다:
- 클라이언트로부터 수신된 데이터를 변환하는 방법;
- 클라이언트로 전송될 데이터를 변환하는 방법.
package com.example.plugins
import io.ktor.serialization.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.util.*
import io.ktor.utils.io.*
class DataTransformation {
companion object Plugin : BaseApplicationPlugin<ApplicationCallPipeline, Configuration, DataTransformation> {
override val key = AttributeKey<DataTransformation>("DataTransformation")
override fun install(pipeline: ApplicationCallPipeline, configure: Configuration.() -> Unit): DataTransformation {
val plugin = DataTransformation()
pipeline.receivePipeline.intercept(ApplicationReceivePipeline.Transform) { data ->
val newValue = (data as ByteReadChannel).readUTF8Line()?.toInt()?.plus(1)
if (newValue != null) {
proceedWith(newValue)
}
}
pipeline.sendPipeline.intercept(ApplicationSendPipeline.Transform) { data ->
if (subject is Int) {
val newValue = data.toString().toInt() + 1
proceedWith(newValue.toString())
}
}
return plugin
}
}
}
파이프라인
Ktor의 파이프라인은 하나 이상의 정렬된 페이즈로 그룹화된 인터셉터들의 집합입니다. 각 인터셉터는 요청을 처리하기 전과 후에 커스텀 로직을 수행할 수 있습니다.
ApplicationCallPipeline은 애플리케이션 호출을 실행하기 위한 파이프라인입니다. 이 파이프라인은 5가지 페이즈를 정의합니다:
Setup
: 호출과 해당 속성을 처리하기 위해 준비하는 데 사용되는 페이즈.Monitoring
: 호출을 추적하기 위한 페이즈입니다. 요청 로깅, 메트릭 수집, 오류 처리 등에 유용할 수 있습니다.Plugins
: 호출을 처리하는 데 사용되는 페이즈입니다. 대부분의 플러그인은 이 페이즈에서 가로챕니다.Call
: 호출을 완료하는 데 사용되는 페이즈.Fallback
: 처리되지 않은 호출을 처리하기 위한 페이즈.
파이프라인 페이즈를 새 API 핸들러에 매핑
Ktor는 v2.0.0부터 커스텀 플러그인 생성을 위한 새로운 간소화된 API를 제공합니다. 일반적으로 이 API는 파이프라인, 페이즈 등과 같은 Ktor 내부 개념에 대한 이해를 요구하지 않습니다. 대신, onCall
, onCallReceive
, onCallRespond
등과 같은 다양한 핸들러를 사용하여 요청 및 응답 처리의 여러 단계에 접근할 수 있습니다. 아래 표는 파이프라인 페이즈가 새 API의 핸들러에 어떻게 매핑되는지 보여줍니다.
기존 API | 새로운 API |
---|---|
before ApplicationCallPipeline.Setup | on(CallFailed) |
ApplicationCallPipeline.Setup | on(CallSetup) |
ApplicationCallPipeline.Plugins | onCall |
ApplicationReceivePipeline.Transform | onCallReceive |
ApplicationSendPipeline.Transform | onCallRespond |
ApplicationSendPipeline.After | on(ResponseBodyReadyForSend) |
ApplicationSendPipeline.Engine | on(ResponseSent) |
after Authentication.ChallengePhase | on(AuthenticationChecked) |