Kotlin 심볼 프로세싱 API
Kotlin Symbol Processing(KSP)은 코틀린을 위한 소스 코드 생성 프레임워크입니다. KSP API를 사용하면 소스 코드의 어노테이션을 기반으로 코드를 생성하는 프로세서를 만들 수 있습니다.
KSP는 경량 컴파일러 플러그인을 더 쉽게 만들 수 있도록 하는 것을 목표로 합니다. 잘 정의된 API는 컴파일러의 변경 사항을 숨겨주므로, 프로세서 유지 보수에 큰 노력을 들일 필요가 없습니다. 하지만 이 방식에는 트레이드오프가 있습니다. 예를 들어, KSP 기반 프로세서는 표현식(expressions)이나 문(statements)을 검사할 수 없으며, 소스 코드를 수정할 수 없습니다.
KSP 기반 플러그인의 전형적인 사례는 다음과 같습니다:
첫 번째 KSP 기반 프로세서를 만드는 방법은 KSP 빠른 시작을 참고하세요.
개요
KSP API는 코틀린 프로그램을 관용적(idiomatically)으로 처리합니다. KSP는 확장 함수(extension functions), 선언 지점 변성(declaration-site variance), 로컬 함수와 같은 코틀린 특유의 기능을 이해합니다. 또한 타입을 명시적으로 모델링하며, 동등성(equivalence) 및 할당 호환성(assign-compatibility)과 같은 기본적인 타입 검사 기능을 제공합니다.
이 API는 코틀린 문법에 따라 심볼 수준에서 코틀린 프로그램 구조를 모델링합니다. KSP 기반 플러그인이 소스 프로그램을 처리할 때 클래스, 클래스 멤버, 함수 및 관련 파라미터와 같은 구조체에는 프로세서가 접근할 수 있지만, if 블록이나 for 루프와 같은 요소에는 접근할 수 없습니다.
개념적으로 KSP는 코틀린 리플렉션의 KType과 유사합니다. 이 API를 통해 프로세서는 클래스 선언에서 특정 타입 인자가 포함된 해당 타입으로 이동하거나 그 반대로 이동할 수 있습니다. 또한 타입 인자 대체, 변성 지정, 스타 프로젝션(star projections) 적용, 타입의 널 가능성(nullability) 표시 등도 가능합니다.
KSP를 바라보는 또 다른 관점은 코틀린 프로그램의 전처리기(preprocessor) 프레임워크로 생각하는 것입니다. KSP 기반 플러그인을 심볼 프로세서(symbol processors), 또는 단순히 _프로세서_라고 할 때, 컴파일 과정에서의 데이터 흐름은 다음과 같은 단계로 설명할 수 있습니다:
- 프로세서가 소스 프로그램과 리소스를 읽고 분석합니다.
- 프로세서가 코드나 다른 형태의 출력을 생성합니다.
- 코틀린 컴파일러가 소스 프로그램과 함께 생성된 코드를 컴파일합니다.
일반적인 컴파일러 플러그인과 달리 프로세서는 코드를 수정할 수 없습니다. 언어의 의미론(semantics)을 변경하는 컴파일러 플러그인은 때로 매우 혼란스러울 수 있습니다. KSP는 소스 프로그램을 읽기 전용으로 취급하여 이를 방지합니다.
이 영상에서도 KSP에 대한 개요를 확인할 수 있습니다:
KSP가 소스 파일을 바라보는 방식
대부분의 프로세서는 입력 소스 코드의 다양한 프로그램 구조를 탐색합니다. API 사용법을 살펴보기 전에, KSP의 관점에서 파일이 어떻게 보이는지 확인해 보겠습니다:
KSFile
packageName: KSName
fileName: String
annotations: List<KSAnnotation> (파일 어노테이션)
declarations: List<KSDeclaration>
KSClassDeclaration // 클래스, 인터페이스, 객체(object)
simpleName: KSName
qualifiedName: KSName
containingFile: String
typeParameters: KSTypeParameter
parentDeclaration: KSDeclaration
classKind: ClassKind
primaryConstructor: KSFunctionDeclaration
superTypes: List<KSTypeReference>
// 내부 클래스, 멤버 함수, 프로퍼티 등을 포함
declarations: List<KSDeclaration>
KSFunctionDeclaration // 최상위 함수
simpleName: KSName
qualifiedName: KSName
containingFile: String
typeParameters: KSTypeParameter
parentDeclaration: KSDeclaration
functionKind: FunctionKind
extensionReceiver: KSTypeReference?
returnType: KSTypeReference
parameters: List<KSValueParameter>
// 로컬 클래스, 로컬 함수, 로컬 변수 등을 포함
declarations: List<KSDeclaration>
KSPropertyDeclaration // 전역 변수
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> // 여기에 집중해 봅시다
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()
}
}리소스
- 빠른 시작
- 왜 KSP를 사용하는가?
- 예제
- KSP가 코틀린 코드를 모델링하는 방법
- 자바 어노테이션 프로세서 작성자를 위한 참조 가이드
- 증분 처리(Incremental processing) 참고 사항
- 다중 라운드 처리(Multiple round processing) 참고 사항
- 멀티플랫폼 프로젝트에서의 KSP
- 커맨드 라인에서 KSP 실행하기
- 자주 묻는 질문(FAQ)
지원되는 라이브러리
다음 표는 안드로이드에서 널리 사용되는 라이브러리들과 각 라이브러리의 KSP 지원 현황입니다:
| 라이브러리 | 상태 |
|---|---|
| Room | 공식 지원됨 |
| Moshi | 공식 지원됨 |
| RxHttp | 공식 지원됨 |
| Kotshi | 공식 지원됨 |
| Lyricist | 공식 지원됨 |
| Lich SavedState | 공식 지원됨 |
| gRPC Dekorator | 공식 지원됨 |
| EasyAdapter | 공식 지원됨 |
| Koin Annotations | 공식 지원됨 |
| Glide | 공식 지원됨 |
| Micronaut | 공식 지원됨 |
| Epoxy | 공식 지원됨 |
| Paris | 공식 지원됨 |
| Auto Dagger | 공식 지원됨 |
| SealedX | 공식 지원됨 |
| Ktorfit | 공식 지원됨 |
| Mockative | 공식 지원됨 |
| Kotest | 공식 지원됨 |
| DeeplinkDispatch | airbnb/DeepLinkDispatch#323을 통해 지원됨 |
| Dagger | 알파 |
| Motif | 알파 |
| Hilt | 진행 중 |
| Auto Factory | 아직 지원되지 않음 |
