왜 KSP인가
컴파일러 플러그인은 코드 작성 방식을 크게 향상시킬 수 있는 강력한 메타프로그래밍 도구입니다. 컴파일러 플러그인은 컴파일러를 라이브러리로서 직접 호출하여 입력 프로그램을 분석하고 수정합니다. 이러한 플러그인은 또한 다양한 용도로 출력을 생성할 수 있습니다. 예를 들어, 보일러플레이트(boilerplate) 코드를 생성할 수 있으며, Parcelable과 같이 특별히 표시된 프로그램 요소에 대해 전체 구현을 생성할 수도 있습니다. 플러그인은 이외에도 다양한 용도로 사용되며, 언어에서 직접 제공하지 않는 기능을 구현하고 미세 조정하는 데에도 사용될 수 있습니다.
컴파일러 플러그인은 강력하지만, 그 대가가 따릅니다. 아주 간단한 플러그인을 작성하려 해도 컴파일러에 대한 배경지식은 물론, 특정 컴파일러의 구현 세부 사항에 대한 어느 정도의 숙련도가 필요합니다. 또 다른 현실적인 문제는 플러그인이 특정 컴파일러 버전에 밀접하게 연결되는 경우가 많다는 점입니다. 즉, 더 새로운 버전의 컴파일러를 지원하고자 할 때마다 플러그인을 업데이트해야 할 수도 있습니다.
KSP는 경량 컴파일러 플러그인 제작을 더 쉽게 만듭니다
KSP는 컴파일러의 변경 사항을 숨기도록 설계되어, 이를 사용하는 프로세서의 유지 관리 노력을 최소화합니다. KSP는 JVM에 종속되지 않도록 설계되어 향후 다른 플랫폼에 더 쉽게 적응할 수 있습니다. 또한 KSP는 빌드 시간을 최소화하도록 설계되었습니다. Glide와 같은 일부 프로세서의 경우, KSP를 사용하면 kapt와 비교했을 때 전체 컴파일 시간이 최대 25%까지 단축됩니다.
KSP 자체도 컴파일러 플러그인으로 구현되어 있습니다. Google의 Maven 저장소에는 미리 빌드된 패키지가 있으므로, 프로젝트를 직접 빌드할 필요 없이 다운로드하여 사용할 수 있습니다.
kotlinc 컴파일러 플러그인과의 비교
kotlinc 컴파일러 플러그인은 컴파일러의 거의 모든 것에 접근할 수 있으므로 최대의 권한과 유연성을 가집니다. 반면에 이러한 플러그인은 컴파일러의 모든 요소에 의존할 가능성이 있기 때문에 컴파일러 변경에 민감하며 빈번한 유지 관리가 필요합니다. 또한 이러한 플러그인은 kotlinc 구현에 대한 깊은 이해가 필요하므로 학습 곡선이 가파를 수 있습니다.
KSP는 잘 정의된 API를 통해 대부분의 컴파일러 변경 사항을 숨기는 것을 목표로 하지만, 컴파일러나 Kotlin 언어 자체의 중대한 변경 사항은 여전히 API 사용자에게 노출되어야 할 수도 있습니다.
KSP는 강력함 대신 단순함을 택한 API를 제공함으로써 일반적인 사용 사례를 충족하고자 합니다. KSP의 기능은 일반적인 kotlinc 플러그인 기능의 엄격한 부분 집합(subset)입니다. 예를 들어, kotlinc는 표현식(expression)과 문(statement)을 검사하고 코드를 수정할 수도 있지만, KSP는 이를 수행할 수 없습니다.
kotlinc 플러그인을 작성하는 것도 매우 즐거운 일일 수 있지만, 많은 시간이 소요될 수 있습니다. kotlinc의 구현을 학습할 여건이 되지 않거나 소스 코드를 수정하거나 표현식을 읽을 필요가 없다면 KSP가 적합할 수 있습니다.
리플렉션과의 비교
KSP의 API는 kotlin.reflect와 비슷해 보입니다. 둘 사이의 주요 차이점은 KSP의 타입 참조(type reference)는 명시적으로 분석(resolve)되어야 한다는 점입니다. 이것이 인터페이스를 공유하지 않는 이유 중 하나입니다.
kapt와의 비교
kapt는 수많은 Java 어노테이션 프로세서를 Kotlin 프로그램에서 즉시 사용할 수 있게 해주는 훌륭한 솔루션입니다. kapt와 비교했을 때 KSP의 주요 장점은 개선된 빌드 성능, JVM에 종속되지 않음, 더 자연스러운(idiomatic) Kotlin API, 그리고 Kotlin 전용 심볼을 이해하는 능력입니다.
수정되지 않은 Java 어노테이션 프로세서를 실행하기 위해, kapt는 Kotlin 코드를 Java 어노테이션 프로세서가 처리할 수 있는 정보를 담은 Java 스텁(stub)으로 컴파일합니다. 이러한 스텁을 생성하기 위해 kapt는 Kotlin 프로그램의 모든 심볼을 분석해야 합니다. 스텁 생성 비용은 전체 kotlinc 분석 시간의 약 1/3, 그리고 kotlinc 코드 생성 시간과 비슷한 수준의 비용이 듭니다. 많은 어노테이션 프로세서의 경우, 이는 프로세서 자체에서 소비하는 시간보다 훨씬 깁니다. 예를 들어, Glide는 미리 정의된 어노테이션이 있는 매우 제한된 수의 클래스만 살펴보며 코드 생성도 상당히 빠릅니다. 빌드 오버헤드의 거의 대부분은 스텁 생성 단계에서 발생합니다. KSP로 전환하면 컴파일러에서 소요되는 시간을 즉시 25% 줄일 수 있습니다.
성능 평가를 위해, Tachiyomi 프로젝트용 코드를 생성하도록 KSP로 Glide의 단순화된 버전을 구현했습니다. 테스트 기기에서 프로젝트의 전체 Kotlin 컴파일 시간은 21.55초였으나, kapt가 코드를 생성하는 데 8.67초가 걸린 반면, KSP 구현이 코드를 생성하는 데는 1.15초가 걸렸습니다.
kapt와 달리, KSP의 프로세서는 Java의 관점에서 입력 프로그램을 바라보지 않습니다. API는 특히 최상위 함수(top-level functions)와 같은 Kotlin 전용 기능에 대해 Kotlin에 더 자연스럽습니다. KSP는 kapt처럼 javac에 위임하지 않으므로 JVM 전용 동작을 가정하지 않으며 잠재적으로 다른 플랫폼에서도 사용될 수 있습니다.
제한 사항
KSP는 대부분의 일반적인 사용 사례에 대해 간단한 솔루션이 되고자 노력하지만, 다른 플러그인 솔루션과 비교했을 때 몇 가지 절충안을 두었습니다. 다음은 KSP의 목표가 아닙니다:
- 소스 코드의 표현식 수준(expression-level) 정보 검사.
- 소스 코드 수정.
- Java 어노테이션 처리 API(Java Annotation Processing API)와의 100% 호환성.
