0. Annotation
Hilt는 annotation 기반으로 돌아간다. annotation의 특징은 소스코드를 해치지 않으면서 컴파일러에게 부가정보를 제공하기 위함이다.
- Hilt annotation을 사용하여 부가정보를 제공
- 컴파일 타임에 의존성 그래프에 이상이 없는지 확인
- 생성된 소스코드를 기반으로 동작하므로 리플렉션을 사용하지 않아도 되어 퍼포먼스가 떨어질 일이 없음
1. 코틀린에서의 Hilt Annotation Processing
dependencies {
implementation 'com.google.dagger:hilt-android:2.48.1'
kapt 'com.google.dagger:hilt-compiler:2.48.1'
}
kapt {
correctErrorTypes true
}
Hilt는 자바 컴파일러에 포함된 플러그인이다. 자바 어노테이션 프로세서 기반으로 작성된 라이브러리이지만 코틀린 컴파일러에는 어노테이션 프로세서가 존재하지 않는다. 그렇다면 어떻게 코틀린으로 작성한 코드도 해석할 수 있는 것일까?
코틀린에서 힐트 어노테이션을 참조해서 작성하는 경우는 자바와 상호운용성을 위해 kapt 플러그인을 추가해주고, kapt는 직접 컴파일타임에 stub이라는 파일을 작성해준다. 이런 것 때문에 빌드할 때 컴파일시간이 늘어나게 되지만 자바 컴파일러가 kapt 가 생성해준 stub 자바 파일을 생성해주기 때문에 코틀린으로 작성한 코드도 해석할 수 있는 것이다. 결국 Hilt annotation processing 이 가능하게 된다.
따라서 Hilt, Room과 같은 라이브러리를 사용할 때는 Kotlin 프로젝트에서 kapt 플러그인을 사용하여 어노테이션 프로세싱을 활성화해야 한다.
@HiltAnnotation 소스코드.kt > KAPT > Stub.java > 자바 컴파일러
kapt 플러그인의 correctErrorTypes의 기본값은 false이다. 힐트 어노테이션 프로세서는 선언된 클래스들의 시그니처를 정확하게 구분해야하는데, 기본적으로 kapt는 모르는 타입에 대해서는 NonExistenceClass라는 임의의 클래스로 대체하여 처리한다. 여기서 hilt annotation이 해석을 하지 못하여 컴파일이 중단되는 경우가 있다. 이것을 회피하기 위해 true로 설정하여 생성된 stub에서 오류가 발생한 타입을 추론해서 계속하여 진행할 수 있게 한다.
2. Annotation Process
컴파일 타임에 어노테이션을 스캔하고, 소스코드를 검사 또는 생성
빌드를 한 후 특정한 라이브러리나 플러그인으로 인해 자동으로 소스코드가 생성되는 것을 볼 수 있는데 이는 어노테이션 프로세서 때문이다. 하나의 프로세싱을 처리하는 단계를 round라 하고 프로세서는 몇 단계의 round를 거쳐 코드를 스캔하게 된다.
3. 바이트 코드 변조
Dagger와 달리 Hilt에서 새롭게 추가된 것은 바이트 코드 변조이다. 자바나 코틀린으로 소스 코드 작성(*.java)하고 소스 코드를 자바 컴파일러를 통해 가상머신이 해석할 수 있는 언어로 만드는 산출물이 자바 바이트 코드(*.class)이다.
Android Gradle Plugin(AGP)에 포함된 API인 Transform API를 통해 바이트 코드 변환을 위한 Gradle Task를 생성한다. 때문에 빌드 단계에서 코드를 생성하게 된다.
바이트 코드 변조의 단계를 알아보기 위해 일반적인 안드로이드 앱 빌드 과정을 살펴보자.
소스 코드 > 컴파일러 > 바이트코드 > D8 > Dex > APK/AAB
안드로이드 런타임에서 구동가능한 Dex파일을 만들기 위해 D8 컴파일러를 거쳐 다시 APK/AAB 로 패키징하게 된다.
여기에 바이트 코드 변조 단계가 아래와 같이 들어가게 된다.
소스 코드 > 컴파일러 > 바이트코드 > 바이트 코드 변조 > D8 > Dex > APK/AAB
그렇다면 바이트 코드 변조를 왜 하는 것일까?
Hilt_App을 상속하도록 바이트 코드가 변조됨
Hilt를 사용할 때 바이트 코드 변조는 필수사항은 아니다. 소스코드를 해치지 않으면서 Hilt를 사용할 때 편리하게 의존성주입을 할 때 바이트 코드 변조는 이를 쉽게 도와준다.
@HiltAndroidApp
public class App extends Application()
App > 컴파일러 > Hilt_App
Hilt로 의존성 주입을 시작하려면 @HiltAndroidApp 어노테이션으로 마킹해주면, 앱 클래스가 어노테이션 프로세싱을 거치면 Hilt_App 이라는 클래스를 생성하게 된다. 의존성 주입을 하기 위해 Hilt가 생성해준 Hilt_App 클래스를 반드시 참조해야한다. 그렇다면 소스 코드는 다음과 같이 바뀌어야한다.
@HiltAndroidApp
public class App extends Hilt_App()
App > Hilt_App > Application
앱 클래스는 Hilt_App 클래스를 상속해야하고 비로소 컴파일러는 App 를 컴파일할 때 Hilt_App 코드를 포함하게 된다. App 상속하는 것을 Hilt_App을 상속하도록 바이트 코드를 변조함으로써 소스코드상에서 App은 Application 상속을 유지하면서도 컴파일 이후 바이트 코드 내부에는 바이트 코드 내부를 통해 App > Hilt_App > Application 순으로 의존하게 된다. 소스코드는 Hilt_App() 을 상속하는 것을 찾아볼 수 없고 개발자가 따로 작성하지 않아도 되는 것이다.
참고자료
'🗡️Hilt' 카테고리의 다른 글
[Hilt] 타입과 의존성 (0) | 2024.08.27 |
---|---|
[Hilt] Dagger Hilt로 의존성 주입하기 (0) | 2024.06.22 |