KSP 란 ?
KSP (Kotlin Symbol Processing) 은 경량 컴파일러 플러그인을 만드는데 사용할 수 있는 API 이다.
왜 KSP 인가요 ?
Kotlin 은 자체적으로 Annotation Processing 이 불가능했습니다.
Java 의 Annotation Processing 을 사용하려면 KAPT 의 도움을 받아야 했습니다.
KSP 는 설계 시점부터 kotlin 단에서 어노테이션 프로세싱이 어떻게 동작할지 고려했습니다.
그 결과 KSP 가 KAPT 보다 약 2배정도 빠르게 동작합니다.
무엇보다 kapt의 기능 개발이 중지되었습니다.
출처 : https://kotlinlang.org/docs/ksp-overview.html
구현하는 방법
1. SymbolProcessorProvider.create 를 통해 SymbolProcessor 를 생성합니다.
2. 메인로직은 SymbolProcessor.process 에 작성합니다.
3. Resolver.getSymbolsWithAnnotation 을 통해 어떤 어노테이션을 처리할지 결정합니다. fully-qualified name 을 인자로 건내줘야합니다.
4. 보통은 KSVisitor 를 구현한 custom Visitor 를 이용합니다.
5. 프로세스 작성이 끝났다면 아래위치에 정규화된 이름을 포함하여 프로세서 공급자를 패키지에 등록합니다.
resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider
예제
먼저 Annotation Processor 를 만들기 위한 Processor 모듈과 Annotation Processor 를 적용시키기 위한 builder-apply 를 멀티모듈로 구성합니다.
processor 모듈의 작업을 시작해보겠습니다.
메인로직을 실행시키기 위한 SymbolProcessor 를 작성해줍니다.
여기서는 간단하게 Builder Annotation 이 붙어있으면 -Builder 라는 클래스를 생성해주는 로직을 작성해보겠습니다.
fun OutputStream.appendText(str: String) {
this.write(str.toByteArray())
}
class BuilderProcessor(
val codeGenerator: CodeGenerator,
val logger: KSPLogger
) : SymbolProcessor {
override fun process(resolver: Resolver): List<KSAnnotated> {
val symbols = resolver.getSymbolsWithAnnotation("com.example.annotation.Builder")
val ret = symbols.filter { !it.validate() }.toList()
symbols
.filter { it is KSClassDeclaration && it.validate() }
.forEach { it.accept(BuilderVisitor(), Unit) }
return ret
}
inner class BuilderVisitor : KSVisitorVoid() {
override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {
classDeclaration.primaryConstructor!!.accept(this, data)
}
override fun visitFunctionDeclaration(function: KSFunctionDeclaration, data: Unit) {
val parent = function.parentDeclaration as KSClassDeclaration
val packageName = parent.containingFile!!.packageName.asString()
val className = "${parent.simpleName.asString()}Builder"
val file = codeGenerator.createNewFile(Dependencies(true, function.containingFile!!), packageName , className)
file.appendText("package $packageName\n\n")
file.appendText("class $className\n")
file.close()
}
}
}
위에서 설명했던대로 SymbolProcessor 를 생성하기 위한 SymbolProcessorProvider 를 작성해줍니다.
class BuilderProcessorProvider : SymbolProcessorProvider {
override fun create(
environment: SymbolProcessorEnvironment
): SymbolProcessor {
return BuilderProcessor(environment.codeGenerator, environment.logger)
}
}
Process, ProcessProvider, Visitor 의 작성이 모두 끝났으므로 프로세서 공급자를 패키지에 등록합니다.
이를 적용시키기위한 builder-apply 을 작업합니다.
Builder Annotation 을 붙인 클래스를 작성합니다.
package com.example
import com.example.annotation.Builder
@Builder
class AClass(
private val a: Int,
val b: String,
val c: Double,
) {
val p = "$a, $b, $c"
fun foo() = p
}
builder-apply 모듈을 빌드하면 아래와 같이 AClassBuilder 가 생성되는것을 확인할 수 있습니다.
Processor 를 디버깅하는것은 일반 프로그램을 디버깅하는것과는 조금 다릅니다.
디버깅 하는 방법은 아래 게시글을 참조바랍니다.
https://csy7792.tistory.com/354
활용케이스
특정케이스에 반복적으로 어떠한 파일들을 생성한다면 어노테이션을 만들고 자동으로 생성하도록 할 수 있을듯합니다.
하지만 KSP 에는 한계점이 존재합니다.
한계점을 잘 인지하고 적절하게 사용하는것이 중요합니다.
- 소스코드 수정이 불가능합니다.
- JAVA Annotation Processing API 와 100% 호환되지 않습니다.
- IDE 에서 KSP 를 통해 생성된 코드를 알 수 없습니다. (추후에는 되지 않을까요 ?)
- expression-level information of source code 를 조사할 수 없습니다.
댓글