원래 SQLite를 활용하여 어플리케이션 내부의 저장소(DB)를 만들어 저장을 했었다.
그러나 SQLite를 사용할 때에는
등을 추가적으로 투자해야 하므로, Android X 버전 이후로 어플리케이션 내부 DB로 Room을 추천하고 있다.
아래의 코드는 Kotlin으로 작성되었음을 미리 밝힙니다.
SQLite의 추상화된 버전이라 생각하시면 조금 더 편할 것 같습니다.
이전에는 SQLite에 직접 데이터를 가공하여 DB에 맞는 형식으로 데이터를 CRUD하는 방식이었다면, Room에서는 객체로 데이터를 주고받을 수 있고 반응형 데이터로도 데이터를 주고받을 수 있게 설계가 되어있습니다!
그렇다면 Room은 어떻게 구성을 할 수 있을까요?
이번 예제에서는 다음과 같은 DB를 짜고 데이터를 입출력하는 기능을 만들어보려 합니다.
@Entity(tableName = "daily_sleep_quality_table")
data class SleepNight(
@PrimaryKey(autoGenerate = true)
var nightId: Long = 0L,
@ColumnInfo(name = "start_time_milli")
val startTimeMilli: Long = System.currentTimeMillis(),
@ColumnInfo(name = "end_time_milli")
var endTimeMilli: Long = startTimeMilli,
@ColumnInfo(name = "sleep_quality")
var sleepQuality: Int = -1
)
데이터베이스의 Table(데이터 구조) 역할을 맡고 있는 Entity입니다. 이 테이블들이 모여서 나중에 살펴볼 Database를 구성할 것입니다.
Room에서의 Entity는 우리가 알고 있는 테이블 형식과 달리 Data Class의 형태로 구성되어있습니다. 이런 구조는 Room에서 데이터 입출력을 할 때 객체로 입출력을 한다는 것을 드러내는 가장 강력한 증거죠!
Entity는 어노테이션(@ 형태)로 클래스를 정의하는데 Entity를 구성하는 어노테이션을 살펴보도록 하죠
이 클래스가 Entity임을 알려주는 어노테이션입니다. 다음과 같은 속성들을 사용할 수 있습니다.
관계형 데이터베이스(RDB)를 다뤄보신 분들은 아실 그 용어죠? DB의 기본 키를 나타내는 어노테이션입니다. 클래스의 멤버변수에 사용합니다.
관계형 DB의 Column의 이름을 변수명을 짓고 싶지 않을 때 사용하는 어노테이션입니다. 코틀린에서 변수 네이밍 컨벤션은 camelCase인데 DB의 네이밍 컨벤션은 대부분 snake_case죠?
그래서 이 부분을 변환하기 위해 이 어노테이션을 사용하는 것 같습니다.
Room의 데이터를 접근하기 위해서는 Database를 접근할 수 있는 객체를 정의해서 그 객체를 통해 접근해야 합니다. 이 객체를 DAO라 합니다.
왜 데이터베이스에 직접 접근하지 않냐고 물어볼 수 있으나 이를 자세히 다룬다면 분명히 이 게시글의 길이는 끝없이 길어질 것이기에 유용한 설명을 해주고 있는 블로그 링크를 첨부하도록 하겠습니다.
그렇다면 DAO는 어떤 형태로 만들어질까요?
@Dao
interface SleepDatabaseDao {
@Insert
fun insert(sleepNight: SleepNight)
@Update
fun update(sleepNight: SleepNight)
@Query("SELECT * FROM daily_sleep_quality_table WHERE nightId = :key")
fun get(key: Long): SleepNight
@Delete
fun deleteAllNights(nights: List<SleepNight>): Int
@Query("DELETE FROM daily_sleep_quality_table")
fun clear()
@Query("SELECT * FROM daily_sleep_quality_table ORDER BY nightId DESC")
fun getAllNights(): LiveData<List<SleepNight>>
@Query("SELECT * FROM daily_sleep_quality_table ORDER BY nightId DESC LIMIT 1")
fun getToNight(): SleepNight?
}
DAO는 interface(혹은 추상 클래스)로 정의할 수 있습니다. 그리고 쿼리를 써야하는 기존 DB의 CRUD 방식과는 달리 조금 더 친절하게 데이터를 다룰 수 있도록 어노테이션을 기반으로 하여 데이터 입출력을 정의합니다.
DB에 row 데이터를 삽입합니다. 위에서는 return 값을 정의 안 했지만, 만약 정의한다면 long 타입 삽입된 row의 id 값(여러 개를 삽입했으면 id들 long의 리스트 형)이 반환됩니다.
DB의 row 데이터를 수정합니다. Data Class 객체를 전달해준다면 id 값을 제외한 최소 한 가지 이상의 데이터가 수정된 채로 전달될 것이니 이를 기반으로 row 데이터를 수정합니다.
DB의 row 데이터를 삭제합니다.
기존 방식과 같이 query를 활용하여 데이터를 CRUD할 수 있게 만드는 어노테이션입니다. 위의 연산들보다 조금 더 복잡한 연산이 필요한 경우에 이런 쿼리 어노테이션을 활용하여 데이터를 가져올 수 있습니다.
@Database(entities = [SleepNight::class], version = 1, exportSchema = false)
abstract class SleepDatabase : RoomDatabase() {
abstract val sleepDatabaseDao: SleepDatabaseDao
companion object {
@Volatile
private var INSTANCE: SleepDatabase? = null
fun getInstance(context: Context): SleepDatabase {
synchronized(this) {
var instance = INSTANCE
if(instance == null) {
instance = Room.databaseBuilder(
context.applicationContext,
SleepDatabase::class.java,
"sleep history database"
)
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
}
return instance
}
}
}
}
RecyclerView의 어댑터 같은 느낌으로 Entity만큼 정의된 Dao 객체들을 반환할 수 있는 함수들을 가지고 있는 추상 클래스 형태로 정의합니다.
Room 데이터베이스에서 Dao를 가져와 이 객체를 통해 데이터를 CRUD한다
즉 Room Database는 수 많은 Dao들을 관리하기 위한 Dao Manager 클래스라고 생각하면 될 것 같습니다.
이 클래스가 Database임을 알려주는 어노테이션입니다.