안드로이드 아키텍처 용어 사전
DIDI · Dagger

Hilt

Dagger 기반의 컴파일 타임 의존성 주입 라이브러리. 안드로이드 컴포넌트에 자동으로 연결된다.

Dagger를 안드로이드에 맞게 감싼 컴파일 타임 DI 라이브러리.

DI가 푸는 문제

// ❌ 직접 만들면 결합이 생긴다
class HomeViewModel {
    private val repo = UserRepositoryImpl(RetrofitApi(), RoomDao())   // 구현에 묶인다
}
// → 테스트에서 가짜로 바꿀 수 없다
// → 의존이 늘면 생성 코드가 폭발한다

// ✅ 받아 쓰면 교체 가능하다
class HomeViewModel @Inject constructor(private val repo: UserRepository)

Hilt의 구성

@HiltAndroidApp
class MyApp : Application()

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    @Provides @Singleton
    fun provideApi(): UserApi = Retrofit.Builder()...create()
}

@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
    @Binds                                   // 인터페이스 → 구현 연결
    abstract fun bindUserRepository(impl: UserRepositoryImpl): UserRepository
}

@AndroidEntryPoint
class HomeFragment : Fragment() {
    private val viewModel: HomeViewModel by viewModels()
}

@HiltViewModel
class HomeViewModel @Inject constructor(private val repo: UserRepository) : ViewModel()

@Provides는 생성 방법을 적을 때, @Binds는 인터페이스를 구현에 연결할 때 쓴다. 후자가 더 효율적이다(코드 생성이 적다).

스코프 — 객체의 수명

  • @Singleton — 앱 전체 (SingletonComponent)

  • @ActivityRetainedScoped 구성 변경을 넘어 유지 (ViewModel 과 같은 수명)

  • @ActivityScoped — Activity 수명

  • @FragmentScoped — Fragment 수명

  • @ViewModelScoped — ViewModel 수명

  • 스코프를 안 붙이면 요청할 때마다 새 인스턴스가 만들어진다

스코프를 잘못 잡으면

  • 너무 길게 → 메모리 누수 (Activity 를 Singleton 이 참조)
  • 너무 짧게 → 불필요한 재생성 (매번 Retrofit 인스턴스 생성)

Koin과의 트레이드오프

  • Hilt — 컴파일 타임 검증 — 의존성이 빠지면 빌드가 실패한다

    • 코드 생성이라 빌드 시간이 는다 (KSP 로 많이 개선됐다)
    • 안드로이드 컴포넌트 통합이 매끄럽다
  • Koin — 런타임 DI — DSL 로 선언한다. 학습이 쉽고 빌드가 빠르다

    • 의존성 누락이 런타임에 드러난다 (앱이 실행 중 크래시)
    • 검증 테스트를 따로 돌려야 한다
  • 규모가 크고 팀이 여럿이면 Hilt, 작고 빠른 반복이면 Koin

면접 함정

  • "DI는 프레임워크가 있어야 한다" → 생성자로 넘기는 것이 이미 DI다. 컨테이너는 그걸 자동화할 뿐이다.
  • "@Singleton을 붙이면 안전하다" → 상태를 가지면 여전히 위험하고, Activity를 참조하면 누수다.

의존성 그래프가 실제로 어떻게 검증되나

빌드 시 Hilt 가 그래프를 만든다 UserApi 가 필요한데 @Provides 가 없다

  • [Dagger/MissingBinding] UserApi cannot be provided...
  • 빌드 실패. 앱이 실행되기 전에 잡힌다

순환 의존도 빌드에서 잡힌다 A → B → A → [Dagger/DependencyCycle]

같은 타입을 두 개 제공해야 할 때 — 한정자

@Qualifier @Retention(AnnotationRetention.BINARY)
annotation class AuthClient

@Provides @AuthClient
fun provideAuthClient(): OkHttpClient = OkHttpClient.Builder().addInterceptor(auth).build()

@Provides
fun providePublicClient(): OkHttpClient = OkHttpClient()

class TokenRepository @Inject constructor(@AuthClient private val client: OkHttpClient)

한정자가 없으면 DuplicateBindings 빌드 에러가 난다 — 런타임에 엉뚱한 걸 받는 일은 없다.

테스트에서 바꿔치기

@HiltAndroidTest
@UninstallModules(NetworkModule::class)
class HomeScreenTest {
    @BindValue @JvmField val api: UserApi = FakeUserApi()
}

@UninstallModules 로 실제 모듈을 떼고 @BindValue 로 가짜를 꽂는다 = DI 를 쓰는 가장 실질적인 이유

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 의존성 주입 — Hilt·Koin