대부분의 앱은 사용자가 앱을 종료한 후에도 저장해야 하는 데이터가 있다. Room은 Android Jetpack에 속하는 지속성 라이브러리다. Room은 SQLite 데이터베이스 위에 있는 추상 레이어로, SQLite를 직접 사용하는 대신 Room은 데이터베이스 설정과, 상호작용 작업을 간소화한다.
권장되는 전체 아키텍쳐는 다음 그림과 같다.
학습할 내용
Data entities
앱 데이터베이스의 테이블을 나타낸다. 테이블의 행에 저장된 데이터를 업데이트하고 삽입할 새 행을 만드는 데 사용된다.
Data Access Objects (DAO)
데이터 검색 및 업데이트, 삽입, 삭제하는 데 사용하는 메서드를 제공한다.
Database class
데이터베이스를 가지고 있고, 앱 데이터베이스 연결을 위한 메인 액세스 포인트다. 앱에게 데이터베이스와 연결된 DAO 인스턴스를 제공한다.
이번 코드랩에서 완성하게 될 앱은 Inventory App이다.
스타터 코드는 여기에서 받을 수 있다.
Entity 클래스는 테이블을 정의하고, 이 클래스의 각 인스턴스는 데이터베이스 테이블의 행을 나타낸다.
Entity 클래스에는 데이터베이스의 정보를 어떻게 나타내고 상호작용 할 수 있는지를 Room에게 알려주는 매핑이 있다.
위의 그림에 해당하는 테이블 Entity 클래스를 만들어보자.
우선 data 패키지를 생성하고 그 아래 item이라는 이름의 파일을 생성했다.
data class인 Item의 속성은 다음과 같다.
@Entity(tableName = "item")
data class Item(
val id: Int = 0,
val itemName: String,
val itemPRice: Double,
val quantityInStock: Int
)
id에 primary key를 설정하고, 나머지 속성에 @ColumnInfo
를 설정한다. ColumnInfo는 특정 필드와 연결된 열을 맞춤설정하는 데 사용된다.
@Entity(tableName = "item")
data class Item(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
@ColumnInfo(name = "name")
val itemName: String,
@ColumnInfo(name = "price")
val itemPRice: Double,
@ColumnInfo(name = "quantity")
val quantityInStock: Int
)
Dao는 추상 인터페이스를 제공해 지속성 레이어를 나머지 애플리케이션과 분리하는 패턴이다. 이 분리(isolation)는 single responsibiliry principle을 따른다.
Dao는 데이터베이스 작업 실행과 관련된 복잡성을 숨기는 기능을 한다. Dao는 데이터베이스에 액세스하는 인터페이스를 정의하는 Room의 기본 구성요소!
data 패키지에 ItemDao.kt
를 생성한다.
클래스 정의는 interface
로하고, 상단에 @Dao
주석을 단다.
@Dao
interface ItemDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(item: Item)
}
OnConflictStrategy.IGNORE
은 기본 키가 이미 데이터베이스가 있다면 새 항목을 insert하지 않는다. insert()
를 실행하면 Room은 SQL 쿼리를 실행해 DB에 데이터를 삽입한다. @Update
suspend fun update(item: Item)
@Delete
suspend fun delete(item: Item)
Insert와 마찬가지로 Update와 Delete도 Room 라이브러리가 제공하는 주석을 사용할 수 있다.
라이브러리가 제공하는 기본 쿼리 이외에 추가하려면 @Query
주석을 사용한다.
@Query("SELECT * from item WHERE id = :id")
fun getItem(id: Int): Flow<Item>
@Query("SELECT * from item ORDER BY name ASC")
fun getItems(): Flow<List<Item>>
앞에서 만든 Entity와 Dao를 사용하는 RoomDatabase
를 만든다.
데이터베이스 클래스는 entity 목록과 data access objects(DAO) 목록을 정의한다. 메인 액세스 포인트이기도 하다.
@Database(entities = [Item::class], version = 1, exportSchema = false)
abstract class ItemRoomDatabase: RoomDatabase() {
abstract fun itemDao(): ItemDao
companion object{
@Volatile
private var INSTANCE: ItemRoomDatabase?=null
fun getDatabase(context: Context): ItemRoomDatabase{
return INSTANCE ?: synchronized(this){
val instance = Room.databaseBuilder(
context.applicationContext,
ItemRoomDatabase::class.java,
"item_database"
)
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
return instance
}
}
}
}
ItemRoomdatabase.kt
파일을 생성한다.@Database
주석을 작성한다.@Database
주석 뒤에는 Room이 데이터베이스 빌드하는데 필요한 인수들을 작성한다.ItemDao
를 반환하는 추상 함수를 선언한다. companion object{
@Volatile
private var INSTANCE: ItemRoomDatabase?=null
fun getDatabase(context: Context): ItemRoomDatabase{
return INSTANCE ?: synchronized(this){
val instance = Room.databaseBuilder(
context.applicationContext,
ItemRoomDatabase::class.java,
"item_database"
)
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
return instance
}
}
}
INSTANCE
를 선언하고, null로 초기화한다. INSTANCE
는 데이터베이스가 생성되면 데이터베이스 참조를 유지한다(keep a reference to the database)@Volatile
로 하면, INSTANCE
값이 항상 최신 상태로 유지된다. 즉 한 스레드에서 INSTANCE를 변경하면 다른 스레드에서도 즉시 표시된다. (변수의 값이 캐시되지 않고 모든 쓰기, 읽기가 메인 메모리에서 실행된다)getDatabase()
메서드를 정의한다. 데이터베이스 빌더에 필요한 Context
매개변수를 전달하고, ItemRoomDatabase
유형을 반환한다.getDatabase()
는 INSTANCE
변수를 반환하거나 INSTANCE가 null일 경우 synchrnized{}
블록 내에서 초기화한다. instance
변수에 데이터베이스 빌더를 사용해 데이터베이스를 가져온다. .fallbackToDestructiveMigration()
INSTANCE
에 새로 빌드한 instance
를 할당한다.InventoryApplication.kt
에서 데이터베이스 인스턴스를 인스턴스화한다.
class InventoryApplication : Application(){
val database: ItemRoomDatabase by lazy { ItemRoomDatabase.getDatabase(this) }
}
database
라는 ItemRoomDatabase
유형의 변수를 만들고, ItemRoomDatabse
에서 getDatabase()
를 호출해 database
인스턴스를 인스턴스화한다.lazy
이므로 참조가 처음 필요하거나 처음 참조에 액세스할 때 인스턴스가 생성된다. database
인스턴스를 사용한다. 앱의 임시 데이터를 저장하고 데이터베이스에도 액세스하려면 ViewModel이 있어야 한다. 인벤토리 ViewModel은 DAO를 통해 데이터베이스와 상호작용하여 UI에 데이터를 제공한다.
InventoryViewModel.kt
파일을 생성한다.
InventoryViewModel
클래스와 InventoryViewModelFactory
클래스를 추가한다.
class InventoryViewModelFactory(private val itemDao: ItemDao): ViewModelProvider.Factory{
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if(modelClass.isAssignableFrom(InventoryViewModel::class.java)){
@Suppress("UNCHECKED_CAST")
return InventoryViewModel(itemDao) as T
}
throw IllegalArgumentException("unknown ViewModel class")
}
}
InventoryViewModelFactory
클래스를 추가하여 InventoryViewModel
인스턴스를 인스턴스화한다.create()
메서드를 재정의한다. 클래스 유형을 인수로 사용하여 ViewModel 객체를 반환한다.modelClass
와 InventoryViewModel
클래스가 같은지 확인하고, 인스턴스를 반환한다. 같지 않다면 exception이 발생한다.class InventoryViewModel(private val itemDao: ItemDao) : ViewModel() {}
InventoryViewModelFactory
위에 있는 InventoryViewModel
클래스를 채워 데이터베이스에 인벤토리 데이터를 추가한다.
class InventoryViewModel(private val itemDao: ItemDao) : ViewModel() {
private fun insertItem(item: Item) {
viewModelScope.launch {
itemDao.insert(item)
}
}
private fun getNewItemEntry(itemName: String, itemPrice: String, itemCount: String): Item {
return Item(
itemName = itemName,
itemPrice = itemPrice.toDouble(),
quantityInStock = itemCount.toInt()
)
}
fun addNewItem(itemName: String, itemPrice: String, itemCount: String) {
val newItem = getNewItemEntry(itemName, itemPrice, itemCount)
insertItem(newItem)
}
}
ViewModelScope
는 ViewModel이 소멸될 때 하위 코루틴을 자동으로 취소하는 ViewModel 클래스의 확장 속성이다.Item
인스턴스를 반환한다.getNewITemEntry()
에서 반환된 값을 newItem
변수에 받아 insertItem()
을 호출해 데이터베이스에 추가한다. private val viewModel: InventoryViewModel by activityViewModels{
InventoryViewModelFactory(
(activity?.application as InventoryApplication).database
.itemDao()
)
}
InventoryViewModel
유형의 변수 viewModel
을 만든다. by activityViewModels
로 전체 프래그먼트에서 ViewModel을 공유한다. viewModel 정의 아래에 Item 유형의 item이라는 lateinit var을 만든다.
lateinit var item: Item
InventoryViewModel
에 TextField가 채워졌는지 확인하는 isEntryValid()
메서드를 추가한다.
fun isEntryValid(itemName: String, itemPrice: String, itemCount: String): Boolean {
if (itemName.isBlank() || itemPrice.isBlank() || itemCount.isBlank()) {
return false
}
return true
}
AddItemFragment.kt
에서는 위에 정의한 메서드를 사용하는 isEntryValid()
를 추가한다.
private fun isEntryValid(): Boolean {
return viewModel.isEntryValid(
binding.itemName.text.toString(),
binding.itemPrice.text.toString(),
binding.itemCount.text.toString()
)
}
private fun addNewItem() {
if (isEntryValid()) {
viewModel.addNewItem(
binding.itemName.text.toString(),
binding.itemPrice.text.toString(),
binding.itemCount.text.toString(),
)
val action = AddItemFragmentDirections.actionAddItemFragmentToItemListFragment()
findNavController().navigate(action)
}
}
addNewItem()
메서드에서 새 item을 추가할 때 텍스트필드가 모두 입력되었는지 확인한다.viewModel
인스턴스에서 addNewItem()
을 호출한다. ItemListFragment
로 다시 이동하는 action
을 만든다.앱을 실행하여 새로운 아이템을 추가하면 Database Inspector에 입력되는 것을 확인할 수 있다. 아직 adapter 등 구현하지 않은 부분이 있어서 실제 화면에 추가되지는 않는다.