들어가기 전
Clean Architecture를 공부하면서 '의존성' 낮추기, '결합도' 낮추기, '의존성' 주입, '의존성' 역전 등 지속적으로 의존성에 대한 내용이 언급되었다. 그래서 소프트웨어 설계에서 Dependency는 무엇을 의미하는지, 안드로이드에서는 이를 어떻게 적용할 수 있는지를 공부해보고자 한다.
Dependency
"A가 B를 의존한다." 라는 표현은 아래와 같은 뜻을 담고 있다.
의존대상 B가 변하면, 그것이 A에 영향을 미친다.
- 이일민, 토비의 스프링 3.1, 에이콘(2012), p113
이런 의존관계를 UML의 표현을 빌려 A--->B로 표현할 수 있다.
이는 B의 기능이 추가나 변경되면 그 영향이 A에 미친다.
B의 기능 추가나 변경이 A에 영향을 미치게되는 경우, B에게 변동사항이 있을 때 마다 A를 수정해야하는 비효율적인 상황이 발생한다. 이는 의존성을 갖는 코드를 제대로 수정하지 않으면 오류를 발생할 뿐더러, 유지보수에도 좋지 않다.
이러한 점을 방지하기 위해 인터페이스(Interface)를 사용해 클래스로 부터 의존성을 없앤다.
인터페이스를 이용하면 특정 클래스에 대한 의존성을 유연하게 만들 수 있다.
Dependency Injection (의존성 주입)
위에서 의존성이 어떤 의미인지, 의존성을 유연하게 만들기 위한 방법에 대해서 살펴보았다. 그렇다면 의존성 주입은 어떤 의미이고, 무슨 역할을 하는 걸까?
의존성 주입은 클래스 외부에서 객체를 생성해 생성한 객체를 클래스 내부에 주입하는 것이다.
아래 예시와 같이 Computer(computer1, computer2, computer3) 을 가졌다고 생각하고, 이 컴퓨터들은 하나의 모니터를 이용해 화면 분할로 내용을 출력하고 있다고 가정해보자. (컴퓨터들은 공통의 모니터를 사용한다)
이 때 Computer class 내부에서 Monitor가 인스턴스화 된다면 Computer 마다 Monitor를 생성하여 갖고 있게 된다.
이를 Computer가 Monitor에 강하게 결합되어 있다 라고 한다. 이처럼 강하게 결합되어 있을 경우 관리 하기가 어렵다.
만약 24인치 모니터 출력을 32인치 모니터로 바꾸면 위 그림에서 각 computer 클래스의 monitor를 Monitor32()로 전부 바꿔줘야한다. 24->32 만 변경했지만 3줄을 바꿔야하는 비효율이 발생한 것이다.
따라서 이를 방지하기 위해 의존성 주입을 사용한다.
위 그림에서는 외부에서 Monitor 인스턴스를 만들어 Monitor가 필요한 클래스에 주입한다. 이렇게 인스턴스를 저장하는 공간을 Container라 부른다. 이제 객체에 대한 제어 권한은 Computer에게 있는 것이 아니라 Container에게 있다. 이를 IOC(Inversion of Control) 제어의 역전이라 부른다.
이렇게 구조를 변경하면 Computer Class는 Monitor의 구현체를 외부에서 주입 받는 로직을 수행하는 것 외에는 신경 쓸 필요가 없어진다.
정리를 하면 의존성 주입을 위해 필요한 사항은
1. 의존성 : 클래스 간의 강한 의존성을 인터페이스 설계를 통해서 약한 의존성으로 만들 수 있다.
2. 결합도 : 클래스 A에 의존성이 있는 클래스 B 인스턴스를 클래스 A내부에 생성하는 것이 아니라 클래스 외부에서 생성하여 클래스 A에 주입하면 클래스 간의 결합도를 낮출 수 있다.
의존성 주입의 장점
이처럼 의존성 주입을 받는다면 클래스간의 결합도가 약해진다.
클래스간의 결합도가 약해진다는 것은 한 클래스의 변경에 다른 클래스가 변경될 필요성이 적어진다는 뜻이다.
- 결합도가 약해지면 리팩토링 및 테스트가 쉽다.
- 인터페이스 기반 설계로 코드를 유연하고 확장 가능성있게 작성할 수 있다.
- 가독성이 높아진다.
Android 에서 Dependency Injection
Android 에서는 의존성 주입을 위해 Dagger Hilt를 사용한다. 아래에서 부터 Hilt의 사용법 및 예시를 보도록 하겠다.
Hilt 사용방법
설정하기
build.gradle
buildscript {
ext.hilt_version = '2.44'
dependencies {
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
}
}
app/build.gradle
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'
dependencies {
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
}
버전 사항들은 versions object를 만들어서 변수를 사용해도 괜찮은 것 같다.
@HiltAndroidApp
Hilt를 사용하기 위해서는 안드로이드의 시작점인 Application에 @HiltAndroidApp이라고 쓴다.
@HiltAndroidApp
class MainApplication: Application() {
}
Dependency Injection
@AndroidEntryPoint
Android 클래스들에 @AndroidEntryPoint annotation을 추가해준다.
Activity, Fragment,Service,BraoadcaseReceiver에 추가할 수 있고, ViewModel에는 @HiltViewModel을 추가해준다.
@AndroidEntryPoint annotation을 추가 할 때는 의존하는 클래스들도 @AndroidEntryPoint 를 추가해주어야한다.
만약에 Fragment 에 annotation을 할 경우, 사용하는 Activity에도 함께 annotation을 추가해주어야한다.
@AndroidEntryPoint로 annotation된 각 android 클래스에 관한 개별 Hilt Component를 생성한다.
이 component 들은 Component hierarchy에 명시된 parent로부터 dependency를 받을 수 있다.
dependency를 받기 위해서는 @Inject annotation을 쓰면된다.
@Inject
클래스 내에 주입할 객체에 붙이는 annotation으로 Inject 되는 field는 private가 될 수 없다.
@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {
@Inject lateinit var analytics: AnalyticsAdapter
...
}
Hilt bindings 정의
field injection 을 수행하려면, Hilt는 dependency를 어디서부터 제공해야할지 알아야한다.
binding은 dependency에 대한 정보를 담고있다.
constructor injection
@Inject annotation 을 class의 constructor에 쓴다.
class AnalyticsAdapter @Inject constructor(
private val service: AnalyticsService
) { ... }
annotated된 클래스의 파라미터는 class의 dependency이다.
예제에서 AnalyticsAdapter는 AnalyticsService에 의존을 갖기때문에, Hilt가 AnalyticsService를 제공해줘야한다.
Hilt Modules
constructor-inject가 아니라 type이 필요한 경우 Hilt Module를 이용해 Hilt에 binding 정보를 제공해야한다.
@Module
Dagger module과 다르게 Hilt module은 @InstallIn annotation을 통해 Hilt가 어떤 Android class를 install 해야하는지를 알린다.
1. interface Injection @Binds
AnalyticsService가 인터페이스라면 이 인터페이스를 constructor-inject 할 수 없다. 대신 Hilt module 내에 @Binds로 지정된 추상함수를 생성해 Hilt에 binding 정보를 제공한다.
interface AnalyticsService {
fun analyticsMethods()
}
// Constructor-injected, because Hilt needs to know how to
// provide instances of AnalyticsServiceImpl, too.
class AnalyticsServiceImpl @Inject constructor(
...
) : AnalyticsService { ... }
@Module
@InstallIn(ActivityComponent::class)
abstract class AnalyticsModule {
@Binds
abstract fun bindAnalyticsService(
analyticsServiceImpl: AnalyticsServiceImpl
): AnalyticsService
}
2. 외부라이브러리 Injection @Provides
contructor-injection이 불가능 한 것은 interface만 있는 것이 아니다.
외부라이브러리 (retrofit) 의 경우에는 모듈 내 함수를 생성하고 @Provides 주석을 지정해 이 유형의 인스턴스 제공을 Hilt에게 알릴 수 있다.
@Module
@InstallIn(ActivityComponent::class)
object AnalyticsModule {
@Provides
fun provideAnalyticsService(
// Potential dependencies of this type
): AnalyticsService {
return Retrofit.Builder()
.baseUrl("https://example.com")
.build()
.create(AnalyticsService::class.java)
}
}
3. 같은 type에 대해 여러개 Binding @Qualifier
같은 type에 대해 다른 구현체를 제공하고 싶을 때, multiple binding을 제공해야한다.
이 때 @Qualifier annotation을 사용하면 된다.
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthInterceptorOkHttpClient
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class OtherInterceptorOkHttpClient
@Module
@InstallIn(ApplicationComponent::class)
object NetworkModule {
@AuthInterceptorOkHttpClient
@Provides
fun provideAuthInterceptorOkHttpClient(
authInterceptor: AuthInterceptor
): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(authInterceptor)
.build()
}
@OtherInterceptorOkHttpClient
@Provides
fun provideOtherInterceptorOkHttpClient(
otherInterceptor: OtherInterceptor
): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(otherInterceptor)
.build()
}
참고 레퍼런스
https://tecoble.techcourse.co.kr/post/2021-04-27-dependency-injection/
https://software-creator.tistory.com/35
https://aroundck.tistory.com/7985
'Android > 공부' 카테고리의 다른 글
[Android] LifecycleOwner란? viewLifecycleOwner와의 비교 (0) | 2023.08.24 |
---|---|
[Android] UI State란 무엇일까? (0) | 2023.08.16 |
[Android] Clean Architecture + MVVM 패턴 (1) | 2023.08.06 |
[Android] 알림 커스텀과 안드로이드 13 알림 권한 설정 (0) | 2023.07.21 |
[Android] Intent 정리하기 (0) | 2023.04.26 |