지난 팀 프로젝트에서도, 이번에 진행 중인 개인 프로젝트에서도 의존성 주입 시에 Hilt를 사용하고 있다.
그런데 누군가 의존성 주입을 왜 하는 건데요? Hilt는 왜 사용하는 거죠?라고 묻는다면
말문이 막힐 것 같다. (정리 전 예상 답:.. 유지 보수?)
Android 공식 문서를 읽어보면서 포스팅으로 한 번 정리해 보자!
의존성 주입(Dependency injection, DI)이란?
의존성 주입을 위키백과에서 찾아보면 다음처럼 소개하고 있다.
소프트웨어 엔지니어링에서 의존성 주입(dependency injection)은 하나의 객체가 다른 객체의 의존성을 제공하는 테크닉이다.
알기 쉽게 설명하면 클래스에서 필요한 객체를 직접 생성하지 않고, 외부에서 생성해 전달하는 방식을 말한다.
왜 필요한가?
Android Developer 공식 문서에는 DI의 이점으로 다음과 같이 말하고 있다.
DI를 사용한 코드와 사용하지 않은 코드를 비교하면서 해당 이점을 들여다보자.
// DI를 사용하지 않은 코드
class Car {
private val engine = Engine()
fun start() {
engine.start()
}
}
fun main(args: Array) {
val car = Car()
car.start()
}
// DI를 사용한 코드
class Car(private val engine: Engine) {
fun start() {
engine.start()
}
}
fun main(args: Array) {
val engine = Engine()
val car = Car(engine)
car.start()
}
코드 재사용 가능
위 코드에서 DI를 사용하지 않은 코드의 Car와 Engine은 서로 밀접하게 결합이 된 상태다.
만약 Engine을 다른 것으로 교체하고 싶을 때, 혹은 다양한 유형의 Engine을 사용하고자 한다.
DI를 사용하지 않은 코드에서는 또 다른 Car 객체를 생성해야 한다.
그러나 DI를 사용한 코드에서는 다른 유형의 Engine을 생성자로 전달하여 Car 객체를 만들 수 있게 된다.
리팩터링 편의성
DI를 사용한 코드를 살펴보면, Car 클래스는 Engine이 필요하다는 정보가 한 눈에 들어온다.(가독성)
코드의 가독성이 좋으면 코드를 쉽게 더 이해할 수 있고, 추후 고치기에도 편리하다.(유지보수)
테스트 편의성
만약 클래스에서 필요한 객체가 Android 의존성을 가지고 있다고 가정해보자.
그럼 Test 환경에서는 실제 객체를 사용할 수 없기 때문에 가짜 객체를 생성하게 된다.(Mocking)
외부에서 의존성을 주입하게 되는 경우, 가짜 객체를 외부에서 주입만 하면 되므로 코드가 줄어들게 된다.
주입 방식
의존성을 주입하는 방식은 다음 두 가지가 있다.
생성자 삽입
위의 예시 코드처럼 생성자로 필요한 객체를 주입한 경우이다.
필드 삽입(혹은 setter 삽입)
아래처럼 필드로 삽입한 경우다.
Activity, Fragment와 같은 경우에는 생성자 주입이 불가능한 컴포넌트로 해당 방법을 사용해야 한다.
nullable한 타입으로 변환하거나, by lazy, 혹은 아래처럼 lateinit으로 선언하여 사용 가능하다.
class Car {
lateinit var engine: Engine
fun start() {
engine.start()
}
}
fun main(args: Array) {
val car = Car()
car.engine = Engine()
car.start()
}
사용 방법
그렇다면 개발 시에 의존성 주입을 하기로 결정했을 때, 어떻게 적용할 수 있을까?
실제 아래의 Android의 권장 앱 아키텍처에 따라 구현하게 될 때 의존성 주입을 고려하게 된다.
수동 삽입
Container와 ViewModelFactory를 구현해서 사용하는 방법으로, 실제 사용법은 공식 문서에서 확인하자.
처음 의존성 주입 개념을 알고 나서 사용했고, 다른 방법에 비해 접하기 쉬웠다.
하지만 종속 항목이 필요해질 때마다 수동으로 입력해주어야 하는 불편함이 있어서
앱의 규모가 더 커질 때에는 복잡해진다는 단점이 있다.
// Container of objects shared across the whole app
class AppContainer {
// Since you want to expose userRepository out of the container, you need to satisfy
// its dependencies as you did before
private val retrofit = Retrofit.Builder()
.baseUrl("https://example.com")
.build()
.create(LoginService::class.java)
private val remoteDataSource = UserRemoteDataSource(retrofit)
private val localDataSource = UserLocalDataSource()
// userRepository is not private; it'll be exposed
val userRepository = UserRepository(localDataSource, remoteDataSource)
}
DI Framework
DI Framework가 없다면, 아무래도 Service Locator 패턴을 사용했어야 했을 것이다.
Service Locator 패턴이란?
디자인 패턴 중 하나로, 만약 DI Framework가 없다면 Service Locator 패턴을 사용해야 한다.
Service Locator는 단순하게 서비스가 모여져 있는 곳을 말하며, 필요한 객체를 제공하는 책임을 갖게 된다.
object ServiceLocator {
fun getEngine(): Engine = Engine()
}
class Car {
private val engine = ServiceLocator.getEngine()
fun start() {
engine.start()
}
}
fun main(args: Array) {
val car = Car()
car.start()
}
Android Developer 공식 문서에 의존성 주입의 대안이라고 나와있지만,
Service Locator가 안티 패턴이라고 말하기도 한다.
DI를 사용하지 않았을 때와 비슷한데, 첫 번째로는 코드에서 클래스의 의존성을 확인하기 어렵다는 점이다. 이는 가독성 측면에서 안 좋아져서 코드가 추가될 때마다 어디서 오류가 발생하는지 파악하기 어렵기 때문에 유지보수가 어려워진다.
두 번째는 코드를 테스트하기가 어렵다. 모든 테스트가 전역적인 Service Locator와 상호작용해야 하기 때문이다.
세 번째는 수명 주기 관리가 까다롭다. 객체를 앱의 수명주기가 아니라 다르게 지정하고 싶을 때, 객체의 수명 주기를 관리하기 어렵다.
그러나 Android에는 DI Framework가 있어서 의존성 주입을 편리하게 구현할 수 있다.
수동 주입에서 A가 B를 필요로 하고, B가 C를 필요로 하고, C가 D를 필요로 하는 등 복잡해지면 Container를 관리하기가 버거워질 것이다.
Hilt와 같은 DI Framework를 사용하면 수동으로 주입할 때보다 보일러 플레이트 코드가 확연히 줄게 되고, 수명 주기를 자동으로 관리해준다.
DI Framework는 Hilt 외에도 Dagger, Koin 등이 있다.
이 중 사용해 본 Framework는 Hilt다.
처음 접했을 때는 수동 주입에 비해 이해가 어려웠지만, 사용법을 익히는 순간부터 신세계..가 펼쳐진다.
게다가 Hilt의 전 버전(?)인 Dagger에 비해서는 쉬운 편이라고 생각한다...(알아야 할 게 산더미라 상대적으로 어려움)
Koin은 아직 해보진 않았다!
요약
- DI: 클래스에 필요한 객체를 외부에서 전달해주는 것
- 이점: 테스트 용이, 리팩토링의 편리함, 코드의 재사용성 증가
- 방식: 생성자 삽입 혹은 field 삽입
- DI Framework: 수동 삽입의 경우 프로젝트의 규모가 커지면 Container 관리가 매우 어렵다. Hilt와 같은 DI Framework를 사용하면 보일러 플레이트 코드가 줄어들며, 수명 주기를 자동으로 관리해주어서 사용하게 된다.
'Android' 카테고리의 다른 글
[적용기] Clean Architecture는 정말 좋을까? (2) | 2023.05.13 |
---|---|
[적용기] MVVM ViewModel과 AAC ViewModel (4) | 2023.05.10 |
[TIL/개념] Android의 Context와 ApplicationContext (0) | 2023.04.30 |
[TIL/개념] Activity와 Fragment (2) - Fragment와 Fragment의 lifecycle (0) | 2023.03.21 |
[TIL/개념] Activity와 Fragment (1) - Activity와 Activity의 lifecycle (0) | 2023.03.21 |