(Android/Hilt) Hilt + Room 연동


1. 연동 구조

Room의 Database는 Room.databaseBuilder()로 생성하므로 @Inject constructor를 쓸 수 없습니다. 따라서 Module의 @Provides 로 제공하고, DAO는 Database에서 꺼내 제공합니다.

Context → AppDatabase → UserDao → UserRepository → ViewModel
 (Hilt)    (@Provides)  (@Provides)  (@Binds)       (@HiltViewModel)

2. Room 구성 요소

@Entity(tableName = "users")
data class UserEntity(
    @PrimaryKey val id: String,
    val name: String,
    val email: String
)

@Dao
interface UserDao {
    @Query("SELECT * FROM users")
    fun observeAll(): Flow<List<UserEntity>>

    @Query("SELECT * FROM users WHERE id = :id")
    suspend fun findById(id: String): UserEntity?

    @Upsert
    suspend fun upsert(user: UserEntity)
}

@Database(entities = [UserEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}

3. DatabaseModule

@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {

    @Provides
    @Singleton  // DB 인스턴스는 앱 전체에서 하나만 — 필수!
    fun provideDatabase(
        @ApplicationContext context: Context  // Hilt가 기본 제공하는 Application Context
    ): AppDatabase =
        Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
            .fallbackToDestructiveMigration()
            .build()

    // DAO는 Database에서 꺼내서 제공 — 별도 @Singleton 불필요 (Database가 싱글턴이므로)
    @Provides
    fun provideUserDao(database: AppDatabase): UserDao = database.userDao()
}

Database는 반드시 @Singleton: Room DB 인스턴스 생성은 비용이 크고, 여러 인스턴스가 같은 파일에 접근하면 데이터 불일치가 생길 수 있습니다.


4. Repository 연결

interface UserRepository {
    fun observeUsers(): Flow<List<User>>
    suspend fun saveUser(user: User)
}

class UserRepositoryImpl @Inject constructor(
    private val userDao: UserDao,  // Hilt가 DatabaseModule에서 찾아 주입
    @IoDispatcher private val ioDispatcher: CoroutineDispatcher
) : UserRepository {

    override fun observeUsers(): Flow<List<User>> =
        userDao.observeAll().map { list -> list.map { it.toDomain() } }

    override suspend fun saveUser(user: User) = withContext(ioDispatcher) {
        userDao.upsert(user.toEntity())
    }
}

@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
    @Binds
    abstract fun bindUserRepository(impl: UserRepositoryImpl): UserRepository
}

ViewModel은 UserRepository 인터페이스만 알면 됩니다.

@HiltViewModel
class UserListViewModel @Inject constructor(
    repository: UserRepository
) : ViewModel() {
    val users = repository.observeUsers()
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
}

5. 테스트: 인메모리 DB로 교체

운영용 DatabaseModule을 테스트에서 인메모리 DB로 바꿔 끼울 수 있습니다 (자세한 내용은 Hilt Testing 글 참고).

@Module
@TestInstallIn(
    components = [SingletonComponent::class],
    replaces = [DatabaseModule::class]  // 운영 모듈을 대체
)
object TestDatabaseModule {
    @Provides
    @Singleton
    fun provideInMemoryDatabase(@ApplicationContext context: Context): AppDatabase =
        Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
            .allowMainThreadQueries()
            .build()
}

6. 정리



Related Posts