Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

Kotlin Night 2022 - 코틀린 어노테이션으로 할 수 있는 것(GDG Se...

TaeHwan
September 06, 2022

Kotlin Night 2022 - 코틀린 어노테이션으로 할 수 있는 것(GDG Seoul)

코틀린 어노테이션으로 할 수 있는 것을 주제로한 발표 자료

TaeHwan

September 06, 2022
Tweet

More Decks by TaeHwan

Other Decks in Programming

Transcript

  1. 코틀린 어노테이션으로 할 수 있는 것 taehwan(레몬트리) 오늘의 발표 내용은

    코틀린 어노테이션으로 할 수 있는 것! 현실이 그렇게 호락하진 않지만(실제 쓸 곳이 많진 않지만) 알아보죠.
  2. 오늘 내용 • 어노테이션이란? • 정말 자주 쓰는 어노테이션 몇

    가지 • 어노테이션 선언 방법 • Runtime에서의 활용과 Compile time에서의 활용성 • KSP 간단한 예제(Android) 내용 내용 위 내용 참고
  3. Annotation? • 메타데이터를 추가하면 어노테이션을 처리하는 도구가 상황에 따라 처리

    ◦ Runtime ◦ Compile time 어노테이션이란 메타데이터를 추가해 어노테이션을 처리하는 도구인 Runtime과 Compile time에 이를 처리해 줍니다.
  4. Annotation? • 메타데이터를 추가하면 어노테이션을 처리하는 도구가 상황에 따라 처리

    ◦ Runtime - kotlin reflection ◦ Compile time - KSP(Kotlin Symbol Processing) Kotlin에서는 Runtime은 Kotlin reflection을 활용할 수 있고(Java reflection을 보통 사용합니다.(하지만 코틀린 관련 문법을 위해 Kotlin Reflection 사용이 좋습니다.)) Compile time에는 KSP를 활용할 수 있습니다.
  5. Annotation 사용법 @Test fun test() {} @Suppress("unused") fun unused() {}

    어노테이션 사용법은 간단하죠. @와 어노테이션 이름을 함께 합칩니다. 필요에 따라 파라미터 추가도 할 수 있습니다.
  6. 흔히 사용하는 Annotation • @Test : UnitTest 시 활용 •

    @Suppress : 컴파일 경고 억제 • @Deprecated(message = “message”) : 코드가 지워질 수 있음을 경고 • @Deprecated( message = "moved to var", replaceWith = ReplaceWith(expression = "run { this.protocols = protocols }"), level = DeprecationLevel.ERROR ) : 코드가 지워질 수 있음을 경고하고 새로운 사용법 안내 많이 사용되는 어노테이션 몇 개 나열해 보았습니다.
  7. Annotation 선언 @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY) @Retention(AnnotationRetention.RUNTIME) annotation class AnnotationName Target은

    Annotation을 사용할 위치 Class, Function, Property, Field 등등 Target은 말 그대로 Annotation 사용 가능 지점을 정하는 것입니다.
  8. @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY) @Retention(AnnotationRetention.RUNTIME) annotation class AnnotationName Retention은 유지 조건(사용

    시점) Runtime, Sources, Binary Annotation 선언 @Retention은 유지 조건을 말합니다. 뒤에서 자세하게 하나씩 정리해뒀습니다.
  9. Annotation 선언 @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY) @Retention(AnnotationRetention.RUNTIME) annotation class AnnotationName Annotation

    이름 Runtime, Compile time에서 이를 search 마지막으로 annotation class를 정의합니다. 이 이름을 Runtime과 Compile time에서 검색할 수 있습니다.
  10. 어노테이션 선언 - @Target • CLASS : Class에 정의 •

    TYPE_PARAMETER : Parameter에 정의 • PROPERTY : val/var property에 정의 • FIELD : 프로퍼티에 의해 생성되는 필드에 정의 • CONSTRUCTOR : 생성자에 정의 • FUNCTION : 함수에 정의 • PROPERTY_GETTER : Getter Property에 정의 • PROPERTY_SETTER : Setter Property에 정의 • FILE : File에 정의 Target은 Class, Property, function, file 등 거의 대부분에 지정해 줄 수 있습니다. 예를 들면 @Suppress 어노테이션은 거의 대부분에서 활용하도록 지정되어 있습니다.(문서 참고)
  11. 어노테이션 선언 - @Retention • SOURCE : build 시에 만

    활용하고, 흔적이 남지 않음 • BINARY : build 시에 활용하고, 흔적이 남지만 reflection에서 보이지 않음 • RUNTIME : reflection에서 활용할 수 있고, 흔적이 남음 유지 조건은 3가지가 있습니다. 3개의 동작 방식이 다르기 때문에 Source는 build에서만 활용하고, 흔적이 남지 않죠. Binary는 지정하지 않으면 기본값으로 동작하는데, build에서 사용하고, 흔적 역시 남습니다. 디컴파일 시 차이는 뒤에서 살펴보고, Runtime은 Reflection에서 활용합니다.
  12. 어노테이션 선언 - @Retention - SOURCE @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.SOURCE) annotation class

    SourceTest 하나씩 살펴보겠습니다. Source에 대한 어노테이션 정의는 위와 같습니다.
  13. 어노테이션 선언 - @Retention - SOURCE @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.SOURCE) annotation class

    SourceTest @SourceTest class TestSource 사용은 @SourceTest처럼 활용할 수 있습니다.
  14. 어노테이션 선언 - @Retention - SOURCE @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.SOURCE) annotation class

    SourceTest @SourceTest class TestSource public final class TestSource {} 마지막 줄이 디컴파일 결과물인데요. 어노테이션은 지워짐을 알 수 있습니다. 결국 컴파일 과정에서만 활용되고 이후에는 흔적조차 없어집니다.
  15. 어노테이션 선언 - @Retention - BINARY @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.BINARY) annotation class

    BinaryTest @BinaryTest class TestBinary @BinaryTest public final class TestBinary {} @BinaryTest를 붙인 상태로 디컴파일 결과물을 살펴보면 어노테이션이 붙어있는 것을 알 수 있습니다. 하지만 Runtime에서 이를 확인할 순 없습니다. 그냥 디컴파일 시 위치를 확인할 수 있는 것이지, 리플렉션에서 활용할 순 없습니다.
  16. 어노테이션 선언 - @Retention - RUNTIME @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) annotation class

    RuntimeTest 마지막으로 Runtime입니다. Runtime annotation 정의는 앞과 동일합니다.
  17. 어노테이션 선언 - @Retention - RUNTIME @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) annotation class

    RuntimeTest @RuntimeTest class TestRuntime 사용법도 별다를 건 없으니 넘어가죠.
  18. 어노테이션 선언 - @Retention - RUNTIME @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) annotation class

    RuntimeTest @RuntimeTest class TestRuntime @RuntimeTest public final class TestRuntime {} Binary와 차이가 없지만 명확하게 Runtime인 리플렉션에서 확인이 가능합니다. 추가로 발표에서는 언급 안 했는데, Runtime을 적용하면 Compile 단계에서도 이를 확인할 수 있습니다. 만약 양쪽 다 필요로 한다면 @Runtime을 적어주시는 게 좋겠죠.
  19. Annotation에 파라미터는? • 파라미터를 가질 수 있다. ◦ 보통 옵션

    정보를 담는다.(Boolean, String, Type 등(Any를 가질 순 없다.)) • annotation class AnnotationName(val args: Boolean, list: List<String>) Annotation에 파라미터 적용이 가능합니다. Annotation 이름 뒤에 생성자 위치에 파라미터를 정의하시면 됩니다. 단, Any 타입을 제외한 primitive types에 대한 적용이 가능합니다. 검색할 때는 Any로 들어오는데, 이를 컨버팅하는 과정을 거쳐야 합니다.
  20. Custom Annotation 활용법 • Runtime - kotlin reflection • Compile

    time - Kotlin Symbol Processing API 활용방법은 앞에서 소개해 드렸듯 Runtime은 kotlin reflection을 활용할 수 있고, Compile time은 KSP(Kotlin Symbol Processing API)를 활용하게 됩니다.
  21. • JVM Dependency - kotlin-reflect 추가 필요(최신 버전은 kotlin version과

    동일(ex. 1.7.10)) ◦ implementation("org.jetbrains.kotlin:kotlin-reflect:1.7.10") • Class reference ◦ val c = MyClass::class • Callable references ◦ fun isOdd(x: Int) = x % 2 != 0 val numbers = listOf(1, 2, 3) println(numbers.filter(::isOdd)) Runtime - kotlin reflection kotlin-reflect를 implementation 추가하고 사용할 수 있습니다. reflection이 별다른 건 없고 흔하게 객체화되기 전의 Class를 reference 형태로 사용할 수 있습니다. 또는 Callable references 활용도 가능합니다. 흔하게 람다 표현식으로 다뤄본 적이 있을 겁니다.
  22. Runtime - kotlin reflection - Class reference Class reference는 ::class로

    접근할 수 있는데 KClass로 접근됩니다. 위에서 일부만 보이지만, anntations 정보와 members 정보, functions 정보 등등이 여기에 포함됩니다. Class 자체의 접근 제어자도 확인할 수 있습니다.
  23. Runtime의 장/단점 • 장점 ◦ Runtime 상태에서 class/function/property 등의 정보를

    확인할 수 있다. • 단점 ◦ Reflection을 활용하기 때문에 프로가드 룰에서 제외 필요 • 주의 ◦ 자주 호출되어지기 때문에 cache 등록도 미리 해두어야 한다. ▪ Class, function, property 등에 대한 cache 등록 이런 reflection의 장단점도 있는데, 당연히 runtime에서 class의 function/property 정보를 확인할 수 있습니다. 안드로이드에서는 프로가드 룰에서 이를 제외해 줘야 합니다.(어노테이션 정보) 그리고 runtime이다 보니 자주 호출 됩니다. 미리 cache에 저장해두고 reflection의 효율성을 증대시켜줄 필요도 있습니다. 또는 이미 어떠한 결과물에 대한 처리를 저장해두고, 종료를 하는 예를 들 수 있겠네요.
  24. 주요 Class 정보 • KClass : Class 내의 정보를 담는다.(function,

    property, nullable 상태 등) • KCallable : Function or Property의 호출(call 함수를 이용한) 가능한 entity 정보를 담는다. • KFunction : 함수의 파라미터 정보와 리턴 정보 등을 담는다.(suspend function 정보 등) • KProperty : val, var로 선언한 property 정보를 담는다. KClass는 class 내의 정보를 담고 있고, 여기엔 KCallable도 포함합니다. 결국 어떠한 작업을 실행할 수 있어야 하니 KCallable을 활용할 수 있습니다. KFunction과 KProperty 정보도 알 수 있습니다. 결국 클래스 내의 함수 정보와 프로퍼티 정보를 모두 확인할 수 있고, 이들을 실행할 수 있는 건 KCallable입니다.
  25. 주요 Class 정보 • KClass : Class 내의 정보를 담는다.(function,

    property, nullable 상태 등) • KCallable : Function or Property의 호출(call 함수를 이용한) 가능한 entity 정보를 담는다. • KFunction : 함수의 파라미터 정보와 리턴 정보 등을 담는다.(suspend function 정보 등) • KProperty : val, var로 선언한 property 정보를 담는다. 도식화하면 아래 그림과 같습니다.
  26. • Symbol-processing-api 추가 필요 ◦ implementation("com.google.devtools.ksp:symbol-processing-api:1.7.10-1.0.6") • Compile time에 활용되기

    때문에 rebuild가 필요치 않는 부분에서 적용해야 이용성이 좋다. • 기존 코드에 새로운 코드 블록을 삽입/삭제할 수 있는 것은 아니다. Compile time - KSP(Kotlin Symbol Processing - Google Open source project) Compile time은 KSP를 이용할 수 있습니다. 이 오픈 소스는 젯브레인이 아닌 Google이 관리하고 배포하고 있습니다. 버전 정보가 코틀린 버전 - KSP 버전을 함께 명시하도록 되어있습니다. KSP는 build 과정에서 코드가 생성되기 때문에 당연히 rebuild가 필요치 않는 부분에 대한 코드 적용이 중요합니다. 이 부분을 매우 심도 있게 고민해 주셔야 실제 적용했을 때 높은 사용성과 효과를 볼 수 있습니다.
  27. Compile time - KSP(Kotlin Symbol Processing - Google Open source

    project) 대략 코드의 구성은 ProcessorProvider를 컴파일러에 알려주고, 내부 구현은 Processor를 상속받아 구현하게 됩니다.
  28. • 장점 ◦ Compile time 상태에 class 정보를 확인 가능

    ◦ Build 시에 코드를 추가한 파일 생성(기존 코드에 추가/제거하는 건 불가) • 단점 ◦ Build 시간이 길어질 수 있다.(기존 자바 용 보다는 2배 빠르다) ◦ Default value 정보를 가져올 수 없다. • 주의 ◦ Compile 과정에서 활용되기 때문에 UnitTest를 잘 작성해둬야 한다.(형태가 벗어나지 않도록) Compile time의 장/단점 Compile time 상태에서 class annotation 등의 정보를 확인할 수 있습니다. Build 시에 코드를 생성하고, 이를 포함하게 됩니다. 다만 기존 코드를 수정하거나 제거하는 건 불가능합니다. 새로운 파일을 만들어 결과를 추가하게 됩니다. 대신 설계를 잘해야 build 시간을 줄일 수 있습니다. 또 다른 단점은 코틀린에서 명시한 defualt value를 가져올 수 없습니다. 그리고 검증도 잘해줘야 합니다.
  29. KSP 잘 적용하려면 • KSP로 어떠한 코드를 자동으로 만들어줄 것인가?

    ◦ 리빌드 없이, 프로그램 실행 시에만 필요로 하는 코드 부분을 찾아 자동화한다. ◦ ex) DI와 같은 형태의 코드 • 실 업무에서 자동화할 부분이 많을까? ◦ 실제 업무에서는 생각보다 자동화할 부분이 많지는 않음 ◦ 보일러 플레이트를 줄여줄 수 있는 형태의 코드를 찾아 자동화(Runtime도 고려 가능) • 자동화되었을 때의 검증을 할 수 있는 방법도 함께 고려 ◦ Unit test 작성을 통해 자동으로 만들어지는 코드에 대한 검증 필수 앞에서도 언급했지만 KSP를 적용하는 건 생각보다 쉽지만 이걸 어디에 어떻게 효율적으로 적용할지가 중요합니다. 제가 작업하는 프로젝트에서는 DI 부분까지 모두 코드를 생성하고, 이를 상위의 APP까지 모두 자동으로 연결하는 구조를 가지고 있습니다. 리빌드 과정 없이 이 부분을 모두 한 번에 처리하고 있는데, 이렇게 작업하지 않으면 매우 비효율적일 수밖에 없습니다. 그러니 잘 고민하고 적용해야 합니다.
  30. 간단한 예를 살펴보자(안드로이드) • 안드로이드 DataStore 활용을 돕는 KSP ◦

    DataStore에 대한 interface 정의만 하면, 필요한 코드를 자동으로 붙여준다. 오늘 살펴볼 예제는 제가 오픈소스로 관리하고 있는 DataStore 활용을 돕는 KSP 코드를 살펴보려고 합니다. Interface 정의만 하면, 필요한 Impl 코드는 모두 자동으로 생성해 주게 됩니다.
  31. 간단한 예를 살펴보자(안드로이드) @UsefulPreferences interface SecurityPreferences { @GetValue(KEY_INT) fun getInt():

    Flow<Int> @ClearValues suspend fun clearAll() companion object { private const val KEY_INT = "key-int" } } 우선 사용법은 위와 같습니다. @UsefulPreferences를 명시하고, fun에 해당하는 값을 GetValue(), SetValue를 지정합니다. @ClearValues도 포함합니다. 이렇게 정의만 해두면 KSP를 통해 이 코드를 분석하는 작업 후 Impl 코드를 만들게 됩니다.
  32. 간단한 예를 살펴보자(안드로이드) class SecurityPreferencesImpl ( private val usefulSecurity :

    UsefulSecurity , private val preferencesStore : DataStore <Preferences>, ) : SecurityPreferences { public override fun getInt(): Flow<Int> = preferencesStore .data .mapDecrypt <Int>(usefulSecurity , Int::class) { it[SecurityPreferencesKeys .KEY_INT] } public override suspend fun setInt(`value`: Int): Unit { preferencesStore .editEncrypt (usefulSecurity , value) { preferences , encrypted -> preferences [SecurityPreferencesKeys .KEY_INT] = encrypted } } public override suspend fun clearAll(): Unit { preferencesStore .edit { it.clear() } } } KSP를 잘 다루는 것도 역시 기존 코드 결과물이 있어야 가능합니다. 이런 코드를 작성하던 반복 부분을 KSP로 만든다!라고 했을 때 도움이 될 수 있죠. 위와 같이 코드 작성해두고, 이 결과물을 KSP를 통해 생성하게 됩니다.
  33. 간단한 예를 살펴보자(안드로이드) internal object SecurityPreferencesKeys { public val KEY_INT:

    Preferences.Key<kotlin.String> = stringPreferencesKey("key-int") } 그리고 또 다른 파일이 하나 더 생성됩니다. Keys를 가지고 있는 파일인데, 오늘 코드에서는 이 결과물을 만드는 과정을 뒤에서 소개합니다.
  34. 간단한 예를 살펴보자(안드로이드) 1. UsefulPreferences를 Compile time에 서칭한다. a. SymbolProcessor의

    과정 중 process 과정에서 resolver를 이용하여 annotation 정보를 가져온다. b. Annotation 정보가 class에 포함되어 있으므로 KSClassDeclaration 정보로 변경한다. 그럼 1단계로 UsefulPreferences를 Compile time에 서칭하는 과정을 진행합니다. SymbolProcessor의 내부 함수 중 process를 override 해야 합니다. 이 함수에서 annotation을 검색하고, 클래스 내부의 구조를 파악하는 게 1단계입니다.(이 발표에서는 전부를 소개하지는 않습니다.)
  35. 간단한 예를 살펴보자(안드로이드) override fun process(resolver: Resolver): List<KSAnnotated> { resolver.getSymbolsWithAnnotation(“UsefulPreferences”)

    .filter { ksAnnotated -> ksAnnotated is KSClassDeclaration } .map { ksAnnotated -> ksAnnotated as KSClassDeclaration } return emptyList() } process의 일부분 코드입니다. 위와 같이 검색하고, 이를 필요한 데이터구조화 작업을 진행하게 됩니다.
  36. 간단한 예를 살펴보자(안드로이드) 1. UsefulPreferences를 Compile time에 서칭한다. a. SymbolProcessor의

    과정 중 process 과정에서 resolver를 이용하여 annotation 정보를 가져온다. b. Annotation 정보가 class에 포함되어 있으므로 KSClassDeclaration 정보로 변경한다. 2. 찾은 KSClassDeclaration 정보를 기반으로 Impl 클래스를 구현한다. a. file은 kt로 만든다. b. 코드 구조화를 쉽게 할 수 있도록 "com.squareup:kotlinpoet을 함께 활용하면 좋다. 찾은 KSClassDeclaration 정보를 기반으로 Impl 클래스를 구현할 것인데, 여기서는 squareup에서 제공하는 kotlinpoet를 활용합니다. kotlinpoet는 왼쪽 이미지처럼 코드 블록을 그대로 적고, 이를 파일로 출력할 수 있는데, 매우 쉽게 코드 작성이 가능하여 사용하고 있습니다.
  37. 간단한 예를 살펴보자(안드로이드) override fun finish() { targetList.generatePreferences( codeGenerator =

    codeGenerator, logger = logger, ) targetList.clear() } 다음 과정은 finish() 함수에서 진행하고 있습니다. 이러한 과정은 실제 적용하시면 증분 빌드를 고민할 필요가 있습니다. KSP는 기본 증분 빌드를 진행하기 때문에 증분에 따라 결과물에 문제가 생길 수 있으니 주의하셔야 합니다.
  38. 간단한 예를 살펴보자(안드로이드) private fun Map<String , KSName >.generateKeysClass (

    disableSecurity : Boolean , className : String , packageName : String , ): ClassName { val newClassName = "${className }Keys" val fileSpec = FileSpec .builder (packageName = packageName , fileName = newClassName) . addImport (DataStoreConst .PREF_PREFERENCES .packageName , DataStoreConst .PREF_PREFERENCES .simpleName ) . addImport (DataStoreConst .PREF_PREFERENCES_KEY .packageName , DataStoreConst .PREF_PREFERENCES_KEY .simpleName ) val classSpec = TypeSpec .objectBuilder (name = newClassName) . addModifiers (KModifier .INTERNAL ) forEach { (key, type) -> val propertySpec = key. createProperty (String ::class.qualifiedName ?: "", DataStoreConst .PREF_GENERATE_STRING , fileSpec) classSpec. addProperty (propertySpec. build ()) } fileSpec. addType (classSpec. build ()) // Write file fileSpec. build ().writeTo ( codeGenerator = codeGenerator , packageName = packageName , fileName = newClassName, ) return ClassName (packageName = packageName , newClassName) } 코드가 매우 긴데요.(발표 때 잘 안 보여서 죄송합니다. 여기서는 다 분할해서 주요 코드 설명하겠습니다.)
  39. 간단한 예를 살펴보자(안드로이드) private fun Map<String, KSName>.generateKeysClass( disableSecurity: Boolean, className:

    String, packageName: String, ): ClassName { return ClassName(packageName = packageName, newClassName) } 먼저 generateKeyClass 코드입니다. 필요한 정보를 넘겨주고 있고, 저는 이 KeysClass의 packageName과 ClassName을 다시 활용해야 해서 ClassName 정보에 return 시켜주고 있습니다.
  40. 간단한 예를 살펴보자(안드로이드) val newClassName = "${className}Keys" val fileSpec =

    FileSpec.builder(packageName = packageName, fileName = newClassName) .addImport(DataStoreConst.PREF_PREFERENCES.packageName, DataStoreConst.PREF_PREFERENCES.simpleName) .addImport(DataStoreConst.PREF_PREFERENCES_KEY.packageName, DataStoreConst.PREF_PREFERENCES_KEY.simpleName) val classSpec = TypeSpec.objectBuilder(name = newClassName) .addModifiers(KModifier.INTERNAL 새로운 파일 스펙을 정의해야 하고, 이 안에 클래스 스펙을 정의해야 합니다. 파일에는 import 구문도 함께 포함되게 되니 파일에 import 정보도 포함합니다. Class 정보는 TypeSpec. 안에 class, object, abstract class, interface를 모두 만들 수 있습니다. 이를 활용해 classSpec도 함께 구성합니다.
  41. 간단한 예를 살펴보자(안드로이드) forEach { (key, type) -> val propertySpec

    = key.createProperty(String::class.qualifiedName ?: "", DataStoreConst.PREF_GENERATE_STRING, fileSpec) classSpec.addProperty(propertySpec.build()) } process 과정에서 검색해둔 property 정보를 새로운 class에 정보로 담기 위한 코드 부분입니다.(전체 코드는 제 github을 참고 부탁드립니다.) N 개의 변수가 있을 수 있으니 이에 대한 정보를 차례대로 classSpec에 property 정보로 담습니다.
  42. 간단한 예를 살펴보자(안드로이드) fileSpec.addType(classSpec.build()) // Write file fileSpec.build().writeTo( codeGenerator =

    codeGenerator, packageName = packageName, fileName = newClassName, ) 마지막으로 fileSpec에 앞에서 추가한 Property를 포함한 정보를 build 해서 추가해 줍니다. 이런 add는 코틀린에서 여러 개도 가능합니다. 함수 추가도 가능하고요. 마지막으로 실제 파일을 쓰게 됩니다. writeTo를 이용해서 파일에 쓰게 되는 과정을 거치면 됩니다.
  43. 간단한 예를 살펴보자(안드로이드) internal fun FileSpec.writeTo( codeGenerator: CodeGenerator, packageName: String,

    fileName: String, ) { val outputStream = codeGenerator.createNewFile( Dependencies.ALL_FILES, packageName, fileName ) OutputStreamWriter(outputStream, "UTF-8").use { writeTo(it) } } writeTo는 제가 만든 확장 함수라서 실제론 위와 같습니다. codeGenerator를 이용해 최종 파일로 저장합니다. 파일 저장하는 방법은 OutputStreamWriter로 저장합니다.
  44. 정리하면 • Annotation을 활용하여 Runtime과 Compile time에 원하는 결과물을 만들

    수 있다. • 리빌드 필요로 하지 않는 부분에 이를 활용해야 효과를 극대화할 수 있다. ◦ 앞에 코드에 DI를 위한 Module도 자동으로 만든다면 사용성이 더 좋아진다. ◦ 그리고 이렇게 만든 Module을 app에서도 자동으로 붙인다면 더 효과가 좋다. • 리빌드 없이 사용되어야 개발에 영향을 미치지 않는다. ◦ 결국 사용할 곳이 많지는 않다. ◦ DI에서 이를 활용하도록 만들어야 효과적 마지막으로 정리를 해보죠. Annotation을 활용할 수 있는 방법은 Runtime, Compile time 모두에서 가능합니다. KSP의 경우 정말 효율 높게 사용하려면 rebuild 과정이 없도록 만들어야 효과적으로 활용할 수 있습니다. 단순히 결과물만 만들어 효율을 높일 수 있진 않습니다. 예를 들면 startActivity를 자동으로 만들기 위해 rebuild 과정을 거치는 것보단 그냥 코드 작성하는 게 더 효율적일 수 있습니다. 결국 리빌드 없이 만들어야 하는데 이 부분을 고려치 않으면 오히려 개발 과정이 귀찮아질 수 있습니다.
  45. End. [email protected] https://thdev.tech/ https://github.com/taehwandev 여기까지가 발표 자료입니다. 부족한 부분은 피드백

    주시거나, 궁금하신 건 메일로 연락 주시면 답변드리도록 하겠습니다. 감사합니다.