[Room] Room의 구조와 사용법
Room 지속성 라이브러리는 SQLite에 추상화 계층을 제공하여 SQLite를 완벽히 활용하면서 더 견고한 데이터베이스 액세스를 가능하게 합니다.
오늘은 Hilt와 ViewModel 등의 여러 라이브러리를 통해 Room을 사용하는 법을 알아보도록 하겠습니다.
Room의 구조
Room을 사용할 때 만들어 주어야 하는 것은 크게
1. DB에 들어가는 테이블의 구조인 Entity,
2. DB의 데이터에 접근할 수 있는 함수를 정의해 놓은 DAO,
3. DB 그 자체인 Database
3가지 입니다.
1. Entity
@Entity(tableName = Constants.TABLE_NAME)
class TeamBookmarkEntity(
var favoriteTeam: TeamData
) {
@PrimaryKey(autoGenerate = true)
var id: Int = 0
}
먼저 Entity는 @Entity 어노테이션으로 표시해 줍니다. 기본은 클래스 이름대로 테이블이 생성되지만, @Entity(tableName = 테이블 이름) 형식으로 테이블 이름을 따로 지정해 줄 수도 있습니다.
그다음 @PrimaryKey 어노테이션으로 기본키를 설정해 줍니다. 기본키는 직접 설정할 수도 있고, @PrimaryKey(autoGenerate = true)와 같이 자동으로 설정되도록 쓸 수도 있습니다.
2. DAO
@Dao
interface TeamBookmarkDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTeamBookmark(teamBookmark: TeamBookmarkEntity)
@Query("SELECT * FROM team_bookmark_table ORDER BY id ASC;")
fun readTeamBookmark(): Flow<List<TeamBookmarkEntity>>
@Delete
suspend fun deleteTeamBookmark(teamBookmarkEntity: TeamBookmarkEntity)
}
DAO는 @Dao 어노테이션으로 표시해주고, 내부에 사용할 기능들을 만들어 줍니다.
단순 삽입, 삭제, 수정은 @Insert, @Delete, @Update 어노테이션을 사용하면 Room이 알아서 처리해주고,
세부적인 쿼리를 작성하기 위해서는 @Query 어노테이션을 달고 내부에 SQL문을 작성해 주면 됩니다.
3. Database
@Database(
entities = [TeamBookmarkEntity::class],
version = 1,
exportSchema = false
)
@TypeConverters(TeamBookmarkTypeConverter::class)
abstract class TeamBookmarkDatabase: RoomDatabase() {
abstract fun teamBookmarkDao(): TeamBookmarkDao
}
Database 역시 @Database 어노테이션으로 표시해 준 후, DB에서 사용할 엔티티들을 넣어줍니다. version은 테이블의 구조가 달라질 경우 올려주어야 한다고 하는데, 저는 자세히 모르겠습니다.
위에 나온 @TypeConverters 어노테이션의 경우, LocalDateTime 같은 Room에 그대로 집어넣을 수 없는 자료형의 경우에 String 같은 자료형으로 변환해주는 TypeConverter를 만들어 지정해준 것 입니다. TypeConverter는 @TypeConverter 어노테이션을 붙여 구현하면 됩니다.
@TypeConverter
fun localDateTimeToString(localDateTime: LocalDateTime): String =
localDateTime.format(DateTimeFormatter.ofPattern(localDateTimePattern))
@TypeConverter
fun stringToLocalDateTime(string: String): LocalDateTime =
LocalDateTime.parse(string, DateTimeFormatter.ofPattern(localDateTimePattern))
이제 Room의 구성요소는 만들었고, DI를 통해 DAO를 주입 받기 위해서 Module을 만들어줍니다.
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Provides
@Singleton
fun provideWakeUpTimeDataBase(@ApplicationContext context: Context): WakeUpTimeDatabase =
Room.databaseBuilder(context, WakeUpTimeDatabase::class.java, DATABASE_NAME).build()
@Provides
@Singleton
fun provideWakeUpTimeDao(database: WakeUpTimeDatabase) = database.wakeUpTimeDao()
}
위와 같이 만들어 주었습니다.
class Repository @Inject constructor(
private val wakeUpTimeDao: WakeUpTimeDao
) {
fun getWakeUpTimes(): Flow<List<WakeUpTimeEntity>> =
wakeUpTimeDao.getWakeUpTimes()
suspend fun insertWakeUpTime(wakeUpTimeEntity: WakeUpTimeEntity) =
wakeUpTimeDao.insertWakeUpTime(wakeUpTimeEntity)
suspend fun deleteWakeUpTime(wakeUpTimeEntity: WakeUpTimeEntity) =
wakeUpTimeDao.deleteWakeUpTime(wakeUpTimeEntity)
}
그 이후 Repository에 DAO를 주입해주었습니다. 데이터를 가져오는 함수는 Flow 형태로 가져오기 때문에 suspend 함수로 만들지 않았고 나머지는 suspend 함수로 만들었습니다.
@HiltViewModel
class MainViewModel @Inject constructor(
private val repository: Repository
) : ViewModel() {
fun getWakeUpTimes(): Flow<List<WakeUpTimeEntity>> = repository.getWakeUpTimes()
fun insertWakeUpTime(wakeUpTimeEntity: WakeUpTimeEntity) = viewModelScope.launch {
repository.insertWakeUpTime(wakeUpTimeEntity)
}
fun deleteWakeUpTime(wakeUpTimeEntity: WakeUpTimeEntity) = viewModelScope.launch {
repository.deleteWakeUpTime(wakeUpTimeEntity)
}
}
이후에 뷰모델에서 Repository를 가져와주고,
private fun checkIsTodaysWakeUpTimeSaved() = mainViewModel.getWakeUpTimes().asLiveData().observe(viewLifecycleOwner) { result ->
mainViewModel.isTodaysWakeUpTimeSaved = result.map { it.wakeUpTime.dayOfMonth }.contains(LocalDate.now().dayOfMonth)
applyLottieStyle()
}
그 이후 위와 같이 Flow를 LiveData로 변환해 값을 가져왔습니다.