새로운 메모리 관리자로 마이그레이션
Kotlin 1.9.20부터 레거시 메모리 관리자 지원이 완전히 제거되었습니다. Kotlin 1.7.20부터 기본적으로 활성화된 현재 메모리 모델로 프로젝트를 마이그레이션하세요.
이 가이드는 새로운 Kotlin/Native 메모리 관리자를 레거시 관리자와 비교하고 프로젝트 마이그레이션 방법을 설명합니다.
새로운 메모리 관리자에서 가장 눈에 띄는 변경 사항은 객체 공유에 대한 제한이 해제되었다는 것입니다. 객체를 스레드 간에 공유하기 위해 더 이상 고정(freeze)할 필요가 없으며, 구체적으로 다음과 같습니다:
- 최상위 프로퍼티는
@SharedImmutable
을 사용하지 않고도 어떤 스레드에서든 접근하고 수정할 수 있습니다. - 인터롭(interop)을 통해 전달되는 객체는 고정하지 않고도 어떤 스레드에서든 접근하고 수정할 수 있습니다.
Worker.executeAfter
는 더 이상 작업이 고정될 것을 요구하지 않습니다.Worker.execute
는 더 이상 생산자가 격리된 객체 서브그래프를 반환할 것을 요구하지 않습니다.AtomicReference
및FreezableAtomicReference
를 포함하는 참조 주기는 메모리 누수를 유발하지 않습니다.
손쉬운 객체 공유 외에도 새로운 메모리 관리자는 다른 주요 변경 사항도 제공합니다:
- 전역 프로퍼티는 해당 파일에 처음 접근할 때 지연 초기화됩니다. 이전에는 전역 프로퍼티가 프로그램 시작 시 초기화되었습니다. 이 문제를 해결하기 위해, 프로그램 시작 시 초기화되어야 하는 프로퍼티를
@EagerInitialization
애노테이션으로 마크할 수 있습니다. 사용하기 전에 문서를 확인하세요. by lazy {}
프로퍼티는 스레드 안전 모드를 지원하며 무한 재귀를 처리하지 않습니다.Worker.executeAfter
의operation
에서 탈출하는 예외는 다른 런타임 부분과 동일하게 처리됩니다. 사용자 정의 처리되지 않은 예외 훅을 실행하려고 시도하거나, 훅을 찾을 수 없거나 훅 자체가 예외를 발생시킨 경우 프로그램을 종료합니다.- 고정(Freezing)은 더 이상 사용되지 않으며 항상 비활성화됩니다.
레거시 메모리 관리자에서 프로젝트를 마이그레이션하려면 다음 지침을 따르세요:
Kotlin 업데이트
새로운 Kotlin/Native 메모리 관리자는 Kotlin 1.7.20부터 기본적으로 활성화되었습니다. Kotlin 버전을 확인하고 필요한 경우 최신 버전으로 업데이트하세요.
의존성 업데이트
버전 1.6.0 이상으로 업데이트하세요. native-mt
접미사가 있는 버전은 사용하지 마세요.
새로운 메모리 관리자와 관련된 몇 가지 특이 사항도 염두에 두어야 합니다:
- 고정이 필요하지 않으므로 모든 공통 프리미티브(채널, 플로우, 코루틴)는 Worker 경계를 통해 작동합니다.
Dispatchers.Default
는 Linux 및 Windows에서는 Worker 풀로 지원되고 Apple 대상에서는 전역 큐로 지원됩니다.- Worker로 지원되는 코루틴 디스패처를 생성하려면
newSingleThreadContext
를 사용하세요. N
개의 Worker 풀로 지원되는 코루틴 디스패처를 생성하려면newFixedThreadPoolContext
를 사용하세요.Dispatchers.Main
은 Darwin에서는 메인 큐로 지원되고 다른 플랫폼에서는 독립 Worker로 지원됩니다.
대부분의 라이브러리는 변경 없이 작동해야 하지만, 예외가 있을 수 있습니다.
의존성을 최신 버전으로 업데이트하고, 레거시 및 새로운 메모리 관리자를 위한 라이브러리 버전 간에 차이가 없는지 확인하세요.
코드 업데이트
새로운 메모리 관리자를 지원하려면 영향받는 API 사용을 제거하세요:
이전 API | 수행할 작업 |
---|---|
@SharedImmutable | 모든 사용을 제거할 수 있지만, 새로운 메모리 관리자에서 이 API를 사용해도 경고는 없습니다. |
FreezableAtomicReference 클래스 | 대신 AtomicReference 를 사용하세요. |
FreezingException 클래스 | 모든 사용을 제거하세요. |
InvalidMutabilityException 클래스 | 모든 사용을 제거하세요. |
IncorrectDereferenceException 클래스 | 모든 사용을 제거하세요. |
freeze() 함수 | 모든 사용을 제거하세요. |
isFrozen 프로퍼티 | 모든 //: # (title: Custom client plugins) |
Starting with v2.2.0, Ktor provides a new API for creating custom client plugins. In general, this API doesn't require an understanding of internal Ktor concepts, such as pipelines, phases, and so on. Instead, you have access to different stages of handling requests and responses using a set of handlers, such as onRequest
, onResponse
, and so on.
Create and install your first plugin
In this section, we'll demonstrate how to create and install your first plugin that adds a custom header to each request:
To create a plugin, call the createClientPlugin function and pass a plugin name as an argument:
kotlinpackage com.example.plugins import io.ktor.client.plugins.api.* val CustomHeaderPlugin = createClientPlugin("CustomHeaderPlugin") { // Configure the plugin ... }
This function returns the
ClientPlugin
instance that will be used to install the plugin.To append a custom header to each request, you can use the
onRequest
handler, which provides access to request parameters:kotlinTo install the plugin, pass the created
ClientPlugin
instance to theinstall
function inside the client's configuration block:kotlinimport com.example.plugins.* val client = HttpClient(CIO) { install(CustomHeaderPlugin) }
You can find the full example here: CustomHeader.kt. In the following sections, we'll look at how to provide a plugin configuration and handle requests and responses.
Provide plugin configuration
The previous section demonstrates how to create a plugin that appends a predefined custom header to each response. Let's make this plugin more useful and provide a configuration for passing any custom header name and value:
First, you need to define a configuration class:
kotlinTo use this configuration in a plugin, pass a configuration class reference to
createApplicationPlugin
:kotlinGiven that plugin configuration fields are mutable, saving them in local variables is recommended.
Finally, you can install and configure the plugin as follows:
kotlin
You can find the full example here: CustomHeaderConfigurable.kt.
Handle requests and responses
Custom plugins provide access to different stages of handling requests and responses using a set of dedicated handlers, for example:
onRequest
andonResponse
allow you to handle requests and responses, respectively.transformRequestBody
andtransformResponseBody
can be used to apply necessary transformations to request and response bodies.
There is also the on(...)
handler that allows you to invoke specific hooks that might be useful to handle other stages of a call. The tables below list all handlers in the order they are executed:
Handler | Description |
Handler | Description |
on(SetupRequest) | The SetupRequest hook is executed first in request processing. |
onRequest | This handler is executed for each HTTP request and allows you to modify it. |
transformRequestBody | Allows you to transform a request body. In this handler, you need to serialize the body into OutgoingContent (for example, |
on(Send) | The |
on(SendingRequest) | The Console
|
onResponse | This handler is executed for each incoming HTTP response and allows you to inspect it in various ways: log a response, save cookies, and so on. |
transformResponseBody | Allows you to transform a response body. This handler is invoked for each |
onClose | Allows you to clean resources allocated by this plugin. This handler is called when the client is closed. |
Share call state
Custom plugins allow you to share any value related to a call so that you can access this value inside any handler processing this call. This value is stored as an attribute with a unique key in the call.attributes
collection. The example below demonstrates how to use attributes to calculate the time between sending a request and receiving a response:
You can find the full example here: ResponseTime.kt.
Access client configuration
You can access your client configuration using the client
property, which returns the HttpClient instance. The example below shows how to get the proxy address used by the client:
import io.ktor.client.plugins.api.*
val SimplePlugin = createClientPlugin("SimplePlugin") {
val proxyAddress = client.engineConfig.proxy?.address()
println("Proxy address: $proxyAddress")
}
Examples
The code samples below demonstrate several examples of custom plugins. You can find the resulting project here: client-custom-plugin.
Custom header
Shows how to create a plugin that adds a custom header to each request:
Logging headers
Demonstrates how to create a plugin that logs request and response headers:
Response time
Shows how to create a plugin that measures the time between sending a request and receiving a response:
Data transformation
Shows how to transform request and response bodies using the transformRequestBody
and transformResponseBody
hooks:
You can find the full example here: client-custom-plugin-data-transformation.
Authentication
A sample Ktor project showing how to use the on(Send)
hook to add a bearer token to the Authorization
header if an unauthorized response is received from the server:
You can find the full example here: client-custom-plugin-auth.사용을 제거할 수 있습니다. 고정이 사용 중단되었으므로, 이 프로퍼티는 항상 false
를 반환합니다. | | ensureNeverFrozen()
함수 | 모든 사용을 제거하세요. | | atomicLazy()
함수 | 대신 lazy()
를 사용하세요. | | MutableData
클래스 | 대신 일반 컬렉션을 사용하세요. | | WorkerBoundReference<out T : Any>
클래스 | T
를 직접 사용하세요. | | DetachedObjectGraph<T>
클래스 | T
를 직접 사용하세요. C 인터롭(interop)을 통해 값을 전달하려면 StableRef
클래스를 사용하세요. |