[Android] Room Database
안드로이드 로컬 데이터 저장소로 Jetpack에서 제공하는 Room을 살펴보려고 한다.
Android 개발자 페이지에서는 다음과 같이 말하고 있다(https://developer.android.com/training/data-storage/room?hl=ko)
Room 지속성 라이브러리는 SQLite를 완벽히 활용하면서 원활한 데이터베이스 액세스가 가능하도록 SQLite의 추상화 계층을 제공합니다. 특히 Room을 사용하면 다음과 같은 이점이 있습니다.
- SQL 쿼리의 컴파일 시간 확인
- 반복적이고 오류가 발생하기 쉬운 상용구 코드를 최소화하는 편의 주석
- 간소화된 데이터베이스 이전 경로
Room에는 3가지 구성요소가 있다.
- 데이터베이스 클래스: 데이터베이스를 보유하고 앱의 영구 데이터와의 기본 연결을 위한 기본 액세스 포인트 역할을 합니다.
- 데이터 항목: 앱 데이터베이스의 테이블을 나타냅니다.
- 데이터 액세스 객체(DAO): 앱이 데이터베이스의 데이터를 쿼리, 업데이트, 삽입, 삭제하는 데 사용할 수 있는 메서드를 제공합니다.
그리고 그 관계도는 다음과 같다.
위에 정보를 종합하면 "SQLite를 직접 사용하는 것보다 코드를 줄일 수 있고 디버깅이 편하다."와 기본 구성을 알려 주고 있다.
직접 사용해 보자.
먼저 app 단위 gradle 파일에 종속성을 추가해주자.
plugins {
...
id("kotlin-kapt")
}
dependencies {
...
val room_version = "2.5.0"
implementation("androidx.room:room-runtime:$room_version")
annotationProcessor("androidx.room:room-compiler:$room_version")
kapt("androidx.room:room-compiler:$room_version")
implementation("androidx.room:room-ktx:$room_version")
implementation("androidx.room:room-rxjava2:$room_version")
implementation("androidx.room:room-rxjava3:$room_version")
implementation("androidx.room:room-guava:$room_version")
testImplementation("androidx.room:room-testing:$room_version")
implementation("androidx.room:room-paging:$room_version")
}
그리고 위에서 말한 3가지 구성요소 중에서 먼저 Entities를 만들어 보자.
@Entity
data class Employee(
@PrimaryKey(autoGenerate = true)
var id: Long = -1,
var name: String,
var phoneNumber: String,
var address: String,
var salary: Double,
)
Entity annotation을 사용하여 해당 클래스가 Room에서 Employee table을 생성 할 수 있도록 하고,
id를 테이블에서 primary key로 사용하기 위해서 PrimaryKey annotation을 설정해주었다.
public annotation class Entity(
val tableName: String = "",
val indices: Array<Index> = [],
val inheritSuperIndices: Boolean = false,
val primaryKeys: Array<String> = [],
val foreignKeys: Array<ForeignKey> = [],
val ignoredColumns: Array<String> = []
)
Entity annotation을 참고하면 위와 같은데, 테이블의 이름을 설정하거나 indexing 설정할 수 있다.
추가적으로 각 Column에 추가 설정하고 싶다면 ColumnInfo annotation을 통하여 해줄 수 있다.
그리고 DAO(Data Access Objects)를 만들어주자.
@Dao
interface EmployeeDao {
@Insert
fun insertEmployee(employee: Employee)
@Update
fun updateEmployee(employee: Employee)
@Delete
fun deleteEmployee(employee: Employee)
@Query("SELECT * FROM Employee")
fun getAllEmployee(): List<Employee>
}
Dao를 구성할때 기본적인 삽입, 수정, 삭제 등은 annotation을 제공하므로 편하게 사용할 수 있다.
만약에 Entity에 특정 Column이 Default value가 설정된 경우에 Insert annotation을 통하여 삽입하게 되면
default value가 적용되지 않고 column 값이 null이 된다.
따라서, query annotation을 사용하여 작성해 주는 것이 좋다.
예시는 다음과 같다.
@Entity
data class Employee(
@PrimaryKey(autoGenerate = true)
var id: Long = -1,
@ColumnInfo(name = "name")
var name: String,
var phoneNumber: String,
@ColumnInfo(defaultValue = "korea")
var address: String,
var salary: Double,
)
@Query("INSERT INTO Employee(name, phoneNumber, salary) VALUES(:name, :phoneNumber, :salary)")
fun insertEmployeeV2(
name: String,
phoneNumber: String,
salary: Double
)
마지막으로 데이터베이스 클래스를 만들어보자.
@Database(entities = [Employee::class], version = 1)
abstract class AppDatabase: RoomDatabase() {
abstract fun employeeDao(): Employee
companion object {
private var INSTANCE: AppDatabase? = null
@Synchronized
fun getInstance(context: Context): AppDatabase? {
if(INSTANCE == null) {
INSTANCE = Room.databaseBuilder(
context,
AppDatabase::class.java,
"app_db"
).build()
}
return INSTANCE
}
}
}
Database annotation을 통하여 entity를 연결하고 database 인스턴스를 singleton 디자인으로 구성했다.
이제 database class를 사용하여 instance를 가지고와 Dao를 통하여 DB를 사용할 수 있게 되었다.
이렇듯 구성하고 app을 빌드하면 RoomDatabase에서 Dao의 interface의 구현부를 자동 구성한 것을 확인 할 수 있다.
(빌드 후에 EmployeeDao_Impl 검색하면 확인 가능하다)
중간에 Dao를 수정하게 되면 오류가 생기는데 수정 후에 빌드를 하지 않아 Dao interface와 구현부가 달라 생기는 오류일 가능성이
높다. 따라서, 구현부에 오류가 발생하면 빌드를 해주자.