Android KSP(Kotlin Symbol Processing) 활용을 위한 준비단계!



개인 광고 영역

KSP란 뭘까? Kotlin Symbol Processing의 약자로 경량 컴파일러 플러그인을 개발할때 사용하는 API라고 한다.

Kotlin Symbol Processing API - 공식 문서

직접 만들지는 않았지만 그간 kapt를 활용하였고, 여전히 활용 중이다.

apply plugin: 'kotlin-kapt'
// 또는
kotlin("kapt")

이와 같은 코드가 존재한다면 kapt를 활용하고 있다는 것이다.

DataBinding, Dagger등에서 이를 활용하고 있는데, 이들이 해주는 역할은 @Annotation을 적어주면 코드를 자동으로 만들어주는 역할을 하고 있다는 것이다.

아래와 같이 DataBinding 코드를 확인해 볼 수 있다.

image_01

kapt는 Java 기반이다. ksp는 kotlin 기반으로, kapt 보다 2배 더 빠르고 기능을 단순화 시켜 더 쉽게 만들 수 있다.

필자도 4개의 ksp 모듈을 만들어 활용하고 있다.


이 글에서 알아볼 내용

  • ksp는 언제 쓰는 게 좋을까?
  • ksp를 활용하기 위한 기초 지식


KSP란?

KSP는 Kotlin Symbol Processing의 약자인데, kotlin 기반의 코드 작성이 가능하다.

kapt 대신 사용할 수 있는 kotlin 기반의 코드 작성이 가능하며, kapt에 비해 2배 빨라진 성능을 확인할 수 있다.

추가 개발하지 않으면 사용자가 명시적 리빌드 해야 하는 단점이 있다.

Google에서는 Room을 kapt 기반에서 ksp 기반으로 변경하였다. 아직 DataBinding과 Dagger 등은 포함되어 있지 않다.

kotlin 사이트에서도 KSP를 소개하고 있지만 공식적으론 Google에서 관리하는 오픈소스 프로젝트이다.


KSP 발표 내용


KSP 언제 쓸까?

KSP를 언제 쓰는 게 좋을까?

매우 귀찮은 중복 코드를 자동화 시켜 처리할 수 있다. 보통은 Room, Dagger, DataBinding, Moshi를 사용하다 보면 자동으로 만들어지는 코드를 본 적이 있을 것이다. 자동으로 만들어진다는 건, 사용자는 알 필요 없이 사용자가 필요한 코드를 자동으로 생성해 준다.

Dagger도 주입이긴 하지만 이런 부분을 자동으로 만들어주고 있다.

그 말은 개인이 KSP를 활용해 코드를 만들 수 있다는 소리다. 하지만 어디까지 지원할 것인지는 사용자에게 달렸다는 점이다.

개인적으로 회사 프로젝트는 아래와 같은 모듈을 활용하고 있다.

  • Dagger를 활용하고 있는데, Dagger의 Provider module, Component, InjectorModule 등을 KSP를 활용해 모두 자동으로 만들고 있다.
  • 결과적으로 Dagger Hilt처럼 사용하도록 만든 모듈이다.
  • Compose CompositionLocalProvider에 ViewModel을 provider 하는 자동화 모듈에 활용

이와 같이 매우 귀찮은 반복작업에 활용하고 있다.

다만, 매번 리빌드 해야 하는 단점이 있는데, 이에 대한 보안으로 코드를 작성하다 보니 리빌드 역시 최소화만 활용하도록 작업하고 있다.

가장 좋은 리빌드 방식은 앱을 Run 할 때 알아서 빌드하고, 코드에 영향 미치지 않는 범위가 가장 좋다.

하지만 자동화가 가능한 부분도 있고, 그렇지 않은 부분도 있는데 이 부분은 KSP를 어디에서 사용할지에 따라 선택해야 할 문제이다.

  • 코드 자동화를 통해 작성 코드의 최소화할 것인가?
  • 그냥 자동화 대신 작성 코드를 중복하지 않는 함수를 쓸 것인가?

모두 사용자의 선택인 것이다. 어떤 부분이 더 좋은지는 사용자가 선택하면 되겠다.


필자가 참고한 자료는?

한국 자료들도 몇몇 있는데, 번역본과 간단한 샘플 코드를 담고 있는 정도이다. 결국 공식 문서와 KSP를 통해 무엇을 할지에 따라 코드 범위가 달라진다.

다양한 자료를 통해 먼저 KSP를 학습하고, 공식 문서를 통해 더 많은 정보를 얻을 수 있을 것이다.


KSP 버전은?

KSP는 코틀린 1.4.10 버전부터 활용이 가능하며, Kotlin 버전을 올릴 때 KSP 버전도 함께 올려야 한다.

KSP-Releases에서 확인할 수 있으며, 현재는 Kotlin 1.7.0-Beta 버전이 나온 상태이니 가장 최신은 1.7.0-Beta로 테스트 가능하다.

필자는 컴포즈를 활용하고 있어, 1.6.20의 디펜던시를 활용하고 있다.

KSP 버전은 앞에 숫자는 코틀린 버전이고, 뒤가 KSP 버전을 나타낸다.

  • 1.6.20-1.0.5 : 코틀린 1.6.20 기반의 1.0.5 버전
  • 1.6.21-1.0.5 : 코틀린 1.6.21 기반의 1.0.5 버전
  • 1.7.0-Beta-1.0.5 : 코틀린 1.7.0-Beta 기반의 1.0.5 버전

자신의 코틀린 버전에 맞는 KSP 버전을 활용하면 되겠다.


KSP 모듈 추가

KSP 작업용 모듈을 추가해 보자. 이 모듈은 jvm 모듈로 생성하고, 디펜던시는 kotlin과 ksp 두 개를 적용하는데, 버전은 kotlin 버전에 맞게 설정해야 한다.

plugins {
    kotlin("jvm")
}

dependencies {
    implementation("org.jetbrains.kotlin:kotlin-stdlib:1.6.21")
    implementation("com.google.devtools.ksp:symbol-processing-api:1.6.21-1.0.5")
}

ksp 임을 명시해 주기 위해 src/main/resources/META-INF/services 폴더를 생성하고, 여기에 com.google.devtools.ksp.processing.SymbolProcessorProvider 파일을 하나 만들고, 이 파일에는 SymbolProcessorProvider을 상속받은 class 명시가 필요하다.

package tech.thdev.ksp.sample

class SampleProcessorProvider : SymbolProcessorProvider {

    override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
        TODO("Not yet implemented")
    }
}

제가 만든 class 이름은 SampleProcessorProvider이니 아래와 같이 추가해 준다.

tech.thdev.ksp.sample.SampleProcessorProvider

최종적으로 만들어진 파일은 다음과 같으니 참고하길

image_02


SymbolProcessorEnvironment

SymbolProcessorEnvironment를 이용해 몇 가지 정보를 가져올 수 있다.

  • options : ksp arg를 통해 지정한 옵션 정보를 가져올 수 있는데, map으로 key, value 형태로 정보를 제공하며, 값이 없을 경우 null을 리턴한다.
  • codeGenerator : 파일 생성을 위한 codeGenerator이다.
  • logger : KSP 빌드 타임에 디버깅하기 위해서는 추가 옵션이 필요하지만, logger를 이용해 error, exception, wran 메시지 등을 출력할 수 있다. 필수로 필요한 정보가 빠졌을 경우 logger를 활용해 오류를 발생시킬 수 있다.

보통 위와 같은 3가지 옵션을 통해 ksp 파일 생성 및 디버깅 파일 생성까지 모두 가능한데 options 활용을 조금 더 살펴보자.

options 활용

options을 추가할 때는 gralde에서 아래와 같이 추가하고,

ksp {
  arg("OPTION_1", "value")
  arg("OPTION_2", "value")
}

이 값을 가져올 때는 다음과 같이 가져올 수 있는데, 이때 설정한 key/value가 없을 경우 null을 리턴한다.

optionOne = environment.options["OPTION_1"] // 없으면 null
optionTwo = environment.options["OPTION_2"] // 없으면 null


Annotation 정의하기 전에 다른 샘플을 보자

Annotation을 정의하기 전에 이미 만들어진 오픈 소스 프로젝트의 Compose 관련 KSP generated 코드를 살펴보자.

  • 정승욱 님이 배포한 Android alatan 프로젝트로 필자도 참여하고 있다.

Provided Compose local ksp

이 코드는 다음의 Annotation을 추가하면 Compose 활용할 수 있는 CompositionLocalProvider에 ViewModel을 등록하고, 이를 viewModel() 함수를 내가 작업 중인 UI 어디에서든 자유롭게 불러 사용할 수 있다.

@ProvidedComposeLocal

이를 직접 작성하는 방법도 있겠지만 KSP를 활용해 중복적인 코드 작성을 줄이고, 자동으로 만들어준 코드를 통해 개발 속도를 올릴 수 있다.

여담이지만 CPU 파워를 더 쓸지, 내 손을 더 사용할지를 결정하면 되겠다. 이 코드의 단점은 리빌드를 해야 viewModel()로 접근할 수 있다.

아래와 같이 Compose 활용 Activity에 @ProvidedComposeLocal 어노테이션을 적어준다.

class ComposeLifecycleSampleActivity : ComposeLifecycleActivity() {

    @ProvidedComposeLocal
    val viewModel = ComposeSampleViewModel()
}

그리고 리빌드하면 다음 코드가 자동으로 생성되는데 2가지로 나누어져 있다.

  • ProvidableCompositionLocal을 등록할 수 있는 파일 하나
private val LocalComposeSampleViewModel: ProvidableCompositionLocal<ComposeSampleViewModel> =
    compositionLocalOf<ComposeSampleViewModel> { error("LocalComposeSampleViewModel isn't provided")
    }

internal fun viewModelProviderValue(viewModel: ComposeSampleViewModel) =
    LocalComposeSampleViewModel provides viewModel

@Composable
internal fun viewModel() = LocalComposeSampleViewModel.current
  • ProvidableCompositionLocal을 사용을 도와주는 파일 하나

이 파일의 extension을 활용해 주면 Compose의 ProvidedComposeLocal을 provided에 등록하고, 이를 viewModel()로 자유롭게 가져와 활용할 수 있다.

@Composable
internal fun ComposeLifecycleSampleActivity.ComposeLocalProvider(content: @Composable () -> Unit):
    Unit {
  CompositionLocalProvider(
      viewModelProviderValue(viewModel),
      content = content
      )
}

아래와 같이 ComposeLocalProvider 함수를 호출해 주면 되겠다.

class ComposeLifecycleSampleActivity : ComposeLifecycleActivity() {

  @ProvidedComposeLocal
  val viewModel = ComposeSampleViewModel()

  @Composable
  override fun ContentView() {
      ComposeLocalProvider {
          // 여기에 Compose 코드 정의
      }
  }
}

그리고 위 클래스와 무관한 곳에서도 viewModel()을 바라볼 수 있게 된다.

fun View() {
  val data = viewModel().data ...
}

internal 키워드를 활용하였기 때문에 다른 모듈이나 외부에서는 접근할 수 없다.

이와 같은 형태의 활용을 위해 Annotation을 정의해 주면 되겠다.

Annotation 정의와 함께 목적에 맞는 코드를 빌드 타임에 자동으로 만들어줄 수 있는 것이다.


마무리

KSP를 적용해 보고, 오류 케이스를 만들어보았다. KSP를 간단하게 다루는 정도로만 이 글에서 소개하였지만, 이어서 작성하는 글에서 조금 더 구체적인 내용을 다루어보도록 하겠다.



About Taehwan

My name is Taehwan Kwon. I have developed Android for 6 years and blog has been active for eight years.

Comments