[Hilt] Hilt 내부 동작의 이해
애노테이션은 여러가지 속성을 가질 수 있다.
정의된 애노테이션은 클래스에 마킹할 수 있다. 뿐만아니라 필드 메서드 파라미터 등에서도 선택적으로 사용가능하다.
안드로이드에서 많이 사용해본 표준 애노테이션으로 @Override @Non-null 등이 있을 것이다.
Hilt의 주요 Annotation의 종류
- @HiltAndroidApp
- @AndroidEntryPoint
- @Module
- @InstallIn
- @HiltViewModel
애노테이션의 특징
- JDK 1.5부터 추가
- 자바(또는 코틀린) 소스코드에 추가하는 메타데이터
- 컴파일러에게 부가정보 제공
- 클래스, 필드, 메서드 및 기타 요소에 선택적으로 선언 가능
- 런타임에서도 참조 가능
애노테이션의 목적은 소스코드를 해치지 않으면서 컴파일러에게 부가정보를 제공하기 위해 추가되었다.
Annotaion Procesoor란?
Annotation Processor란, 영단어 말 그대로 애노테이션 처리기로, 컴파일 타임에 애노테이션을 스캔하고, 소스코드를 검사 또는 생성한다. 빌드를 다 하고 나서 특정 라이브러리나 플러그인에 의해 프로젝트에 자동으로 코드가 생성되는 것을 본 적이 있을 것이다. 이것이 Annotation Processing이다.
하나의 애노테이션을 처리하는 과정을 round라고 하며 몇차례의 round를 거치며 Anootation Processor가 전체 코드를 스캔하고 처리하게 된다.
애노테이션 프로세싱 라운드에서 소스 코드 생성이 가능하다.
- 경로 지정 - java.io.File API로 적절한 경로 지정
- 소스 코드 작성 - Java 문법에 맞게 소스코드를 작성한다. (String 문자열)
- 파일 저장 - 작성한 문자열을 파일 스트림을 통해 방출하고 저장한다.
보통 이런 일이 번거롭기에 손쉽게 도와주는 별도의 라이브러리를 사용하게 된다.
Java의 경우 JavaPoet이라는 코드 생성 도구를 사용한다. Dagger도 마찬가지이다.
그렇게 생성된 코드는 일반적으로 Build 폴더 하위에 위치하게 되며, 프로젝트 폴더 구조를 확인해보면 generated라고 되어있는 것들을 확인할 수 있다.
Hilt 애노테이션 처리 요약
- Hilt 애노테이션을 사용하여 부가정보를 제공
- 컴파일 타임에 의존성 그래프에 이상이 없는지 확인
- 생성된 소스코드를 기반으로 동작하므로 리플렉션을 사용하지 않아도 됨
Hilt는 전용 애노테이션을 사용해서 컴파일 타임에 애노테이션을 적절히 처리한다.
이 과정에서 object 그래프에 문제가 발생한다면 예외를 발생시키고 빌드를 중단시킨다.
만약 런타임까지 가서 오류가 밝혀진다면 개발-테스트에 시간이 오래 걸렸겠으나 컴파일 타임에 오류를 체크할 수 있기에 이러한 부분에서 시간단축이 가능하다.
바이트코드 변조
바이트코드란, 자바 소스코드가 컴파일을 거쳐 나온 결과물이다.
자바 소스코드(*.java) -> 자바 컴파일러 -> 자바 바이트코드(*.class)
Transform API
- AGP(Android Gradle Plugin)에 포함된 API
- 중간 빌드 산출물들을 처리
- 바이트코드 변환을 위한 Gradle Task 생성
- AGP는 변조된 내용들 사이의 의존성을 핸들링
여러 플랫폼에서 보편적으로 Gradle이란 시스템을 사용한다. 안드로이드도 마찬가지이며 이를 확장한 AGP라는 것을 프로젝트 생성시부터 사용하게 된다.
이 AGP에 포함된 API 중 하나가 Transform API이다.
이 API는 바이트코드를 재처리하는게 가능해진다. Hilt는 Build 단계에서부터 Transform API를 사용해서 바이트코드를 변조한다.
안드로이드 앱 빌드 과정 (추상화)
- 소스코드 -> 컴파일러 -> 바이트코드 -> D8 -> Dex -> APK/AAB
여기서 바이트코드 변조가 바이트코드 바로 다음에 이뤄진다.
바이트코드 변조를 도대체 왜 하는 것일까?
바이트코드 변조 예시
@HiltAndroidApp
public class App extends Application()
Hilt를 사용할 때, 바이트코드 변조 기능이 필수는 아니다. 하지만, 소스코드를 해치지 않으면서 편리하게 의존성 주입을 할 수 있도록 바이트코드 변조가 도와준다.
- App -> 컴파일러 -> Hilt_App
위 코드가 컴파일러를 거치면 다음과 같이 Hilt_App 클래스가 생성된다.
의존성 주입을 하기 위해 이 Hilt_App이라는 클래스를 반드시 참조해야한다.
그렇다면 소스코드는 다음과 같이 변해야한다.
@HiltAndroidApp
public class App extends Hilt_App()
App 클래스는 Hilt_App를 상속하고, 컴파일러는 App 클래스를 컴파일할 때 Hilt가 생성한 코드들을 포함하게 된다.
Hilt_App 클래스가 생성되기 이전이었다면? 아마 Hilt_App을 찾지 못한다는 에러가 발생할 것이다.
App 클래스의 이름을 바꾸게 된다면? Hilt_App은 Hilt_ 접두사를 붙여서 클래스를 만들기 때문에 Hilt_App 클래스의 이름도 바꿔야한다.
또한, 상속을 안하는 실수를 저지를 수도 있다.
그렇기에 그러한 불편함들을 Hilt의 바이트코드 변조가 필요하다.
Hilt의 전체 프로세스 요약
- Hilt 애노테이션이 포함된 소스 코드
- 애노테이션 프로세싱
- 생성된 소스코드
- 컴파일
- 바이트코드 산출
- 바이트코드 변조
- D8 컴파일 이후 APK/AAB 패키징
Hilt 애노테이션이 포함된 소스코드를 컴파일 타임에 애노테이션 프로세싱을 통해 처리한다.
그때 생성된 소스코드와 소스코드를 컴파일하면 바이트코드 산출이 된다.
이때 해당 바이트코드를 변조하여 APK/AAB로 패키징하면 끝난다.
요약
- 애노테이션은 메타데이터를 제공한다.
- 애노테이션 프로세서는 메타데이터를 읽고 소스코드를 생성한다
- 생성된 소스코드로 인해 보일러플레이트가 감소한다
- AGP에 포함된 Transform API로 바이트코드를 변조할 수 있다
- Dagger(Hilt)로 생성된 소스코드는 바이트코드 변조 과정을 통해 자동으로 프로젝트에 적용이 된다