안드로이드 개발자 JD를 보면 Hilt를 쉽게 볼 수 있다.
안드로이드 개발을 접한지 얼마 안되었을 시기에는 Hilt가 너무너무 어려웠다.
하지만 이제는 Hilt 없는 안드로이드 프로젝트를 찾아보기 어려울 정도로 안드로이드 계의 기본 소양이 되었다.
오늘은 Hilt, 그리고 DI에 대해 탐구해보려 한다!
안드로이드 개발을 하다 보면 DI(Dependency Injection)를 “편해서 쓰는 도구” 정도로 받아들이기 쉽다.
하지만 실제로 DI는 단순한 편의 기능이 아니라, 변경에 강한 구조를 만들기 위한 설계 기법이다.
이 글에서는
DI를 사용하지 않고 ViewModel에서 Repository를 직접 생성한다고 가정해보자.
class ReviewViewModel : ViewModel() {
private val repository = ReviewRepository(
apiService,
localDataSource
)
}
이 구조에는 몇 가지 문제가 있다.
ViewModel은 본래 상태 관리와 UI 로직에 집중해야 하지만,
Repository를 어떻게 생성하는지까지 알고 있다.
여러 ViewModel에서 같은 Repository를 사용한다면,
각 ViewModel마다 동일한 생성 코드가 반복된다.
만약 ReviewRepository 생성자에 파라미터가 하나 추가된다면 어떻게 될까?
class ReviewRepository(
apiService: ApiService,
localDataSource: LocalDataSource,
logger: Logger
)
이 경우,
ReviewRepository를 생성하는 모든 ViewModel을 수정해야 한다이 구조는 변경에 닫혀 있지 않다.
DI를 적용하면 ViewModel은 더 이상 Repository를 생성하지 않는다.
@HiltViewModel
class ReviewViewModel @Inject constructor(
private val repository: ReviewRepository
) : ViewModel()
Repository는 별도의 Module에서 생성된다.
@Module
@InstallIn(SingletonComponent::class)
object RepositoryModule {
@Provides
fun provideReviewRepository(
apiService: ApiService,
localDataSource: LocalDataSource
): ReviewRepository {
return ReviewRepository(apiService, localDataSource)
}
}
여기서 중요한 변화는 단 하나다.
ViewModel은 Repository를 “어떻게 만드는지”를 더 이상 모른다
이제 ReviewRepository 생성자가 변경되더라도,
fun provideReviewRepository(
apiService: ApiService,
localDataSource: LocalDataSource,
logger: Logger
): ReviewRepository
즉,
이 지점에서 개방–폐쇄 원칙(OCP) 과 연결된다.
개방–폐쇄 원칙은 다음과 같이 정의된다.
확장에는 열려 있고, 변경에는 닫혀 있어야 한다
DI 구조에서 이를 다시 해석하면 다음과 같다.
구성(configuration)은 바뀔 수 있지만, 사용 코드는 닫혀 있다
DI는 의존성 생성 책임을 분리함으로써
변경이 필요한 지점과 변경되지 않아야 할 지점을 명확히 나눈다.
이것이 DI가 OCP를 자연스럽게 만족시키는 이유다.
DI의 장점을 설명할 때 흔히 “객체를 하나만 만들어서 재사용한다”는 이야기가 나온다.
하지만 이는 부가적인 효과일 뿐, 핵심은 아니다.
DI를 사용하면서 싱글톤을 선택할 수는 있지만,
DI의 본질은 재사용이 아니라 변경 관리에 있다.
DI가 유지보수성을 높이는 이유는 단순하다.
이 구조는 자연스럽게 개방–폐쇄 원칙을 만족시킨다.
DI는 객체를 주입하는 기술이 아니라,
변경에 강한 구조를 만드는 설계 도구다.