Hilt의 2단계 컴파일 모델과 증분 빌드
Hilt의 2단계 컴파일 모델과 증분 빌드
Hilt는 Dagger 위에 구축된 안드로이드용 컴파일 타임 의존성 주입(dependency injection) 프레임워크입니다. Hilt의 가장 핵심적인 아키텍처 결정은 어노테이션 프로세싱을 두 단계로 분리하는 2단계 컴파일 모델에 있습니다. 첫 번째 단계에서는 격리(isolating) 모드로 의존성 메타데이터를 수집하고, 두 번째 단계에서는 집계(aggregating) 모드로 루트 컴포넌트를 생성합니다. 이러한 분리 덕분에 대규모 멀티 모듈 프로젝트에서도 빠른 증분 빌드(incremental build)를 누릴 수 있습니다. 단일 Hilt 모듈을 변경하면 해당 모듈의 메타데이터만 재처리되며, 전체 의존성 그래프를 다시 처리할 필요가 없기 때문입니다. 이번 면접 질문을 통하여 아래 내용들을 학습하실 수 있습니다.
- Hilt의 어노테이션 프로세서가 작업을 격리 단계와 집계 단계로 어떻게 분리하는지 설명할 수 있습니다.
@AggregatedDeps메타데이터 클래스가 어떤 정보를 담고 있으며, 왜 존재하는지 서술할 수 있습니다.- ISOLATING, AGGREGATING, DYNAMIC 어노테이션 프로세서 모드의 차이점과 증분 컴파일에 미치는 영향을 구분할 수 있습니다.
@InstallIn모듈이 소스 어노테이션에서 최종 Dagger 컴포넌트로 생성되기까지의 전체 흐름을 추적할 수 있습니다.- Hilt Gradle 플러그인의
enableAggregatingTask옵션을 적용하고, 이 옵션이 빌드 성능을 어떻게 개선하는지 설명할 수 있습니다.
Phase 1: ISOLATING 모드에서의 의존성 집계
Hilt 컴파일의 첫 번째 단계는 AggregatedDepsProcessor가 담당합니다. 이 프로세서는 ISOLATING 모드로 선언되어 있으며, Java 및 Kotlin 어노테이션 프로세서가 사용할 수 있는 모드 중 증분 빌드에 가장 유리한 모드입니다. ISOLATING 모드에서는 프로세서가 현재 처리 중인 단일 어노테이션 요소에만 의존하는 출력 파일을 생성할 수 있으며, 컴파일 유닛 내 다른 소스 파일에는 의존하지 않습니다. 다시 말해, 각 입력과 출력이 1:1로 대응하므로 Gradle이 변경된 파일만 정밀하게 재처리할 수 있습니다.
@IncrementalAnnotationProcessor(ISOLATING)
@AutoService(Processor.class)
public final class AggregatedDepsProcessor extends JavacBaseProcessingStepProcessor {
@Override
protected BaseProcessingStep processingStep() {
return new AggregatedDepsProcessingStep(getXProcessingEnv());
}
}
Dagger 모듈에 @Module과 @InstallIn(SingletonComponent::class)을 지정하면, 이 프로세서는 hilt_aggregated_deps라는 잘 알려진 패키지에 소규모 메타데이터 클래스를 생성합니다. 생성된 클래스는 본문이 비어 있으며, 해당 모듈이 어떤 컴포넌트에 속하는지를 기록하는 @AggregatedDeps 어노테이션을 전달하는 역할만 수행합니다.
package hilt_aggregated_deps
@AggregatedDeps(
components = "dagger.hilt.components.SingletonComponent",
modules = "com.example.NetworkModule"
)
public class _com_example_NetworkModule {}
이 클래스는 로직, 필드, 메서드가 전혀 없는 순수 메타데이터 산출물입니다. 이후 컴파일 단계에서 hilt_aggregated_deps 패키지를 스캔하고 어노테이션 값을 읽어 정보를 수집할 수 있도록 하기 위해서만 존재합니다. 프로세서가 ISOLATING 모드로 동작하므로 Gradle은 _com_example_NetworkModule이 오직 NetworkModule.kt에만 의존한다는 사실을 파악할 수 있습니다. 따라서 NetworkModule을 수정하면 해당 메타데이터 클래스만 재생성됩니다.
@AggregatedDeps 어노테이션은 Phase 2가 필요로 하는 모든 정보를 원본 소스에 접근하지 않고도 전달할 수 있도록 설계되었습니다.
@Retention(CLASS)
public @interface AggregatedDeps {
String[] components(); // 대상 컴포넌트
String test() default ""; // 연관된 테스트 클래스 (있을 경우)
String[] replaces() default {}; // 이 모듈이 대체하는 모듈
String[] modules() default {}; // 설치 대상 모듈
String[] entryPoints() default {};// 설치 대상 진입점(entry point)
}
replaces 필드는 테스트 모듈이 프로덕션 모듈을 대체하는 Hilt 테스트 인프라를 지원하며, entryPoints 필드는 @EntryPoint 인터페이스를 처리합니다. 모든 데이터가 어노테이션 값으로 직렬화되어 있으므로, Phase 2에서는 메타데이터만으로 의존성 그래프를 재구성할 수 있습니다. 이렇게 모든 정보를 어노테이션에 직렬화하는 접근 방식 덕분에, Phase 1과 Phase 2 사이에 소스 코드 수준의 의존성이 완전히 끊어집니다.