Kotlin Symbol Processing API

Kotlin Symbol Processing API

Kotlin Symbol Processing(KSP)은 Kotlin을 위한 소스 코드 생성 프레임워크예요. KSP API를 사용하면 소스 코드 안의 애너테이션을 기반으로 코드를 생성하는 프로세서를 만들 수 있습니다.

출처: Kotlin Symbol Processing API

본문

KSP는 가벼운 컴파일러 플러그인을 쉽게 만들 수 있도록 돕는 걸 목표로 해요. 잘 정의된 API가 컴파일러 변경 사항을 숨겨 주기 때문에, 프로세서를 유지보수하는 데 큰 노력을 들일 필요가 없죠. 다만 이 방식에는 맞바꿈(trade-off)이 있어요. 예를 들어 KSP 기반 프로세서는 표현식이나 문을 조사할 수 없고, 소스 코드를 수정할 수도 없습니다.

KSP 기반 플러그인의 대표적인 사용 사례는 다음과 같아요.

  • 의존성 주입(Dagger)
  • 직렬화(Moshi)
  • 데이터베이스 관리(Room)

첫 KSP 기반 프로세서를 만드는 방법은 KSP 퀵스타트(quickstart)에서 확인할 수 있어요.

개요

KSP API는 Kotlin 프로그램을 관용적(idiomatic)으로 처리해요. KSP는 확장 함수, 선언부 변성(declaration-site variance), 로컬 함수 같은 Kotlin 고유의 기능을 이해합니다. 또 타입을 명시적으로 모델링하고, 동등성과 대입 호환성 같은 기본적인 타입 검사도 제공해요.

이 API는 Kotlin 문법에 따라 심볼(symbol) 수준에서 Kotlin 프로그램 구조를 모델링해요. KSP 기반 플러그인이 소스 프로그램을 처리할 때 클래스, 클래스 멤버, 함수, 관련 파라미터 같은 구조는 프로세서가 접근할 수 있지만, if 블록이나 for 루프 같은 것은 접근할 수 없습니다.

개념적으로 KSP는 Kotlin 리플렉션의 KType과 비슷해요. 이 API는 프로세서가 클래스 선언에서 특정 타입 인자가 붙은 대응 타입으로, 또는 그 반대로 탐색할 수 있게 해 줍니다. 타입 인자를 치환하고, 변성을 지정하고, 스타 프로젝션(star projection)을 적용하고, 타입의 널 가능성을 표시하는 것도 가능해요.

KSP를 Kotlin 프로그램의 전처리기(preprocessor) 프레임워크로 보는 방법도 있어요. KSP 기반 플러그인을 심볼 프로세서, 줄여서 그냥 프로세서라고 생각하면, 컴파일의 데이터 흐름은 다음과 같은 단계로 설명할 수 있습니다.

  1. 프로세서가 소스 프로그램과 리소스를 읽고 분석해요.
  2. 프로세서가 코드나 다른 형태의 출력을 생성해요.
  3. Kotlin 컴파일러가 소스 프로그램을 생성된 코드와 함께 컴파일해요.

완전한 기능을 갖춘 컴파일러 플러그인과 달리, 프로세서는 코드를 수정할 수 없어요. 언어 시맨틱을 바꾸는 컴파일러 플러그인은 때로 매우 혼란스러울 수 있는데, KSP는 소스 프로그램을 읽기 전용으로 취급해서 그 문제를 피합니다.

이 영상에서 KSP의 개요를 보실 수도 있어요.

KSP가 소스 파일을 보는 방식

대부분의 프로세서는 입력 소스 코드의 다양한 프로그램 구조를 탐색해요. API 사용법을 깊이 파기 전에, KSP의 관점에서 파일이 어떻게 보이는지 먼저 살펴볼게요:

KSFile
  packageName: KSName
  fileName: String
  annotations: List<KSAnnotation>  (File annotations)
  declarations: List<KSDeclaration>
    KSClassDeclaration // class, interface, object
      simpleName: KSName
      qualifiedName: KSName
      containingFile: String
      typeParameters: KSTypeParameter
      parentDeclaration: KSDeclaration
      classKind: ClassKind
      primaryConstructor: KSFunctionDeclaration
      superTypes: List<KSTypeReference>
      // contains inner classes, member functions, properties, etc.
      declarations: List<KSDeclaration>
    KSFunctionDeclaration // top level function
      simpleName: KSName
      qualifiedName: KSName
      containingFile: String
      typeParameters: KSTypeParameter
      parentDeclaration: KSDeclaration
      functionKind: FunctionKind
      extensionReceiver: KSTypeReference?
      returnType: KSTypeReference
      parameters: List<KSValueParameter>
      // contains local classes, local functions, local variables, etc.
      declarations: List<KSDeclaration>
    KSPropertyDeclaration // global variable
      simpleName: KSName
      qualifiedName: KSName
      containingFile: String
      typeParameters: KSTypeParameter
      parentDeclaration: KSDeclaration
      extensionReceiver: KSTypeReference?
      type: KSTypeReference
      getter: KSPropertyGetter
        returnType: KSTypeReference
      setter: KSPropertySetter
        parameter: KSValueParameter

이 관점은 파일에 선언된 흔한 것들, 즉 클래스, 함수, 프로퍼티 등을 나열한 거예요.

진입점: SymbolProcessorProvider

KSP는 SymbolProcessor를 인스턴스화하기 위해 SymbolProcessorProvider 인터페이스의 구현을 기대해요:

interface SymbolProcessorProvider {
    fun create(environment: SymbolProcessorEnvironment): SymbolProcessor
}

반면 SymbolProcessor는 다음과 같이 정의됩니다.

interface SymbolProcessor {
    fun process(resolver: Resolver): List<KSAnnotated> // Let's focus on this
    fun finish() {}
    fun onError() {}
}

ResolverSymbolProcessor에 심볼 같은 컴파일러 세부 정보에 대한 접근을 제공해요. 모든 최상위 함수와 최상위 클래스 안의 비로컬(non-local) 함수를 찾는 프로세서는 대략 다음과 같을 수 있습니다.

class HelloFunctionFinderProcessor : SymbolProcessor() {
    // ...
    val functions = mutableListOf<KSFunctionDeclaration>()
    val visitor = FindFunctionsVisitor()

    override fun process(resolver: Resolver) {
        resolver.getAllFiles().forEach { it.accept(visitor, Unit) }
    }

    inner class FindFunctionsVisitor : KSVisitorVoid() {
        override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {
            classDeclaration.getDeclaredFunctions().forEach { it.accept(this, Unit) }
        }

        override fun visitFunctionDeclaration(function: KSFunctionDeclaration, data: Unit) {
            functions.add(function)
        }

        override fun visitFile(file: KSFile, data: Unit) {
            file.declarations.forEach { it.accept(this, Unit) }
        }
    }
    // ...
    
    class Provider : SymbolProcessorProvider {
        override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor = TODO()
    }
}

지원 라이브러리

아래 표는 Android의 인기 라이브러리들과 KSP 지원 단계를 보여줘요:

Library Status
Room Officially supported
Moshi Officially supported
RxHttp Officially supported
Kotshi Officially supported
Lyricist Officially supported
Lich SavedState Officially supported
gRPC Dekorator Officially supported
EasyAdapter Officially supported
Koin Annotations Officially supported
Glide Officially supported
Micronaut Officially supported
Epoxy Officially supported
Paris Officially supported
Auto Dagger Officially supported
SealedX Officially supported
Ktorfit Officially supported
Mockative Officially supported
Kotest Officially supported
DeeplinkDispatch Supported via airbnb/DeepLinkDispatch#323
Dagger Alpha
Motif Alpha
Hilt In progress
Auto Factory Not yet supported

더 알아보기