티스토리 뷰

CMP

Ktorfit(Ktor + Retrofit)

pjm1n 2026. 1. 21. 00:01

Ktorfit이란?

Ktorfit은 안드로이드의 네트워크 라이브러리인 Retrofit과 ktor를 합한 라이브러리이다. 현재 KMP로 기존 안드로이드 프로젝트에서 확장중이다. Retrofit은 내부적으로 OkHttp를 사용하는 안드로이드 전용 라이브러리이기 때문에 Retrofit을 멀티플랫폼 환경에서 사용 불가하다. 따라서 Retrofit과 같은 방식을 사용하되, 멀티 플랫폼에서 사용가능한 Ktorfit을 사용했다.

의존성 추가

build.gradle(app 단위)

plugins {
	id("com.google.devtools.ksp") version "CURRENT_KSP_VERSION"
  	id("de.jensklingenberg.ktorfit") version "2.7.2"
}
sourceSets {
	commonMain.dependencies {
		implementation("de.jensklingenberg.ktorfit:ktorfit-lib:$ktorfitVersion")
		implementation("de.jensklingenberg.ktorfit:ktorfit-converters-response:$CONVERTER_VERSION")
	}
}

build.gradle(project 단위)

plugins {
    alias(libs.plugins.ksp) apply false
}

다음과 같이 어노테이션 프로세서인 KSP와 Ktorfit의 의존성을 추가해준다. 나는 응답을 Response객체로 받기 위해 ResponseConverter를 만들어주기 위한 의존성도 추가했다.

https://foso.github.io/Ktorfit/converters/converters/

https://github.com/google/ksp

위 코드에는 없지만 추가로 직렬화를 위해 kotlinx-serialization의 의존성도 추가해주었다.

https://github.com/Kotlin/kotlinx.serialization

요청 방법

Retrofit

import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.POST

interface DeviceService {
    @POST("devices")
    suspend fun registerDevice(
        @Body deviceRegisterRequest: DeviceRegisterRequest,
    ): Response<DeviceRegisterResponse>
}

Ktorfit

import de.jensklingenberg.ktorfit.Response
import de.jensklingenberg.ktorfit.http.Body
import de.jensklingenberg.ktorfit.http.POST

interface DeviceService {
    @POST("devices")
    suspend fun registerDevice(
        @Body deviceRegisterRequest: DeviceRegisterRequest,
    ): Response<DeviceRegisterResponse>
}

코드를 보면 알 수 있듯이 import만 다르고 요청 방법이 같다.

Interceptor

Retrofit

@ContributesBinding(AppScope::class)
@Inject
class FestaBookAuthInterceptor(
    private val festivalLocalDataSource: FestivalLocalDataSource,
) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val originalRequest = chain.request()
        val festivalId = festivalLocalDataSource.getFestivalId()
        val requestBuilder = originalRequest.newBuilder()

        if (festivalId != null) {
            Timber.d("festivalId : $festivalId")
            requestBuilder.addHeader("festival", festivalId.toString())
        }

        val requestWithHeader = requestBuilder.build()
        return chain.proceed(requestWithHeader)
    }
}
  1. chain을 통해 Retrofit이 생성한 현재 Request객체를 가져온다.(Request 객체는 불변이다.)
  2. 로컬 데이터를 datasource에서 받아 헤더에 실는다.
  3. 헤더에 실은 후 새로운 Request 객체를 생성하여 반환한다.

Ktorfit

@Inject
class FestaBookAuthPlugin(
    private val festivalLocalDataSource: FestivalLocalDataSource,
) {
    val plugin =
        createClientPlugin("FestaBookAuthPlugin", ::FestaBookAuthPluginConfig) {
            onRequest { request, _ ->
                val festivalId = festivalLocalDataSource.getFestivalId()

                if (festivalId != null) {
                Timber.d("festivalId : $festivalId")
                    request.headers["festival"] = festivalId.toString()
                }
            }
        }
}

  1. Ktorfit이 Request객체를 생성하기 전에 HttpRequestBuilder에 접근한다.
  2. 로컬데이터를 dataSource에서 받아온다.
  3. 받아온 데이터를 헤더에 추가한다.

"FestaBookAuthPlugin"은 Ktorfit이 plugin을 설치할 때 식별자로 사용한다. 중복 이름의 여러 plugin이 생기면 예외가 발생한다.

Ktorfit Client, Service 객체 생성

@BindingContainer
@ContributesTo(AppScope::class)
object NetworkBindings {
    @Provides
    fun provideJson(): Json = Json { ignoreUnknownKeys = true }

    @Provides
    fun provideResponseConverterFactory(): ResponseConverterFactory = ResponseConverterFactory()

    @Provides
    fun provideAppConfig(): AppConfig = createAppConfig()

    @Provides
    fun provideKtorfit(
        appConfig: AppConfig,
        responseConverterFactory: ResponseConverterFactory,
        authPlugin: FestaBookAuthPlugin,
        json: Json,
    ): Ktorfit =
        ktorfit {
            baseUrl(appConfig.baseUrl)
            converterFactories(responseConverterFactory)
            httpClient {
                HttpClient {
                    install(ContentNegotiation) {
                        json(json)
                    }
                    install(authPlugin.plugin)
                }
            }
        }

    @Provides
    fun provideScheduleService(ktorfit: Ktorfit): ScheduleService = ktorfit.createScheduleService()

    @Provides
    fun provideNoticeService(ktorfit: Ktorfit): NoticeService = ktorfit.createNoticeService()

    @Provides
    fun providePlaceService(ktorfit: Ktorfit): PlaceService = ktorfit.createPlaceService()

    @Provides
    fun provideDeviceService(ktorfit: Ktorfit): DeviceService = ktorfit.createDeviceService()

    @Provides
    fun provideFestivalNotificationService(ktorfit: Ktorfit): FestivalNotificationService = ktorfit.createFestivalNotificationService()

    @Provides
    fun provideFAQService(ktorfit: Ktorfit): FAQService = ktorfit.createFAQService()

    @Provides
    fun provideFestivalService(ktorfit: Ktorfit): FestivalService = ktorfit.createFestivalService()

    @Provides
    fun provideLostItemService(ktorfit: Ktorfit): LostItemService = ktorfit.createLostItemService()

    @Provides
    fun provideFestivalLineupService(ktorfit: Ktorfit): FestivalLineupService = ktorfit.createFestivalLineupService()
}

다음 코드는 Metro 자동 DI 라이브러리를 통해 Ktorfit Client 와 Service 객체를 생성하는 코드이다.

요청 받은 값을 Response 타입으로 받기 위해 ResponseConverterFactory를 사용했다. Retrofit은 자동으로 별다른 추가 설정 없이 자동으로 Response 타입을 명시하면 가능하지만 Ktorfit은 ConverterFactory를 통해 구현해주어야한다.

 

Retrofit은 메서드 체이닝을 통해 Retrfit 객체를 Build해주었지만, Ktorfit은 Kotlin DSL방식을 지원하기 때문에 지저분하게 체이닝하지 않고 Ktorfit{} 을 통해 Client 생성이 가능하다.

 

Retrofit은 create()를 통해 Service 객체를 생성했지만 Ktorfit은 Service 인터페이스에서 HTTP 메서드 관련 어노테이션을 달아준 후, 빌드를 한 번하면 KSP가 자동으로 createScheduleService같은 Service 객체를 생성해주는 확장함수를 만들어준다.

'CMP' 카테고리의 다른 글

SharedPreferences에서 DataStore로 마이그레이션하기  (1) 2026.02.01
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2026/02   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
글 보관함