Slide 1

Slide 1 text

Kotlin KSP 활용하기 KSP로 뭘 할까? taehwan

Slide 2

Slide 2 text

KSP? Kotlin Symbol Processing API ● 기존 Java 기반의 kapt 대용 ● kapt 대비 2배 빠른 속도 ● 장점 > code generate / 단점 > 리빌드 필요 ● Google open source project ● Apache-2.0 license 코드를 자동으로 코드를 만들어주긴 하지만 리빌드가 필요. 리빌드를 최소화시키는 구조르 KSP 개발이 필요

Slide 3

Slide 3 text

KSP 버전 정보 ● KSP의 버전은 Kotlin 버전에 밀접하다. ● Kotlin Version - KSP Version ● 현재 기준 1.0.5 최신 버전 적용은 3개를 배포했다. ○ 1.6.20-1.0.5 ○ 1.6.21-1.0.5 ○ 1.7.0-Beta-1.0.5 KSP는 Kotlin 버전에 KSP 버전을 배포하는데, 현재 최신 버전은 Kotlin 1.6.21에 KSP 1.0.5 버전입니다. 사용 시 사용하는 Kotlin 버전 - KSP 버전 매칭이 필요합니다.

Slide 4

Slide 4 text

KSP 어디서 활용할까? KSP는 어디에서 사용하고 있을까요?

Slide 5

Slide 5 text

Room | Moshi Room/Moshi에서 사용하고 있고, 아직 변환 전이거나, 변환 예정인 SDK 목록은 KSP 사이트에서 확인할 수 있습니다.

Slide 6

Slide 6 text

CPU에게 일을 시키거나 KSP를 활용하는 것은 직접 코딩하는 것이 아닌 CPU에게 일하도록 시키는 작업입니다.

Slide 7

Slide 7 text

사용 예 오픈 소스를 통해 KSP 사용법을 살펴보겠습니다.

Slide 8

Slide 8 text

KSP - Compose @ProvidedComposeLocal GitHub - android-alatan/LifecycleComponents 포함된 _lifecycle-handler/_compose/provided-compose-local-ksp/ 경로 위치 정승욱 님이 배포한 android-alatan에 포함되어 있는 compose local ksp를 통해 KSP 활용 방법을 습득해 보겠습니다.

Slide 9

Slide 9 text

KSP - Compose @ProvidedComposeLocal - 목적 ● Android Compose의 CompositionLocalProvider에 viewModel 등록 ● ComposeView 전에 ComposeLocalProvider {}로 시작 ● 파라메터로 전달하던 viewModel을 위치와 관계없이 viewModel() 사용 가능 fun View() { val data = viewModel().data ... } 이 모듈의 목적은 Compose의 CompositionLocalProvider에 ViewModel을 등록하고, 위치와 관계없이 viewModel() 함수로 CompositionLocalProvider에 등록된 viewModel을 활용할 수 있도록 만들어주는 코드를 KSP를 활용하여 자동으로 만들어주게 됩니다.

Slide 10

Slide 10 text

KSP - Compose - 직접 구현한다면? private val LocalComposeSampleViewModel: ProvidableCompositionLocal = compositionLocalOf { error("LocalComposeSampleViewModel isn't provided") } internal fun viewModelProviderValue(viewModel: ComposeSampleViewModel) = LocalComposeSampleViewModel provides viewModel @Composable internal fun viewModel() = LocalComposeSampleViewModel.current 먼저 직접 구현할 수 있어야 KSP로 자동화 시킬 수 있는데, 이와 같은 형태의 코드를 자동화 시켜줍니다. 여기서는 2개의 파일이 만들어지는데, Composition을 등록하는 부분

Slide 11

Slide 11 text

KSP - Compose - 직접 구현한다면? 사용하는 위치에 아래 코드를 등록하고, 사용 CompositionLocalProvider(viewModelProviderValue(viewModel)) {} 또는 만들어 사용 internal fun ComposeLifecycleSampleActivity.ComposeLocalProvider(content: @Composable () -> Unit) { CompositionLocalProvider(viewModelProviderValue(viewModel), content = content) } 이 코드는 View에서 활용할 부분의 코드입니다. 이 코드 호출 시 Composition을 등록하고, 사용할 수 있는 준비까지 모두 마치게 됩니다. 직접 만들어 사용하면 동일한 코드가 수십 개 만들어질 수 있지만, 이를 KSP 활용 시 자동으로 만들 수 있습니다.

Slide 12

Slide 12 text

KSP - Compose 구현체는 https://github.com/android-alatan/LifecycleComponents/blob/main/_lifecycle- handler/_compose/provided-compose-local-ksp/src/main/java/io/androidalata n/compose/local/ksp/ComposeLocalProcessor.kt 코드 자세한 내용은 위 링크를 통해 확인할 수 있습니다.

Slide 13

Slide 13 text

직접 만들어보자

Slide 14

Slide 14 text

작업 순서? 1. KSP 모듈 추가 2. 초기화 코드 작성 3. Target 할 Annotation 정의 4. 사용할 모듈에 build.gradle 적용하고, Annotation 정의 5. Annotation을 찾고 6. 생성할 코드 정의 7. 예외 처리 이 발표에서 진행하기 위한 작업 순서를 정해보았습니다. 실제 적용과는 차이가 있으니 참고만 하세요

Slide 15

Slide 15 text

KSP Module 추가 plugins { kotlin("jvm") } dependencies { implementation("org.jetbrains.kotlin:kotlin-stdlib:KotlinVersion") implementation("com.google.devtools.ksp:symbol-processing-api:KotlinVersion-KSPVers ion") } Releases · google/ksp · GitHub 먼저 KSP 모듈을 추가합니다. KSP 개발할 부분에 이 코드를 추가해 주면 됩니다. KSP 모듈은 JVM 기반의 코드로 작성하면 됩니다.

Slide 16

Slide 16 text

Square - kotlinpoet도 추가 KSP Generate 쉽게 도와주는 kotlinpoet 추가 implementation("com.squareup:kotlinpoet:LastVersion") KotlinPoet KSP만을 가지고 실제 적용 코드 작성은 복잡할 수 있습니다. 다행히도 Square에서 Kotlin poet을 배포하고 있고, 이를 활용하면 위 코드와 같이 코드 그대로 출력도 가능하고, 클래스, function, property 형태의 코드 만드는 걸 편하게 활용할 수 있습니다.

Slide 17

Slide 17 text

먼저 알아보자 ● SymbolProcessorProvider - KSP에서 실행 ○ KSP의 SymbolProcessor 객체 생성과 options, codeGenerator, logger 접근이 가능 ○ options : KSP에 arg 지정 정보를 가져올 수 있다. ○ codeGenerator : 파일 생성을 위한 generator ○ logger : KSP 진행 중 메시지, error, exception 등을 출력할 수 있다. ● SymbolProcessor - 내부 구현 ○ process(resolver: Resolver): List : KSP의 시작점 ○ finish() : KSP의 종료 지점 KSP는 크게 2개의 상속을 받아 구현해야 합니다. SymbolProcessorProvider는 KSP에서 실행을 하기 위해 상속 구현해 줘야 합니다. SymbolProcessor는 저희가 내부 구현을 할 부분입니다. 이 클래스는 process를 구현해 주고, finish 처리해 줄 수 있습니다.

Slide 18

Slide 18 text

(중요) KSP ProcessorProvider 지정 ● resources/META-INF/services 폴더를 생성 ● com.google.devtools.ksp.processing.SymbolProcessorProvider 파일 생성 ● SymbolProcessorProvider 상속 받은 class 위치를 알려준다. ○ ex) tech.thdev.ksp.sample.SampleProcessorProvider KSP 실행을 위해 services 등록을 해줘야 합니다. KSP 모듈의 하위 main 폴더 안에 resources 폴더를 생성하고 위와 같이 파일을 추가해 줍니다. 마지막으로 SymbolProcessorProvider를 상속받는 클래스를 해당 파일에 명시해 주면 됩니다.

Slide 19

Slide 19 text

Annotation 추가 @Retention(AnnotationRetention.SOURCE) @Target(AnnotationTarget.CLASS) annotation class GenerateAnnotation 작업을 위한 Annotation을 하나 만들어줘야 합니다. 이 어노테이션을 찾아서 필요한 코드를 작성할 수 있습니다.

Slide 20

Slide 20 text

Test 가능한 환경 구축 - build.gradle buildscript { dependencies { classpath("com.android.tools.build:gradle:7.2.0") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.21") classpath("com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:1.6 .21-1.0.5") } } KSP 개발하기 위한 구조는 만들었으니, 이제 실제 테스트 및 사용이 가능한 환경을 구축해야 합니다. 먼저 root의 build.gradle에 devtools을 추가합니다.

Slide 21

Slide 21 text

Test 가능한 환경 구축 - 모듈.build.gradle plugins { id("com.android.application") // 또는 id("com.android.library") id("com.google.devtools.ksp") } android { sourceSets.create("debug") { kotlin.srcDir("build/generated/ksp/debug/kotlin") } sourceSets.create("release") { kotlin.srcDir("build/generated/ksp/release/kotlin") } } dependencies { ksp(project(":ksp-sample")) } 안드로이드 build.gradle.kts에서 Flavor 나눠진 경우 KSP 빌드 폴더를 flavor 별 sourceSets 설정 방법 사용할 모듈에 KSP에서 빌드 한 결과물에 대한 위치를 지정해 주면 되겠습니다. 마지막으로 ksp로 시작하는 import를 처리해 주면 되겠습니다.

Slide 22

Slide 22 text

Test 가능한 환경 구축 - Annotation 적용 @GenerateAnnotation class SecondActivity : AppCompatActivity() 테스트할 activity를 하나 추가하고, 여기에서 GenerateAnnotaiton을 추가해 줍니다.

Slide 23

Slide 23 text

SymbolProcessor에서 Annotation을 찾아보자 Annotation을 찾고, 이에 대한 로그를 출력해 보겠습니다. getSymbolWithAnnotation() 함수를 통해 모듈에 적용한 Annotation을 찾고, 이 클래스의 정보를 받아올 수 있습니다. 클래스에 정의한 함수와 클래스에 정의한 property 정보를 모두 불러올 수 있습니다.

Slide 24

Slide 24 text

동작 로그 확인 ● w: [ksp] findKSAnnotated SecondActivity ● w: [ksp] Function onCreate ● w: [ksp] Function ● w: [ksp] Properties binding Annotation 하나만을 가지고, 몇 가지 출력한 결과는 위와 같습니다.

Slide 25

Slide 25 text

자동으로 만들어줄 코드는? 그럼 어떤 코드를 만들어볼까요? showXXXActivity() 함수를 자동으로 만들어보겠습니다.

Slide 26

Slide 26 text

코드 그대로 파일로 출력해 보자 파일이 잘 출력되고, 원하는 코드가 잘 나오는지를 먼저 확인할 필요성이 있습니다. 먼저 KSP를 활용은 하지만, 코드에 대한 generate는 동적이 아닌 정적으로 만들어보겠습니다.

Slide 27

Slide 27 text

코드 그대로 파일로 출력해 보자 여기서는 kotlinpoet을 활용해 파일을 생성하는 것까지 진행합니다. 먼저 패키지와 새로운 파일 이름을 만들어주고, context를 kotlin extensions 형태로 만들었으니, receiver() 에 context를 추가합니다.

Slide 28

Slide 28 text

코드 그대로 파일로 출력해 보자 마지막으로 파일을 출력해 주면 됩니다.

Slide 29

Slide 29 text

코드 그대로 파일로 출력해 보자 코드 동작에 문제가 없다면 모듈 위치의 build/generated/ksp/debug(또는 release) 아래에 지정한 패키지 경로와 파일이 만들어짐을 확인할 수 있습니다. 만약 정상적이라면 오른쪽과 같이 파일까지 모두 출력됩니다.(정렬은 무시하세요)

Slide 30

Slide 30 text

문제 1 하지만 이 코드는 문제가 있습니다. 문제를 하나씩 파악해 보죠

Slide 31

Slide 31 text

문제 1 여기서 붉은 부분이 보이시나요? Intent가 import 안 되어있음을 확인할 수 있습니다.

Slide 32

Slide 32 text

해결 - Import 가 없으니 추가한다 file.addImport("android.content", "Intent") 해결 방법은 간단합니다. 앞에서 작성한 코드에 import 하나 추가하면 됩니다.

Slide 33

Slide 33 text

문제 2 2번째 문제를 찾아보겠습니다.

Slide 34

Slide 34 text

문제 2 2번째 문제로 이 코드에 있습니다. 샘플로 작성했던 코드를 그대로 활용했기 때문에 @GenerateAnnotation을 몇 개를 부르던 모든 코드에서 SampleActivity라는 이름의 함수를 만들게 됩니다. 그럼 공용성이 없고, 그냥 만들기만 할 뿐…

Slide 35

Slide 35 text

해결 - SampleActivity 전용이므로 수정 필요 다행히 Kotlin poet에서는 string에서 % 형태의 코드 작성이 가능합니다. 이 위치에 simpleName을 그대로 넣고 코드를 완성할 수 있습니다. 여기서는 %L을 활용해 완전한 형태의 코드 작성을 완료합니다.

Slide 36

Slide 36 text

문제 3 마지막 하나만 더 보겠습니다. 문제 3

Slide 37

Slide 37 text

문제 3 문제 3은 실제 Activity가 아니더라도 코드를 만들어준다는 것입니다. Activity 일 때만 활용되어야 하니 Activity 인지 체크가 필요합니다.

Slide 38

Slide 38 text

해결 - filter 현재 클래스의 상속을 파악하고, 상속된 클래스 정보가 AppCompatActivity 상속받을 경우에 만 동작하도록 만들면 됩니다.

Slide 39

Slide 39 text

오류 발생시켜 진행을 막자 앞에는 코드 동작에 초점을 맞췄습니다. 하지만 하지 않아야 할 경우도 있습니다.

Slide 40

Slide 40 text

error를 내보자 if (targetClass.not()) { logger.error( "No activity class!!! ${ksClassDeclaration.simpleName.asString()} Remove @GenerateAnnotation", ksClassDeclaration ) } KSP logger를 활용해 error를 만들 수 있지만, 마지막의 클래스 정보를 넘겨주지 않을 경우에는 단순히 오류만 노출할 뿐 멈추지는 않습니다. 멈추게 하려면 class를 마지막 정보에 넘겨주면 됩니다.

Slide 41

Slide 41 text

KSP 사용 TIP 몇 가지 Tip을 알아보겠습니다.

Slide 42

Slide 42 text

Class 생성자 변수 정보를 알고 싶어요 클래스의 상속은 superclass를 불러올 수 있었고, 생성자의 primary 생성자 역시 가져올 수 있습니다.

Slide 43

Slide 43 text

실제 Class가 위치하는 지 알고 싶어요? 내 모듈에 포함되어 있는 정보가 실제 접근이 가능한지 파악할 필요가 있습니다. KSName을 지정해 주고, 이와 같은 형태의 코드를 활용하면 KSName이 패키지에 접근할 수 있고, 실제로 존재하는지도 파악할 수 있습니다. 파일이 위치에 없다면 null을 리턴합니다.

Slide 44

Slide 44 text

Visitor는 언제 쓰나요? ● Annotation을 찾을 때 이미 Visitor를 활용하고 있다. 이미 클래스 내부정보를 알 수 있어 다시 할 필요는 없다. ● 많은 예제에 보면 Visitor를 이용하는데, 이는 문서에도 나와있지만 Multiple round processing 에서 활용 많은 코드를 보다 보면 Visitor를 활용하고 있는데, 문서에 나와있지만 Multiple round processing 문서에 설명되어 있습니다. 만약 해당 코드 샘플처럼 validate까지 진행하고, multiple round processing을 활용해야 한다면 샘플을 찾아보시길…

Slide 45

Slide 45 text

KSP의 전체 구조 전체 구조는 KSP의 파일 구조를 이해하는 데 도움 됩니다. https://kotlinlang.org/docs/images/ksp-class-diagram.svg

Slide 46

Slide 46 text

여기까지

Slide 47

Slide 47 text

참고한 자료들 공식 문서 Kotlin Symbol Processing API 소스코드 google/ksp: Kotlin Symbol Processing API My first Kotlin Symbol Processing Tool for Android | by SeongUg Steve Jung KSP: Fact or kapt?. Fast and ergonomic annotation… | by David Rawson | ProAndroidDev KSP(Kotlin Symbol Processor) 톺아보기 KAPT보다 2배 더 빠르게, 코틀린을 위한 KSP