안드로이드 연구소

[여기가 DI 설명 제일 잘함] Dagger1, Dagger2 그리고 Hilt 본문

안드로이드 연구소/의존성주입

[여기가 DI 설명 제일 잘함] Dagger1, Dagger2 그리고 Hilt

안드로이드 연구원 2023. 5. 17. 13:27

지난번에 포스트에서 의존중인 코드가 어떤것들인지,

의존중이 코드를 없애기위해 어떻게 3가지 방법으로 의존성을 주입하였는지(Dagger, Hilt, Koin아님),

가장 손쉽게 의존성을 주입한 3번째 방법을 하기위해 어떤 라이브러리들이 있었는지(Dagger, Hilt, Koin맞음)

알아보았습니다.

 

그렇다면 오늘 이 라이브러리들 사용법에 대해서 알아보겠습니다.

첫번째는 바로 Dagger입니다.

 

앞전의 의존성에 문제가 있었던 예제를 Dagger를 사용하면 어떻게 해결할 수 있는지 확인해볼까요?

class UserViewModel : ViewModel() {
    private val userRepository = UserRepository()
    // using userReposiroty instance ...
}

 

Q1. ChatGPT, 위 예제를 Dagger1를 사용한 예제를 보여줘.

최신버전: https://github.com/google/dagger/releases

dependencies {
    implementation 'com.google.dagger:dagger:<version>'
    annotationProcessor 'com.google.dagger:dagger-compiler:<version>'
}

 

첫번째로 @Module과 @Provides를 사용하여 "UserRepository 종속성을 제공하는 모듈"을 정의합니다.

UserRepository클래스가 ViewModel안에 종속성 주입이 될것이기 때문에

UserRepository를 @Provides 종속시켜준다.

@Module
class UserModule {
    @Provides
    UserRepository provideUserRepository() {
        return new UserRepository();
    }
}

 

두번째로 위에서 만든 모듈 클래스를 포함한 컴포넌트를 생성합니다.

@Component(modules = UserModule.class)
interface UserComponent {
    void inject(UserViewModel viewModel);
}

 

세번째로 viewModel에서 Dagger컴포넌트를 생성하여 의존성을 주입하면

이 수정된 코드에서 userRepository 필드에 @Inject 주석을 추가하여 주입해야 함을 나타냅니다.

UserComponent 구성 요소의 인스턴스를 만든 다음 component.inject(this)를 호출하여 주입을 수행합니다.

class UserViewModel : ViewModel() {
    @Inject
    UserRepository userRepository;

    init {
        // Create the Dagger component
        UserComponent component = DaggerUserComponent.create();

        // Perform injection
        component.inject(this);
    }
}

이제 @Inject한 userRepository를 호출없이 사용가능합니다.

한번 세팅만 해놓는다면 너무나 편하겠죠?

근데 Dagger 뭔가 복잡해보이는감이 없지 않아있네요.

이를 더 사용할 수 있도록 만든 Dagger2를 확인해볼까요?

 

 

Q2. 위 예제를 Dagger2 사용하여 해결한 예제를 보여줘.

첫번째로, 아까와 똑같이 UserModule 모듈을 생성합니다.

@Module
class UserModule {
    @Provides
    fun provideUserRepository(): UserRepository {
        return UserRepository()
    }
}

 

두번째로, UserComponent 컴포넌트 인터페이스를 생성합니다.

아까와는 달리 @Component(modules = UserModule.class) 사용해서 모듈을 연결시키지 않고

아까와 같이 상위 종속 클래스인 viewModel를 파라미터로 받게 합니다.

 

@Component
interface UserComponent {
    fun inject(viewModel: UserViewModel)
}

 

마지막으로 UserViewModel에서 

의존성을 주입할 userRepository에 @Inject어노테이션을 추가하고

생성해둔 Dagger컴포넌트를 생성하고 주입을 실행합니다.

class UserViewModel : ViewModel() {
    @Inject
    lateinit var userRepository: UserRepository

    init {
        // Create the Dagger component and perform injection
        DaggerUserComponent.create().inject(this)
    }

    // Use the injected userRepository instance...
}

 

근데 Dagger2도 Module클래스와 component인터페이스를 만들어서 관리를 해줘야하는 귀찮음이 있습니다.

이러한 수고 없이 의존성 주입 가능할까요?

 

 

Q3. 위 예제를 Hilt 사용하여 해결한 예제를 보여줘.

최신버전: https://developer.android.com/jetpack/androidx/releases/hilt?hl=ko 

 

build.gradle(app)

plugins {
    id("com.google.dagger.hilt.android") version "2.44" apply false
}

 

build.gradle(android)

plugins {
    id("dagger.hilt.android.plugin")
}

dependencies {
    implementation 'com.google.dagger:hilt-android:$version'
    kapt 'com.google.dagger:hilt-android-compiler:$version'
}

 

첫번째로 애플리케이션 클래스에서 @HiltAndroidApp으로 주석을 달아 Hilt를 활성화합니다.

애플리케이션 단위에 Hilt파일을 만들어줍니다.

HiltApplication.kt

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class HiltApplication : Application()

 

마지막으로 AndroidManifest에서

아래처럼 이름을 변경해주면 기본 세팅 끝!

    <application
        android:name=".HiltApplication"

 

두번째로 Hilt를 사용하여 종속성에 주석을 달고 삽입합니다.

class UserViewModel @ViewModelInject constructor(
    private val userRepository: UserRepository
) : ViewModel() {
    // Use the userRepository instance ...
}

이게 끝입니다!!

Dagger처럼 Module클래스도 Component인터페이스를 만들 필요없이

구성요소의 기본이되는 Application에서 Hilt를 활성화시켜서

의존성을 주입할 클래스에 맞는 @Inject어노테이션을 사용한 constructor안에 종속성 주입된 데이터를 선언합니다.

그리고 이제 종속받은 userRepository 인스턴스를 사용하면 됩니다.

 

그렇다면 이 예제에서는 @ViewModelInject 어노테이션이 쓰였는데

viewModel에서 사용해서 해당 생성자 어노테이션이 쓰였을까요?

또 어떤 생성자 어노테이션이 있었을까요?

 

 

Q4. Hilt에서 constructor annotation(생성자 어노테이션)에 어떤 종류가 있니?

1. @Inject
: Hilt에서 삽입할 생성자, 필드 또는 메서드를 표시하는 데 사용됩니다.
class UserRepository @Inject constructor() {
    // ...
}​
class UserViewModel @Inject constructor(
    private val userRepository: UserRepository
) : ViewModel() {
    // ...
}

UserRepository는 @Inject constructor()에 아무런 인자 없이 생성되었므로 다른 클래스에 삽입될 클래스임을 의미합니다.

그리고 UserViewModel에서 @Inject constructor안에 가져온 userRepository를 의존성 주입받아 인스턴스를 사용합니다.

 

만약 엑티비티에서 의존성 주입하여 사용한다면 아래와 같이 사용할 수 있다.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var userRepository: UserRepository

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // The dependencies will be injected before onCreate() is called

        // Use the injected dependencies
        userRepository.getUserData()
    }
}​

@AndroidEntryPoint로 해당 엑티비티에서 의존성 주입을 활성화하여
의존성 주입할 UserRepository를 @Inject 어노테이션하여 의존성 주입한다.
이때는 위와 다르게 UserRepository에 빈 인자를 넣은 생성자를 만들 필요는 없다.

 

2. @AssistedInject
: 일부 런타임 파라미터(컴파일 타임에 알려지는 것이 아니라 프로그램 실행 중에 결정되고 제공되는 값 또는 인수)가 필요한 클래스에 종속성을 주입하는 방법인 보조 주입에 사용됩니다.
class UserViewModel @AssistedInject constructor(
    private val userRepository: UserRepository,
    @Assisted private val userId: String
) : ViewModel() {
    // ...
}​


아래처럼 Factory클래스를 만들어서 보조 주입 시킬 수 있다.

interface UserViewModelFactory {
    fun create(userId: String): UserViewModel
}

// Usage
val userViewModel = userViewModelFactory.create(userId)

 

@Assisted 주석은 Dagger 또는 Hilt 프레임워크의 일부가 아니라
Square에서 개발한 "Assisted Injection"이라는 별도의 라이브러리의 일부입니다.
보조 주입은 파라미터를 수동으로 주입하여 사용하는데
Dagger 또는 Hilt의 자동 의존성 주입을 함께 사용할 수 있게 하였습니다.
 
3. @ViewModelInject
: ViewModel 클래스에 종속성을 주입하는 데 사용됩니다.
class UserViewModel @ViewModelInject constructor(
    private val userRepository: UserRepository
) : ViewModel() {
    // ...
}​

이번 예제에서는 "1번의 @Inject어노테이션"처럼

UserRepository클래스에 빈 인자의 @Inject constructor()를 사용하지 않습니다. 

 

그 이유는 "@ViewModelInject 어노테이션"에서는

자동으로 인자안의 의존성 주입될 클래스에 의존성주입 처리를 해놓기 때문입니다.

 

그렇다면 여기서 고민인 점은 Hilt로 개발한다면 Dagger2는 사용하지 않아도 되는 것일까요? 

Q5. Hilt는 Dagger2보다 모든 관점에서 성능이 더 우수하니?

Hilt와 Dagger2는 모두 매우 효율적이고 성능이 뛰어난 종속성 주입 프레임워크로 설계되었습니다.

실제로 Hilt와 Dagger 2의 성능 차이는 대부분의 애플리케이션에서 무시해도 될 정도라는 점은 주목할 가치가 있습니다.

따라서 성능 고려 사항에만 초점을 맞추는 것보다 프로젝트 요구 사항, 팀 전문 지식 및 선호하는 개발 스타일에 더 잘 맞는 프레임워크를 선택하는 것이 좋습니다.

성능의 관점에서는 두 라이브러리에서 차이점은 찾기 힘들것으로 보입니다.

 

하지만 제 개인적인 견해로는 Dagger는 2012년부터 오랫동안 많은 사람들에게 사용되어서

많은 자료들과 사례들이 있어서 다양하게 최적화를 해야하는 상황에서는 Dagger2를 사용하는게 우수해보입니다.

그리고 이미 Dagger2를 사용하고 있다면 Hilt로 바꿔야할 이유가 없는 것 같습니다.

 

하지만 새로운 DI라이브러리를 채택해내가야하는 관점에서 보면

러닝커브가 낮아서 사용하기 쉬우면서 간단하면서

Dagger의 기능들을 거의 다 구현 가능한

Hilt를 사용하는것이 더 유리해보입니다.

 

즉, "기존에 Dagger2쓰시는 분은 Dagger쓰고 새로 의존성 주입을 도입하면 Hilt를 쓰자!" 라는 저의 생각!

하지만 Koin과 비교해보면 어떻게 될까요?

다음시간에

Comments