[Clean Architecture] Dependency Injection

ee·2024년 10월 5일

Clean Architecture

목록 보기
3/6

Dependency Injection이란

의존성 주입은 클래스 내부에서 다른 클래스의 인스턴스를 생성하지 않고 외부에서 주입하여 객체간의 결합도를 낮추고 유지보수와 테스트의 용이성을 증가시키는 데 도움이 된다.

간단하게 예를 들자면

class Car(){
	val engine = Engine()
}

이런식으로 클래스 내부에서 다른 클래스의 인스턴스를 생성하는 것은 의존성 문제가 생길 수 있다는 것이다. 따라서 등장한 것이 dagger hilt 라이브러리이다.

Dagger2

Dagger2에서 DI를 구현하는 방법은 바로 외부 컨테이너에서 의존성 객체를 생성하여 요청받은 인스턴스에 생성된 객체를 주입하는 것이다.

class UserRepository(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

-->

class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

실제로는 더 많은 코드가 있지만 직관적으로 이해하기 쉽게 생략했다.
dagger2의 핵심 개념은 5가지가 있다.

  • Inject
    의존성 주입을 요청. Inject 어노테이션으로 주입을 요청하면 연결된 Component가 Module로부터 객체를 생성하여 넘긴다.

  • Component
    연결된 Module을 이용하여 의존성 객체를 생성하고, Inject로 요청받은 인스턴스에 생성한 객체를 주입한다. 의존성을 요청받고 주입하는 Dagger의 주된 역할을 수행한다.

  • Subcomponent
    Component는 계층관계를 만들 수 있다. Subcomponent는 Inner Class 방식의 하위계층 Component이다. Sub의 Sub도 가능. Subcomponent는 Dagger의 중요한 컨셉인 그래프를 형성한다. Inject로 주입을 요청받으면 Subcomponent에서 먼저 의존성을 검색하고, 없으면 부모로 올라가면서 검색한다.

  • Module
    Component에 연결되어 의존성 객체를 생성한다. 생성 후 Scope에 따라 관리도 한다.

  • Scope
    생성된 객체의 Lifecycle 범위이다. 안드로이드에서 주로 PerActivity, PerFragment 등으로 화면의 생명주기와 맞추어 사용한다. Module에서 Scope을 보고 객체를 관리한다.

Hilt

hilt는 dagger를 쉽게 사용하기 위해 만들어졌다. hilt는 코드의 재사용성, 테스트의 편의성, 보일러 플레이트 코드 감소등 여러 이점이 있다.

@HiltAndroidApp

@HiltAndroidApp
class ExampleApplication : Application() { ... }

Hilt를 사용하는 모든 앱은 @HiltAndroidApp으로 주석이 지정된 Application 클래스를 포함해야 한다. 최상위 컴포넌트(컨테이너)에 해당하며 Hilt 코드를 생성시켜준다.

@AndroidEntryPoint

@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() { ... }

@AndroidEntryPoint 어노테이션이 달린 android 클래스는 해당 클래스의 생명주기를 따르는 컴포넌트(컨테이너)를 만든다.
Hilt는 현재 다음 Android 클래스를 지원한다.

  • Application(@HiltAndroidApp을 사용하여)
  • ViewModel(@HiltViewModel을 사용하여)
  • Activity
  • Fragment
  • View
  • Service
  • BroadcastReceiver

Android 클래스에 @AndroidEntryPoint로 주석을 지정하면 이 클래스에 종속된 Android 클래스에도 주석을 지정해야 한다. 예를 들어 프래그먼트에 주석을 지정하면 이 프래그먼트를 사용하는 액티비티에도 주석을 지정해야 한다.

@Inject

@AndroidEntryPoint는 프로젝트의 각 Android 클래스에 관한 개별 Hilt 컴포넌트를 생성한다. 이러한 컴포넌트는 Component Hierarchy에 설명된 대로 해당하는 각 상위 클래스에서 종속 항목을 받을 수 있다.

컴포넌트에서 종속 항목을 가져오려면 다음과 같이 @Inject 주석을 사용하여 필드 삽입을 실행한다.

@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {

  @Inject lateinit var analytics: AnalyticsAdapter
  ...
}

Hilt에서 삽입된 필드는 private이 될 수 없다.

  • constructor injection

constructor injection은 두 가지 기능이 있는데 현재 클래스를 종속 항목으로 주입하거나, 현재 클래스에서 종속 항목을 주입받을 때 사용한다.

class DatabaseService @Inject constructor(){

    fun log(message : String){
        Log.d(TAG,"Database Service $message")
    }
}
class DatabaseAdapter @Inject constructor(var dataBaseService: DatabaseService){
    fun log(message : String){
        Log.d(TAG, "DatabaseAdapter : $message")
        dataBaseService.log(message)
    }
}

위의 예시처럼 DatabaseService를 @Inject constructor를 사용해 Hilt에게 해당 타입을 제공하는 방법을 알려주고 DatabaseAdapter에 constructor에 dataBaseService를 주입하여 이를 인스턴스화 하여 내부함수 log를 사용할 수 있게 된다.

Hilt Modules

외부 라이브러리나 인터페이스는 constructor inject할 수 없다. 이 경우에는 Hilt modules을 사용해서 Hilt에 결합 정보를 제공할 수 있다.

Hilt 모듈은 @Module로 어노테이션이 지정된 클래스다. 이 모듈은 특정 유형의 인스턴스를 제공하는 방법을 Hilt에 알려준다. 그리고 반드시 @InstallIn 어노테이션을 지정하여 각 모듈을 사용하거나 설치할 Android 클래스를 Hilt에 알려야 한다.

@Binds

앞서 말한듯이 인터페이스는 constructor inject가 불가능 하기 때문에 대신 Hilt 모듈 내에 @Binds로 주석이 지정된 추상 함수를 생성하여 Hilt에 결합 정보를 제공한다.

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
}

AnalyticsService는 인터페이스고 AnalyticsServiceImpl은 다른 곳에 인스턴스로 주입될 것이기 때문에 constructor injection이 사용되었다.
Hilt가 AnalyticsModule의 종속 항목을 ExampleActivity에 삽입하기를 원하기 때문에 Hilt 모듈 AnalyticsModule에 @InstallIn(ActivityComponent.class) 어노테이션을 지정했다. 이 어노테이션은 AnalyticsModule의 모든 종속 항목을 앱의 모든 activity에서 사용할 수 있음을 의미한다.

@Provides

@Provides 어노테이션은 인터페이스와 외부 라이브러리를 Constructor injection 할 때 사용할 수 있다.

@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)
  }
}

@Provides 어노테이션이 달린 함수는 다음 정보를 Hilt에게 제공한다.

  • 함수 반환 유형은 함수가 어떤 유형의 인스턴스를 제공하는지 Hilt에 알려준다.
  • 함수 매개변수는 해당 유형의 종속 항목을 Hilt에 알려준다.
  • 함수 본문은 해당 유형의 인스턴스를 제공하는 방법을 Hilt에 알려준다. Hilt는 해당 유형의 인스턴스를 제공해야 할 때마다 함수 본문을 실행한다.

@Qualifier

인터페이스에 여러 구현이 있을 때 Hilt가 어떤 것을 주입할 지 알기 위해 @Qualifier 어노테이션을 사용하여 구분한다.

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

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

@Module
@InstallIn(SingletonComponent::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()
  }
}

// As a dependency of another class.
@Module
@InstallIn(ActivityComponent::class)
object AnalyticsModule {

  @Provides
  fun provideAnalyticsService(
    @AuthInterceptorOkHttpClient okHttpClient: OkHttpClient
  ): AnalyticsService {
      return Retrofit.Builder()
               .baseUrl("https://example.com")
               .client(okHttpClient)
               .build()
               .create(AnalyticsService::class.java)
  }
}

// As a dependency of a constructor-injected class.
class ExampleServiceImpl @Inject constructor(
  @AuthInterceptorOkHttpClient private val okHttpClient: OkHttpClient
) : ...

// At field injection.
@AndroidEntryPoint
class ExampleActivity: AppCompatActivity() {

  @AuthInterceptorOkHttpClient
  @Inject lateinit var okHttpClient: OkHttpClient
}

@Singleton

@Singleton 어노테이션은 인스턴스를 주입할 때마다 동일한 인스턴스를 주입할 때 사용한다. 데이터베이스 객체 같은 비용이 많이 드는 객체는 @Singleton 어노테이션을 사용해 동일한 인스턴스를 제공하는 것이 좋다.

@Module
@InstallIn(SingletonComponent::class)
abstract class AnalyticsModule {

  @Singleton
  @Binds
  abstract fun bindAnalyticsService(
    analyticsServiceImpl: AnalyticsServiceImpl
  ): AnalyticsService
}

// If you don't own AnalyticsService.
@Module
@InstallIn(SingletonComponent::class)
object AnalyticsModule {

  @Singleton
  @Provides
  fun provideAnalyticsService(): AnalyticsService {
      return Retrofit.Builder()
               .baseUrl("https://example.com")
               .build()
               .create(AnalyticsService::class.java)
  }
}

마무리

Hilt를 언젠간 공부해야겠다고 생각했는데 한 번 정리를 쭉 해서 뿌듯하다. 실습을 통해서 여러번 사용해보면서 익히는 것이 좋을 것 같다.

profile
정진이

0개의 댓글