KSP 시작하기
KSP 시작하기
이 가이드에서는 다음 내용을 배워요.
- 프로젝트에 KSP 기반 애너테이션 프로세서를 추가하는 방법.
- KSP API로 나만의 애너테이션 프로세서를 만드는 방법.
- 프로세서가 생성한 코드를 어디에서 찾을 수 있는지.
본문
프로젝트에 KSP 기반 프로세서 추가하기
프로젝트에서 외부 프로세서를 사용하려면 build.gradle(.kts) 파일의 plugins {} 블록에 KSP를 추가해요. 프로세서가 특정 모듈에서만 필요하다면 그 모듈의 build.gradle(.kts) 파일에 추가하면 됩니다.
// build.gradle.kts
plugins {
kotlin("jvm") version "2.4.20"
id("com.google.devtools.ksp") version "2.3.10"
}
// build.gradle
plugins {
id 'org.jetbrains.kotlin.jvm' version '2.4.20'
id 'com.google.devtools.ksp' version '2.3.10'
}
KSP의 최신 버전은 GitHub Releases에서 확인할 수 있어요.
최상위 dependencies {} 블록에 사용할 프로세서를 추가해요. 이 예제는 Moshi를 사용하지만, 다른 프로세서도 방법은 같아요:
// build.gradle.kts
dependencies {
ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.2")
}
// build.gradle
dependencies {
ksp 'com.squareup.moshi:moshi-kotlin-codegen:1.15.2'
}
ksp(...) 설정은 프로세서를 애플리케이션 소스에만 적용해요. 테스트 소스를 처리하려면 kspTest(...) 설정으로 프로세서를 추가하세요.
ksp(...) 설정은 단일 플랫폼 프로젝트에서만 사용할 수 있어요. 개별 Kotlin Multiplatform 타깃과 컴파일을 위한 프로세서 구성 방법은 KSP와 Kotlin Multiplatform 문서를 참고하세요.
나만의 프로세서 만들기
아래 단계를 따라가면 helloWorld() 함수를 생성하는 간단한 애너테이션 프로세서를 만들 수 있어요. 실무에서 크게 유용하진 않지만, 나만의 프로세서와 애너테이션을 만드는 기초를 보여 줍니다.
프로젝트에 KSP 추가하기
새 Kotlin 프로젝트를 만들고 KSP 플러그인을 추가해요.
- IntelliJ IDEA에서 File | New | Project를 선택해요.
- 왼쪽 목록에서 Kotlin을 고르세요.
- 빌드 시스템으로 Gradle을 선택하고 Create를 클릭해요.
build.gradle(.kts)파일에 KSP 플러그인을 추가해요:
// build.gradle.kts
plugins {
kotlin("jvm") version "2.4.20"
id("com.google.devtools.ksp") version "2.3.10" apply false
}
// build.gradle
plugins {
id 'org.jetbrains.kotlin.jvm' version '2.4.20'
id 'com.google.devtools.ksp' version '2.3.10' apply false
}
애너테이션 만들기
프로젝트 루트에 새 모듈을 만들고 애너테이션을 선언해요.
- File | New | Module을 선택해요.
- 왼쪽 목록에서 Kotlin을 선택해요.
- 다음 필드를 지정하고 create를 클릭해요:
- Name: annotations
- Build system: Gradle
- 모듈 안에
HelloWorldAnnotation.kt파일을 만들고HelloWorldAnnotation이라는 애너테이션을 선언해요:
// annotations/src/main/kotlin/com/example/annotations/HelloWorldAnnotation.kt
package com.example.annotations
annotation class HelloWorldAnnotation
프로세서 만들고 등록하기
- 프로젝트 루트에 processor라는 또 다른 모듈을 만들어요.
- 모듈의
build.gradle(.kts)파일에 KSP API와 선언한 애너테이션을 의존성으로 추가해요:
// processor/build.gradle.kts
plugins {
kotlin("jvm")
}
dependencies {
implementation(project(":annotations"))
implementation("com.google.devtools.ksp:symbol-processing-api:2.3.6")
}
// processor/build.gradle
plugins {
id 'org.jetbrains.kotlin.jvm'
}
dependencies {
implementation project ':annotations'
implementation 'com.google.devtools.ksp:symbol-processing-api:2.3.6'
}
- processor 모듈에서 새
HelloWorldProcessor.kt파일을 만들고 다음 코드를 추가해요:
// processor/src/main/kotlin/HelloWorldProcessor.kt
class HelloWorldProcessor(val codeGenerator: CodeGenerator) : SymbolProcessor {
// 1️⃣ process() function
override fun process(resolver: Resolver): List<KSAnnotated> {
resolver
.getSymbolsWithAnnotation("com.example.annotations.HelloWorldAnnotation")
.filter { it.validate() }
.filterIsInstance<KSFunctionDeclaration>()
.forEach { it.accept(HelloWorldVisitor(), Unit) }
return emptyList()
}
// 2️⃣ Visitor
inner class HelloWorldVisitor : KSVisitorVoid() {
override fun visitFunctionDeclaration(function: KSFunctionDeclaration, data: Unit) {
createNewFileFrom(function).use { file ->
file.write(
"""
fun helloWorld(): Unit {
println("Hello world from function generated by KSP")
}
""".trimIndent()
)
}
}
}
// 3️⃣ createNewFileFrom() function
private fun createNewFileFrom(function: KSFunctionDeclaration): OutputStream {
return codeGenerator.createNewFile(
dependencies = createDependencyOn(function),
packageName = "",
fileName = "GeneratedHelloWorld"
)
}
// 3️⃣ createDependencyOn() function
private fun createDependencyOn(function: KSFunctionDeclaration): Dependencies {
return Dependencies(aggregating = false, function.containingFile!!)
}
}
// Utility function for writing string to OutputStream
fun OutputStream.write(string: String): Unit {
this.write(string.toByteArray())
}
IDE가 제안하는 import를 추가해 주세요. Resolver와 Dependencies 클래스는 com.google.devtools.ksp.processing에서 가져와야 해요. 아니면 HelloWorldProcessor.kt 맨 위에 다음 줄들을 복사해 넣어도 돼요.
import com.google.devtools.ksp.processing.CodeGenerator
import com.google.devtools.ksp.processing.Dependencies
import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.symbol.KSAnnotated
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
import com.google.devtools.ksp.symbol.KSVisitorVoid
import com.google.devtools.ksp.validate
import java.io.OutputStream
코드를 한 줄씩 살펴볼게요.
-
1️⃣
process()함수는 프로세서의 핵심 로직이에요.HelloWorldAnnotation이 붙은 모든 심볼을 가져와 각각에 대해HelloWorldVisitor를 호출합니다.process()함수는 나중 라운드에서 처리할 처리되지 않은 심볼 목록을 반환해요. 이 예제에서는 안전하게emptyList()를 반환합니다. 자세한 내용은 다중 라운드 처리 문서를 참고하세요. -
2️⃣ 프로세서는 방문자(visitor)를 사용해 KSP가 바라보는 Kotlin 추상 구문 트리(AST)를 탐색해요.
HelloWorldProcessor클래스 안에서HelloWorldVisitor클래스가 바로 그 방문자예요.HelloWorldAnnotation이 함수에만 쓰이므로,visitFunctionDeclaration()만 오버라이드합니다.KSVisitorVoid는 KSP가 제공하는 방문자 클래스 중 하나로, 오버라이드해 자신에 맞게 조정할 수 있어요.KSVisitor<D, R>인터페이스를 구현해 나만의 방문자를 만들 수도 있습니다. -
3️⃣
createNewFileFrom()은 KSP가 코드를 생성하는 파일을 만들어요.createDependencyOn()은 출력 파일이 애너테이션이 사용된 소스 파일에 의존하도록 만듭니다.KSP가 파일을 만들고 관리하는 방법이 궁금하다면
CodeGenerator인터페이스의 소스 코드를 확인해 보세요.
HelloWorldProcessorProvider.kt파일을 만들어요. 그 안에SymbolProcessorProvider를 상속하는HelloWorldProcessorProvider클래스를 선언합니다.
// processor/src/main/kotlin/HelloWorldProcessorProvider.kt
import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
import com.google.devtools.ksp.processing.SymbolProcessorProvider
class HelloWorldProcessorProvider : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
return HelloWorldProcessor(environment.codeGenerator)
}
}
- 프로세서 프로바이더를 등록해요.
resources/META-INF/services디렉터리에com.google.devtools.ksp.processing.SymbolProcessorProvider파일을 만들고 프로바이더의 정규화된 이름(FQN)을 추가합니다.
## processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider
HelloWorldProcessorProvider
프로세서 사용하기
이제 프로세서를 테스트할 준비가 됐어요. 아래 단계를 따라 클라이언트 모듈을 만들고, 애너테이션이 달린 요소를 기반으로 프로세서가 코드를 생성하게 해 봅시다.
- 프로젝트 루트에
app이라는 모듈을 만들어요. - 모듈의
build.gradle(.kts)파일에서:plugins {}블록에 KSP 플러그인을 추가해요.dependencies {}블록에 프로세서와 애너테이션을 추가해요.
예를 들어:
// app/build.gradle.kts
plugins {
kotlin("jvm")
id("com.google.devtools.ksp")
}
dependencies {
implementation(project(":annotations"))
ksp(project(":processor"))
}
// app/build.gradle
plugins {
id 'com.google.devtools.ksp'
}
dependencies {
implementation project (':annotations')
ksp project (':processor')
}
- 프로젝트 레벨
settings.gradle(.kts)파일에서 모든 하위 모듈이 자동으로 포함되었는지 확인해요:
// settings.gradle.kts
include("annotations")
include("app")
include("processor")
// settings.gradle
include 'processor'
include 'annotations'
include 'app'
app모듈에서Main.kt파일을 만들고 다음 코드를 추가해요:
// app/src/main/kotlin/Main.kt
import com.example.annotations.HelloWorldAnnotation
@HelloWorldAnnotation
fun main() {
helloWorld()
}
main() 함수는 아직 존재하지도 않는 helloWorld()를 호출해요. IDE가 helloWorld()를 정의되지 않은 참조로 강조 표시하겠죠. 이건 정상이에요. 프로젝트를 빌드하고 실행하면 KSP가 helloWorld() 함수를 생성합니다.
- 프로그램을 실행해요. 콘솔에서
helloWorld()함수의 출력을 볼 수 있어요:
Hello world from function generated by KSP
KSP는 GeneratedHelloWorld.kt 파일에 코드를 생성해요:
app/build/generated/ksp/main/kotlin/GeneratedHelloWorld.kt
프로젝트 구조 살펴보기
프로젝트의 최종 파일 구조는 다음과 같아야 해요.
.
├── app
│ ├── build.gradle.kts
│ └── src
│ └── main
│ └── kotlin
│ └── Main.kt
├── annotations
│ ├── build.gradle.kts
│ └── src
│ └── main
│ └── kotlin
│ └── com
│ └── example
│ └── annotations
│ └── HelloWorldAnnotation.kt
├── processor
│ ├── build.gradle.kts
│ └── src
│ └── main
│ ├── kotlin
│ │ ├── HelloWorldProcessor.kt
│ │ └── HelloWorldProcessorProvider.kt
│ └── resources/META-INF/services
│ └── com.google.devtools.ksp.processing.SymbolProcessorProvider
├── build.gradle.kts
└── settings.gradle.kts
다른 파일이나 디렉터리가 추가로 있을 수도 있어요.
다음 단계는?
- KSP 저장소에서 이 예제의 전체 코드를 살펴보세요.
- KSP 저장소에서 더 복잡하고 실제적인 예제를 찾아보세요.
- KSP 지원 라이브러리 목록을 둘러보세요.