KSP 시작하기

KSP 시작하기

이 가이드에서는 다음 내용을 배워요.

  • 프로젝트에 KSP 기반 애너테이션 프로세서를 추가하는 방법.
  • KSP API로 나만의 애너테이션 프로세서를 만드는 방법.
  • 프로세서가 생성한 코드를 어디에서 찾을 수 있는지.

출처: Getting started with KSP

본문

프로젝트에 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 플러그인을 추가해요.

  1. IntelliJ IDEA에서 File | New | Project를 선택해요.
  2. 왼쪽 목록에서 Kotlin을 고르세요.
  3. 빌드 시스템으로 Gradle을 선택하고 Create를 클릭해요.
  4. 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
}

애너테이션 만들기

프로젝트 루트에 새 모듈을 만들고 애너테이션을 선언해요.

  1. File | New | Module을 선택해요.
  2. 왼쪽 목록에서 Kotlin을 선택해요.
  3. 다음 필드를 지정하고 create를 클릭해요:
    • Name: annotations
    • Build system: Gradle
  4. 모듈 안에 HelloWorldAnnotation.kt 파일을 만들고 HelloWorldAnnotation이라는 애너테이션을 선언해요:
// annotations/src/main/kotlin/com/example/annotations/HelloWorldAnnotation.kt

package com.example.annotations

annotation class HelloWorldAnnotation

프로세서 만들고 등록하기

  1. 프로젝트 루트에 processor라는 또 다른 모듈을 만들어요.
  2. 모듈의 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'
}
  1. 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를 추가해 주세요. ResolverDependencies 클래스는 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 인터페이스의 소스 코드를 확인해 보세요.

  1. 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)  
    }  
}
  1. 프로세서 프로바이더를 등록해요. 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

프로세서 사용하기

이제 프로세서를 테스트할 준비가 됐어요. 아래 단계를 따라 클라이언트 모듈을 만들고, 애너테이션이 달린 요소를 기반으로 프로세서가 코드를 생성하게 해 봅시다.

  1. 프로젝트 루트에 app이라는 모듈을 만들어요.
  2. 모듈의 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')
}
  1. 프로젝트 레벨 settings.gradle(.kts) 파일에서 모든 하위 모듈이 자동으로 포함되었는지 확인해요:
// settings.gradle.kts

include("annotations")
include("app")
include("processor")
// settings.gradle

include 'processor'
include 'annotations'
include 'app'
  1. app 모듈에서 Main.kt 파일을 만들고 다음 코드를 추가해요:
// app/src/main/kotlin/Main.kt

import com.example.annotations.HelloWorldAnnotation

@HelloWorldAnnotation
fun main() {
    helloWorld()
}

main() 함수는 아직 존재하지도 않는 helloWorld()를 호출해요. IDE가 helloWorld()를 정의되지 않은 참조로 강조 표시하겠죠. 이건 정상이에요. 프로젝트를 빌드하고 실행하면 KSP가 helloWorld() 함수를 생성합니다.

  1. 프로그램을 실행해요. 콘솔에서 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 지원 라이브러리 목록을 둘러보세요.

더 알아보기