Kotlin Symbol Processing API
Kotlin Symbol Processing API
Kotlin Symbol Processing(KSP)은 Kotlin을 위한 소스 코드 생성 프레임워크예요. KSP 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 기반 플러그인을 심볼 프로세서, 줄여서 그냥 프로세서라고 생각하면, 컴파일의 데이터 흐름은 다음과 같은 단계로 설명할 수 있습니다.
- 프로세서가 소스 프로그램과 리소스를 읽고 분석해요.
- 프로세서가 코드나 다른 형태의 출력을 생성해요.
- 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() {}
}
Resolver는 SymbolProcessor에 심볼 같은 컴파일러 세부 정보에 대한 접근을 제공해요. 모든 최상위 함수와 최상위 클래스 안의 비로컬(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 |