Developing Myself Everyday
이 게시글은 아래의 게시글에서 이어진 내용입니다.

 

 

[1] MVVM Repository에 Room과 Hilt 사용하기

만약 MVVM Repository 패턴에 대한 이해가 부족하다면 아래의 게시글을 갔다가 오기 바란다. MVVM Pattern with Repository Pattern(저장소 패턴) by Kotlin MVVM에 대한 설명은 아래의 게시글에서 확인하길 바란다.

everyday-develop-myself.tistory.com


 

Hilt & Dagger2


Hilt는 Dagger2를 기반으로 한 안드로이드용 의존성 주입 라이브러리이다. 안드로이드 애플리케이션의 구성 요소 간의 의존성을 관리하고 의존성을 주입하는 기능을 제공한다.

 

 

Dagger2

Dagger2는 Google에서 개발한 Java 및 Kotlin 용 의존성 주입 라이브러리이다. 컴파일 타임에 의존성 그래프를 생성하므로 런타임에 주입 로직의 오버헤드가 없고 성능이 우수하다. 주입할 의존성을 명시적으로 선언하고 그래프를 구성하기 위해 주입 대상 클래스에 주석 기반의 코드를 작성해야 한다.

 

😖 하지만 이런 Dagger2 에도 단점은 존재한다. 바로 배우기 어렵고, 프로젝트 설정이 어렵다는 것이다. Dagger2만의 개념 코드를 공부해야 하기 때문에 러닝 커브가 매우 높다. 그리고 설정이 매우 번거롭고 같은 결과에 대한 다양한 방법이 존재하게 된다.

 

그래서 이전 단점을 해소해주기 위해 나온것이 바로 Hilt이다.

 

 

Hilt

Hilt는 안드로이드 애플리케이션의 의존성 주입을 보다 쉽고 간편하게 구현할 수 있도록 지원한다. Hilt는 주로 안드로이드 프레임워크 구성요소 (Activity, Fragment, ViewModel 등) 및 AndroidX 라이브러리와의 통합을 제공한다. Hilt는 컴포넌트 및 모듈을 사용해 의존성을 정의하고 주입한다.

 

 

Dagger2에는 자유로운 커스터마이징과 강력한 성능을 제공하지만, 그에따른 복잡한 설정과 코드작성이 필요하다. 반면에 Hilt는 Dagger2의 기능을 활용하면서 안드로이드 애플리케이션에서 보다 쉽고 편리하게 의존성 주입을 구현할 수 있다. Hilt는 안드로이드 프레임워크와의 통합을 강화하여 개발자들이 안드로이드 앱 개발에 집중할 수 있도록 도와준다.

 

 

 

 

MVVM 저장소 패턴에 Room과 Hilt를 사용


저번 게시글에서 Room을 이용해 Database를 구현하였다. 이제 이를 사용하기 위한 설정을 완료하도록 하겠다.

 

Hilt 설정 

Hilt를 사용하기위해 필수로 선행되어야 하는 부분이다.

 

@HiltAndroidApp
class App : Application()

 

위의 같은 애플리케이션 클래스에 @HiltAndroidApp 어노테이션을 추가한다.

 

그리고 AndroidManifest.xml 에 Application에 name을 App으로 바꿔준다.

 

<application
        android:name=".App"
        
        ....
        
	</application>

 

 

 

Repository 구현

이제 ViewModel에서 데이터베이스로 접근하기 위한 저장소를 만들어야 한다. 저장소는 Room 데이터베이스와 ViewModel간의 중간 계층으로 동작한다. 저장소에서는 데이터베이스와 상호 작용하는 메서드를 구현하면 된다.

 

 

저장소 인터페이스

interface UserRepository {
    suspend fun insertUser(user: User)
    suspend fun getUserByEmail(email: String): User?
}

 

저장소 구현 클래스

class UserRepositoryImpl @Inject constructor(
    private val userDao: UserDao
) : UserRepository {
    override suspend fun insertUser(user: User) =
        userDao.insertUser(user)


    override suspend fun getUserByEmail(email: String): User? =
        userDao.getUserByEmail(email)
}

 

UserRepositoryImpl 클래스는 @Inject 어노테이션을 사용해 의존성 주입을 받는다. userDao라는 UserDao 인터페이스 의존성을 주입받게 된다. userDao는 데이터베이스와 상호작용하여 사용자 데이터를 처리하는데 사용된다.

 

 

 

ViewModel 설정

이제 ViewModel을 생성할 차례이다. ViewModel에 Hilt를 사용해 의존성 주입을 요청하기 위해서는 @HiltViewModel 어노테이션을 추가해야 한다. 이를 통해 LoginViewModel 인스턴스를 생성할 때 필요한 UserRepositoryImpl 인스턴스가 주입되게 된다.

 

@HiltViewModel
class LoginViewModel @Inject constructor(
    private val userRepository: UserRepositoryImpl
): ViewModel() {

    private val _user = MutableLiveData<User>()
    val user: LiveData<User> = _user

    private val _successMessage = MutableLiveData<String>()
    val successMessage: LiveData<String> = _successMessage

    private val _errorMessage = MutableLiveData<String>()
    val errorMessage: LiveData<String> = _errorMessag

    private fun insertUser(user: User) =
        viewModelScope.launch {
            userRepository.insertUser(user)
        }

    private suspend fun onLoadUserInfo(email: String): User? =
        withContext(viewModelScope.coroutineContext) {
            userRepository.getUserByEmail(email)
        }

    fun onLogin(loginInfo: User) =
        viewModelScope.launch {
            val loadedUser = onLoadUserInfo(loginInfo.email)

            if (loadedUser == null) {
                if (check(loginInfo)) insertUser(loginInfo)
            } else {
                if (loadedUser.password == loginInfo.password) {
                    _successMessage.value = "Login success"
                } else {
                    _errorMessage.value = "Wrong Password"
                }
            }
        }
        
    private fun check(user: User): Boolean =
        when {
            TextUtils.isEmpty(user.email) -> {
                _errorMessage.value = "Please enter Email"
                false
            }
            !Patterns.EMAIL_ADDRESS.matcher(user.email).matches() -> {
                _errorMessage.value = "Please enter a valid Email"
                false
            }
            TextUtils.isEmpty(user.password) -> {
                _errorMessage.value = "Please enter Password"
                false
            }
            user.password.length <= 6 -> {
                _errorMessage.value = "Please enter Password greater than 6 characters"
                false
            }
            else -> true
        }
}

 

ViewModel에는 사용자 데이터 및 성공 및 오류 메시지와 관련된 LiveData 속성들이 정의되어 있다. 

 

 

 

Module 설정

위에서 우린 저장소 구현 클래스인 UserRepositoryImpl에 의존성 주입을 했고, ViewModel에 의존성 주입을 했다. 이제는 이 클래스들에 데이터베이스와 DAO를 제공해야 한다.

 

모듈을 사용하기 위해서는 @Module 어노테이션과 @InstallIn 어노테이션이 필요하다. 

@Module 어노테이션은 Hilt 모듈을 정의하고 @InstallIn 어노테이션은 모듈이 설치될 컴포넌트를 지정하는 데 사용된다.

 

이를 위한 코드는 아래와 같다.

 

ContentDatabaseModule

@Module
@InstallIn(SingletonComponent::class)
object ContentDatabaseModule {
    @Singleton
    @Provides
    fun provideDatabase(
        @ApplicationContext context: Context): AppDatabase =
        AppDatabase.getDatabase(context)
}

 

위의 코드에서 예를 들면 @InstallIn(SingletonComponent::class)은 모듈이 SigletonComponent에 설치되도록 지정한다. SigletonComponent는 앱의 전역 싱글톤 스코프로 사용되는 컴포넌트이다. 따라서 이 컴포넌트에 설치된 모듈들은 앱 전체에서 단일 인스턴스로 관리되며, 해당 모듈에서 제공되는 의존성 객체들은 Singleton 스코프로 동작한다.

 

 

ContentRepositoryModule

@Module
@InstallIn(SingletonComponent::class)
object ContentRepositoryModule {
    @Singleton
    @Provides
    fun provideContentDao(appDatabase: AppDatabase): UserDao {
        return appDatabase.userDao()
    }
}

 

@Singleton 어노테이션은 Singleton 스코프로 지정되어 한 번의 인스턴스만 생성되게 한다. @Provides 어노테이션을 사용하면 해당 메서드가 의존성을 제공하는 메서드임을 나타낸다.

 

이제 이 모듈들이 ViewModel과 저장소 사이에서 어떻게 동작하는지 과정을 알아보겠다.

 

UserRepositoryImpl의 생성자에서 userDao를 인자로 받아 UserRepositoryImpl 인스턴스를 생성한다. 이 때 ContentRepositoryModule의 provideContentDao() 메서드는 ContentDatabaseModule에서 제공하는 데이터베이스 인스턴스를 사용하여 UserDao 인스턴스를 생성한다. 그리고 이 UserDao 인스턴스는 UserRepositoryImpl에 주입되어 사용된다.

 

마찬가지로, LoginViewModel은 UserRepositoryImpl에 의존성을 가지고 있다. Hilt는 LoginViewModel을 생성할 때 UserRepositoryImpl의 인스턴스를 주입하고, UserRepositoryImpl은 ContentRepositoryModule에서 제공되는 UserDao를 사용하여 데이터베이스에 접근한다.

 

 

 

Activity 에서 ViewModel 사용

Activity를 주입 가능한 컴포넌트로 지정하기 위해서는 Hilt의 @AndroidEntryPoint 어노테이션을 사용해야한다.

 

그리고 Activiy에서 ViewModel을 사용하기 위해 Hilt의 viewModels() 함수를 호출해 ViewModel 인스턴스를 가져온다.

 

onCreate() 내에서는 ViewModel의 LivewData를 관찰하여 반응형 프로그래밍을 하게 된다.

 

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    private val loginViewModel: LoginViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.lifecycleOwner = this
        binding.viewModel = loginViewModel

        binding.mButtonLogin.setOnClickListener {
            onLogin()
        }

        loginViewModel.successMessage.observe(this) { successMessage ->
            onLoginSuccess(successMessage)
        }

        loginViewModel.errorMessage.observe(this) { errorMessage ->
            onLoginError(errorMessage)
        }
    }

    private fun onLogin() {
        val email = binding.edtEmail.text.toString()
        val password = binding.edtPassword.text.toString()

        val loginInfo = User(email, password)

        loginViewModel.onLogin(loginInfo)
    }

    private fun onLoginSuccess(message: String?) {
        Toast.makeText(this,message, Toast.LENGTH_SHORT).show()
    }

    private fun onLoginError(message: String?) {
        Toast.makeText(this,message, Toast.LENGTH_SHORT).show()
    }
}

 

 

이렇게 해서 MVVM에 Room을 사용해서 데이터베이스를 생성하고 그 데이터를 저장소를 사용해 접근하는데 의존성을 Hilt로 정의해 간단한 로그인 코드를 작성해 보았다. 

profile

Developing Myself Everyday

@배준형

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!