Room Database는 Android Jetpack Library 중 하나로 안드로이드 앱에서 로컬 데이터를 관리하기 위한 영구 저장소 라이브러리입니다.
Room은 SQLite 데이터베이스를 바탕으로 만들어졌기 때문에 SQLite의 기능의 대부분을 더 쉽게 사용할 수 있게 해줍니다.
이 글에서는 Room의 사용법에 대해 다룰 예정이며, Room에 대한 자세한 설명이 필요하신 분은 구글 문서를 참고하시면 됩니다.
Room을 사용하는데 중요한 구성요소는 Entity, DAO, Database 3가지가 있습니다.
Entity는 '개체', '독립체'라는 뜻으로 Room에서는 데이터베이스에 저장될 데이터의 형태를 가지는 개체로, 데이터베이스의 테이블을 나타내는 데이터 클래스를 의미합니다.
Entity는 데이터베이스에 저장될 데이터 필드를 정의하며, 일반적으로는 클래스의 멤버 변수로 선언됩니다.
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "people")
data class Person(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val name: String,
val age: Int,
val address: String
)
위의 코드와 같은 형태로 Entity 클래스를 선언할 수 있습니다.
코드의 각 부분을 설명하면,
@Entity(tableName = "people")
@Entity
어노테이션을 통해 이 data class가 Room에서 사용할 Entity라는 것을 설정(표시)합니다.
tableName
의 값으로 테이블의 이름을 설정합니다.
tableName
은 선택적으로 사용할 수 있으며, 이 부분이 없을시 테이블 이름의 기본값은 data class의 이름으로 설정됩니다.
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@PrimaryKey
어노테이션은 테이블의 테이블의 각 행을 고유하게 식별할 수 있는 고유키를 설정합니다.
autoGenerate
을 true
로 설정하면 각 항목에 자동으로 고유키가 부여됩니다.
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val name: String,
val age: Int,
val address: String
@ColumnInfo(name = "필드 이름")
어노테이션을 사용하여 설정할 수 있습니다.DAO는 Data Access Object의 약자로 데이터에 접근할 수 있는 메소드(SELECT, INSERT, DELETE, UPDATE)를 정의 해놓은 인터페이스를 의미합니다.
import androidx.room.*
import kotlinx.coroutines.flow.Flow
@Dao
interface PersonDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(person: Person)
@Update
suspend fun update(person: Person)
@Delete
suspend fun delete(person: Person)
@Query("SELECT * from people WHERE id = :id")
fun getPerson(id: Int): Flow<Person>
@Query("SELECT * from people ORDER BY name ASC")
fun getAllPeople(): Flow<List<Person>>
}
위의 코드와 같은 형태로 DAO 인터페이스를 선언할 수 있습니다.
코드의 각 부분을 설명하면,
@Dao
@Dao
어노테이션을 통해 이 인터페이스가 Room에서 사용할 Dao 인터페이스라는 것을 설정합니다.@Insert(onConflict = OnConflictStrategy.IGNORE)
onConflict
는 데이터베이스에 같은 고유키를 가진 항목을 추가할 때 어떻게 처리할 지를 설정합니다.
OnConflictStrategy.IGNORE
는 해당 항목의 추가 요청을 무시하고 다음 작업을 합니다.
@Query("SELECT * from people WHERE id = :id")
fun getPerson(id: Int): Flow<Person>
@Query("SELECT * from people ORDER BY name ASC")
fun getAllPeople(): Flow<List<Person>>
Room 데이터베이스에서 SQLite의 SELECT
메소드는 @Query
어노테이션 뒤에 쿼리를 직접 작성하여 입력해야 합니다.
쿼리에서 인터페이스의 인자를 사용하려면 :id
처럼 콜론+변수명
을 통해 사용할 수 있습니다.
예시 코드에서 볼 수 있듯이, insert(), update(), delete() 메소드는 suspend fun으로 선언되었으나, getPerson(), getAllPeople() 메소드는 fun으로 선언되어 있습니다.
일반적으로 데이터베이스와 관련된 작업은 백그라운드 스레드에서 비동기적으로 실행해야 합니다. 이러한 이유로 getPerson(), getAllPeople() 메소드도 비동기적으로 실행되어야 하지만 fun으로 선언한 이유는 이 메소드들은 Flow<T>
유형을 반환하도록 선언되었기 때문입니다.
Room에서 Flow 반환하는 데이터베이스 쿼리를 수행하는 경우, Room은 내부적으로 백그라운드 스레드에서 비동기적으로 쿼리를 실행합니다. 따라서 getPerson(), getAllPeople() 메소드를 비동기적으로 호출하기 위해 suspend fun으로 설정할 필요가 없습니다.
Database는 데이터베이스와 연결되는 클래스로, 데이터베이스를 생성하고 Entity와 DAO를 연결해줍니다.
Room에서는 데이터베이스를 싱글톤으로 관리하여 앱 전체에서 공유하도록 합니다.
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database(entities = [Person::class], version = 1, exportSchema = false)
abstract class PeopleDatabase : RoomDatabase() {
abstract fun personDao(): PersonDao
companion object {
@Volatile
private var Instance: PeopleDatabase? = null
fun getDatabase(context: Context): PeopleDatabase {
return Instance ?: synchronized(this) {
Room
.databaseBuilder(context, PeopleDatabase::class.java, "people_database")
.fallbackToDestructiveMigration()
.build()
.also { Instance = it }
}
}
}
}
위의 코드와 같은 형태로 Database를 만들 수 있습니다.
코드의 각 부분을 설명하면,
@Database(entities = [Person::class], version = 1, exportSchema = false)
@Database
어노테이션을 통해 이 추상 클래스가 Room의 Database라는 것을 설정합니다.
@Database
어노테이션에는 entities
, version
인자가 필요합니다.
entities
의 값으로는 데이터베이스에 포함될 Entity 클래스가 배열 형태로 전달 됩니다.
version
은 데이터베이스의 버전을 나타내고, 데이터베이스가 업데이트되면 이 값을 증가시켜야 합니다.
exportSchema
는 필수적인 인자는 아니고, 이 값이 true
면 Room은 데이터베이스 스키마를 export 폴더에 저장하여 앱의 디렉토리 내에서 확인할 수 있습니다.
@Volatile
private var Instance: PeopleDatabase? = null
@Volatile
어노테이션을 사용하면 변수가 cpu의 캐시 메모리가 아니라, 메인 메모리에 직접 저장됩니다.
메인 메모리에 값이 저장되기 때문에, 다른 스레드에서 변수를 업데이트 하더라도 항상 최신 값을 가져올 수 있습니다.
return Instance ?: synchronized(this) {
...(생략)...
}
앨비스 연산자를 통해 Instance가 아직 초기화 되지 않았으면 synchronized 블럭이 실행되도록 하는 코드입니다.
synchronized 블럭
은 한 번에 하나의 스레드만 해당 블럭에 접근할 수 있도록 합니다.
데이터베이스에서 synchronized 블럭을 사용함으로써 동시에 여러 스레드에서 데이터베이스 객체에 접근하는 것을 방지합니다.
Room
.databaseBuilder(context, PeopleDatabase::class.java, "people_database")
.fallbackToDestructiveMigration()
.build()
.also { Instance = it }
databaseBuilder()
메소드를 통해 데이터베이스 생성을 시작합니다.
databaseBuilder()
메소드는 context
, RoomDatabase 클래스를 상속받은 데이터 베이스 클래스
, 데이터베이스 파일의 이름
을 인자로 받습니다.
fallbackToDestructiveMigration()
메소드는 데이터베이스가 업그레이드될 때, 마이그레이션에 실패하면 데이터베이스를 삭제하고 새로운 스키마를 적용하는 것을 허용합니다.
build()
메소드를 통해 데이터베이스를 생성합니다.
also { Instance = it }
메소드를 통해 데이터베이스 객체를 Instance 변수에 할당합니다.
also()
메소드를 사용하지 않고 아래와 같이 Instance 변수에 직접 할당할 수도 있습니다.
Instance = Room
.databaseBuilder(context, PeopleDatabase::class.java, "people_database")
.fallbackToDestructiveMigration()
.build()
Room을 사용하는데 필요한 구성요소인 Entity
, DAO
, Database
를 모두 만들었습니다.
이제 Database.getDatabase()
를 통해 데이터베이스를 인스턴스화 하고
Repository
에서 DAO의 인스턴스
를 전달해 Room 데이터베이스를 사용할 수 있습니다.