주입된 객체가 얼마나 오래 살고 누구와 공유되는지를 정하는 범위.
-
@Singleton — 앱 프로세스 전체
-
@ActivityRetainedScoped — 구성 변경을 넘어 유지 (ViewModel 과 같은 수명)
-
@ViewModelScoped — ViewModel 하나의 수명
-
@ActivityScoped — Activity 하나 (회전하면 새로 만들어진다)
-
@FragmentScoped — Fragment 하나
-
@ServiceScoped — Service 하나
-
스코프를 안 붙이면 = 주입할 때마다 새 인스턴스
무엇을 어디에 두나
-
@Singleton — OkHttpClient · Retrofit · Room 데이터베이스 · DataStore
- 만드는 비용이 크고 상태가 안전하며 앱 전체가 공유한다
-
스코프 없음 — Mapper · UseCase · 순수 함수 객체
- 상태가 없어 만드는 비용이 거의 0 이다
-
@ActivityRetainedScoped
- 한 화면 흐름 안에서 여러 ViewModel 이 공유할 상태
- (예: 여러 단계로 나뉜 가입 플로우)
@Module
@InstallIn(SingletonComponent::class)
object DataModule {
@Provides @Singleton
fun provideDb(@ApplicationContext ctx: Context): AppDatabase =
Room.databaseBuilder(ctx, AppDatabase::class.java, "app.db").build()
@Provides // 스코프 없음 — DAO 는 DB 가 캐싱한다
fun provideUserDao(db: AppDatabase): UserDao = db.userDao()
}
스코프를 잘못 잡으면
너무 길다 @Singleton 객체가 Activity 를 참조한다
-
Activity 가 파괴돼도 GC 되지 않는다 = 메모리 누수 @Singleton 객체가 사용자별 상태를 들고 있다
-
로그아웃 후에도 이전 사용자 데이터가 남는다
너무 짧다 Retrofit 을 스코프 없이 제공한다
- 주입될 때마다 새 OkHttp 커넥션 풀이 만들어진다
- 커넥션 재사용이 안 되고 메모리가 는다
컴포넌트 계층
SingletonComponent
└ ActivityRetainedComponent
├ ViewModelComponent
└ ActivityComponent
└ FragmentComponent
└ ViewComponent
아래는 위의 바인딩을 볼 수 있다 (Fragment 가 @Singleton 객체를 주입받을 수 있다)
위는 아래를 못 본다 (@Singleton 이 @ActivityScoped 객체를 주입받으면 빌드 실패)
면접 함정
- ❌ "@Singleton을 많이 붙일수록 빠르다" → 앱이 살아 있는 내내 메모리를 점유한다. 생성 비용이 클 때만이다.
- ❌ "싱글턴이니 스레드 안전하다" → 오히려 반대다. 여러 곳이 동시에 접근하므로 상태가 있으면 위험하다.
실제로 누수가 어떻게 생기나
// ❌ 앱 전체 수명 객체가 Activity Context 를 붙든다
@Provides @Singleton
fun provideImageLoader(ctx: Context): ImageLoader = ImageLoader(ctx)
// ✅ Application Context 를 명시적으로 받는다
@Provides @Singleton
fun provideImageLoader(@ApplicationContext ctx: Context): ImageLoader = ImageLoader(ctx)
Hilt 는 @ApplicationContext 와 @ActivityContext 를 다른 한정자로 구분한다
- SingletonComponent 에서 @ActivityContext 를 요청하면 빌드가 실패한다
- 이 실수를 컴파일 타임에 막아 준다
로그아웃 시 상태를 어떻게 비우나
-
@Singleton 캐시에 사용자 데이터가 남아 다음 계정으로 로그인해도 보인다
-
대응 1 — 로그아웃 시 명시적으로 clear() 를 부른다 (호출 누락 위험)
-
대응 2 — 사용자 범위 컴포넌트를 직접 정의한다 (@DefineComponent)
-
대응 3 — DB 를 SSOT 로 두고 로그아웃 시 테이블을 비운다 — 가장 단순하고 확실하다
확인하는 법
// 같은 인스턴스인지 로그로 확인한다
class HomeViewModel @Inject constructor(repo: UserRepository) : ViewModel() {
init { Log.d("DI", "repo=${System.identityHashCode(repo)}") }
}
// 화면을 여러 번 열어 값이 같으면 공유, 다르면 매번 새로 생성이다