| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | ||||
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 25 | 26 | 27 | 28 | 29 | 30 | 31 |
- ios
- launch screen
- 앱 서명 키 인증서
- React Native
- data driven construct
- Android
- presentationbackgroundinteraction
- native
- react
- 명시적 정체성
- 리액트
- React-Native
- 뷰 정체성
- 안드로이드
- panorama view
- 업로드 키 인증서
- 구조적 정체성
- 다가구 전세보증보험
- SwiftUI
- HUG보증보험지사
- 뷰 생명주기
- 네이티브
- completion handler
- 리액트 네이티브
- ssot
- @sendable
- 다가구 전세
- requirenativecomponent
- 파노라마 뷰
- 앱 성능 개선
- Today
- Total
NEON의 이것저것
Hilt 라이브러리 정리 (ft. Annotation Processing) 본문
Android Developers에서는 Hilt를 아래와 같이 설명하고 있습니다.
종속 항목 수동 삽입을 실행하는 상용구를 줄이는 Android용 종속 항목 삽입(DI) 라이브러리
Hilt는 안드로이드 컴포넌트(Activity/Fragment/Compose NavHost/ ViewModel 등)의 생명주기에 맞춰 의존성 그래프를 자동 생성하고, Gradle 플러그인 및 Annotation processing을 통해 Factory 코드를 만들어 보일러 플레이트를 줄여줍니다.
Annotation Processing: 소스코드 상단의 어노테이션(ex. @Composable)을 컴파일 시점에 스캔하여, 추가 소스/ 클래스/메타 데이터를 자동 생성하는 컴파일 확장 단계.
JSR 269 표준(플러그인 API) 위에서 동작하며, Dagger/Hilt, Room, Glide 등이 이 메커니즘으로 팩토리/바인딩 코드를 생성
Hilt 라이브러리가 보일러 플레이트를 줄여주는 과정을 자세히 설명하기 위해서는 안드로이드 프로젝트가 빌드되는 과정부터 설명이 필요한데요.
빌드 파이프라인
1. Kotlin 컴파일 전 전처리: KAPT면 스텁 생성, KSP면 심벌 분석.
1.1 KAPT(Java annotation processor를 Kotlin에서 사용 가능하게 함)
1. kotlinc가 프로세서가 읽을 수 있도록 Kotlin 소스를 Java 스텁으로 변환
2. Javac가 스텁을 기반으로 Annotation Processor를 돌려 새 소스(주로 .java)를 생성하고, 그 생성물을 포함해 컴파일
3. 이후 kotlinc가 원본 Kotlin 소스(+ 생성된 Kotlin 소스가 있다면 그것까지)를 컴파일
4. 최종적으로 모두 .class 바이트코드로 합쳐짐
1.2 KSP
1. kotlinc가 Kotlin 심벌을 직접 읽고, 프로세서가 Kotlin/Java 소스를 생성
2. 생성된 Kotlin 소스(.kt)는 kotlinc가, 생성된 Java 소스가 있다면 Javac가 각각 컴파일
3. 최종적으로 모두 .class 바이트코드로 합쳐짐
비교: KAPT는 스텁 생성 + Javac 라운드를 거쳐야 해 오버헤드가 있고, 증분/멀티플랫폼 제약이 있습니다. KSP는 스텁 없이 심벌을 직접 읽어 일반적으로 더 빠르고 증분/멀티플랫폼 지원이 낫습니다.
2. 이후 단계: 클래스 머징 → D8/DEX 변환 → 리소스 병합/패키징(APK/AAB) 등으로 빌드 마무리
여기서 Kapt 기준 2단계, KSP 기준 1단계에서 Hilt 라이브러리는 Factory 코드를 생성하게 됩니다.
// 실제 Hilt 라이브러리의 Annotation이 기입된 소스코드
@Singleton
class AuthManager @Inject constructor(
@ApplicationContext private val context: Context
) {
...
}
// 컴파일 단계에서 Annotation Processing으로 생성된 Factory 코드
@ScopeMetadata("javax.inject.Singleton")
@QualifierMetadata("dagger.hilt.android.qualifiers.ApplicationContext")
@DaggerGenerated
@Generated(
value = "dagger.internal.codegen.ComponentProcessor",
comments = "https://dagger.dev"
)
@SuppressWarnings({
"unchecked",
"rawtypes",
"KotlinInternal",
"KotlinInternalInJava"
})
public final class AuthManager_Factory implements Factory<AuthManager> {
// @ApplicationContext로 한정된 Context를 Dagger가 공급
private final Provider<Context> contextProvider;
public AuthManager_Factory(Provider<Context> contextProvider) {
this.contextProvider = contextProvider;
}
@Override
public AuthManager get() {
// contextProvider.get()으로 실제 Context를 받아 newInstance를 호출
return newInstance(contextProvider.get());
}
public static AuthManager_Factory create(Provider<Context> contextProvider) {
return new AuthManager_Factory(contextProvider);
}
public static AuthManager newInstance(Context context) {
// AuthManager의 @Inject constructor(@ApplicationContext Context)를 그대로 사용
return new AuthManager(context);
}
}
위 예시는 Hilt 라이브러리의 @Singleton 어노테이션을 코드에 기입할 때, 컴파일 과정 자동생성되는 코드입니다.
이 Factory 코드는 AuthManager 생성자 호출을 감싸며, Dagger가 DI 그래프를 조립할 때 AuthManager의 생성자에 필요한 Context를 주입해 새 인스턴스를 생성하는 역할을 수행합니다. 이로써, 런타임에 리플렉션 없이 빠른 객체 공급이 가능해집니다.
Hilt/ Dagger로 설계되는 의존성 방향 중 하나의 예시로 아래와 같은 상황 및 코드가 있습니다.
0. 의존성 방향
@HiltAndroidApp → 모듈(Repo/UseCase/Network) → @HiltViewModel → Compose hiltViewModel()/EntryPoint
1. 루트 그래프 선언
// 루트 그래프 선언
@HiltAndroidApp
class ExampleApplication : Application() {
// Inject 어노테이션을 통해 클래스를 접근하여 루트 그래프에서 사용할 수 있도록 의존성 설계
@Inject lateinit var exampleManager: ExampleManager
override fun onCreate() {
super.onCreate()
exampleManager.exampleFunc()
}
}
2. 의존성 정의 모듈
abstract class RepositoryModule {
// 도메인 인터페이스를 @Binds @SingleTon으로 구현체에 연결
@Binds
@Singleton
abstract fun bindExampleRepository(
exampleRepositoryImpl: ExampleRepositoryImpl
): ExampleRepository
...
}
3. 화면/상태 계층 주입
// @HiltViewModel + @Inject constructor로 상위 클래스를 주입받아 사용
@HiltViewModel
class ExampleViewModel @Inject constructor(
private val exampleManager: ExampleManager
) {
fun test() {
exampleManager.exampleFunc()
}
...
}
@Composable
fun ExampleView(
// Compose 네비게이션에서 hiltViewModel()로 주입된 ViewModel 사용
viewModel: ExampleViewModel = hiltViewModel()
) {
...
4. EntryPoint
@EntryPoint
@InstallIn(SingletonComponent::class)
interface AuthManagerEntryPoint {
fun authManager(): AuthManager
}
// EntryPoint를 통해 불러올 수 있도록 인터페이스를 사전에 정의
@EntryPoint
@InstallIn(SingletonComponent::class)
interface PopupManagerEntryPoint {
fun popupManager(): PopupManager
}
@Composable
fun HumaniaView(humaniaViewModel: HumaniaViewModel) {
val toastManager: ToastManager = humaniaViewModel.toastManager
val context = LocalContext.current
// EntryPointAccessors.fromApplication으로 상위 Manager 클래스들을 꺼내, UI 컨테이너에 연결
val popupManager: PopupManager = remember {
EntryPointAccessors.fromApplication(context, PopupManagerEntryPoint::class.java).popupManager()
}
val authManager = EntryPointAccessors.fromApplication(context, AuthManagerEntryPoint::class.java).authManager()
val navController = rememberNavController()
...
// 컴포저블 정의 섹션
Box {
...
PopupContainer(popupManager = popupManager)
}
루트 Compose에서 EntryPointAccessors.fromApplication으로 ToastManager, PopupManager, AuthManager 등을 꺼내 UI 컨테이너에 연결합니다.
감사합니다.
'개발지식 정리 > Kotlin' 카테고리의 다른 글
| 플레이 스토어 앱 업데이트 실패 해결 과정(certificate mismatch) (0) | 2025.12.03 |
|---|