본문 바로가기
개발/Kotlin

KSP 란 ?

by 상용최 2022. 3. 6.
반응형

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

 

Kotlin Symbol Processing API | Kotlin

 

kotlinlang.org

 

구현하는 방법

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

 

IntelliJ 를 활용하여 KSP(Kotlin Symbol Processing API) 디버깅 하는방법

Terminal 을 이용하여 아래 명령어를 실행합니다. ./gradlew build --no-daemon -Dorg.gradle.debug=true -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.daemon.jvm.options="-Xdebug,-Xrunjdwp:tran..

csy7792.tistory.com

 

활용케이스

특정케이스에 반복적으로 어떠한 파일들을 생성한다면 어노테이션을 만들고 자동으로 생성하도록 할 수 있을듯합니다.

 

하지만 KSP 에는 한계점이 존재합니다.

한계점을 잘 인지하고 적절하게 사용하는것이 중요합니다.

  • 소스코드 수정이 불가능합니다.
  • JAVA Annotation Processing API 와 100% 호환되지 않습니다.
  • IDE 에서 KSP 를 통해 생성된 코드를 알 수 없습니다. (추후에는 되지 않을까요 ?)
  • expression-level information of source code 를 조사할 수 없습니다.
반응형

댓글