-1- Room Database
Room DataBase Library를 사용하는 이유
- 특정 Database는 서버에서 관리하지 않고 로컬에서 관리하는 것이 필요한데, 이때 Android Studio에 내장되어 있는 SQLite를 사용하여 DB를 관리하는 경우가 일반적이다. 하지만 SQLite를 독자적으로 사용할 시 성능적인 문제를 포함함과 동시에 DB의 유지보수 작업이 번거로워지는 이슈가 있다.
- Room DB Library를 사용함으로 이러한 문제를 해결할 수 있으며, 간결하고 직관적인 코드 작성이 가능하며 컴파일 시 SQL 쿼리 및 스키마 관련 오류를 확인할 수 있어 안정성 향상에도 도움을 준다.
- 새로운 버전의 앱을 배포하거나 스키마를 변경할 때 Migration을 지원해 이전 버전과의 호환성 유지 및 데이터 이관 작업을 처리할 수 있다.
SQLite를 독자적으로 사용했을 때 주의사항
Room Database란?
- Room은 Android Studio에 내장된 SQLite를 통한 데이터 베이스 사용에 도움을 주는 Wrapper Library이며. ORM ( Object Relational Mapping ) 도구이다.
- Room은 SQLite에 추상화 레이어 (Entity, DAO,Room Database)를 제공해 SQLite를 활용하여 원활한 데이터 베이스 액세스가 가능하도록 한다.
ORM
- 데이터 베이스와 객체 지향 프로그래밍 언어간의 호환되지 않는 데이터를 변환하는 프로그래밍 기법으로 DateBase 테이블(표)과 매핑되는 객체를 만들고 그 객체에서 DataBase를 관리하는 것이다. 이를 통해 SQL 쿼리 작성과 같은 저수준의 데이터베이스 조작을 최소화하고, 객체 모델과 데이터베이스 스키마 사이의 매핑 작업을 다음의 작업을 통해 단순화할 수 있다.
- Entity 클래스 - DB 테이블 매핑
- 클래스와 테이블, 속성과 컬럼, 외래 키 등을 연결한 Entity 클래스 객체를 사용하여 데이터를 조작할 수 있도록 한다.
- CRUD 작업
- 복잡한 SQL 쿼리문 대신 Room ORM 의 CRUD 등의 기본적인 데이터 조작 작업에 대한 메서드 호출로 간단하게 데이터 CRUD 작업을 수행할 수 있다.
- Entity 클래스 - DB 테이블 매핑
- 이 외에도 ORM은 캐싱 및 성능 최적화를 통해 DB 접근 횟수를 줄여 앱의 응답 속도를 개선할 수 있으며 추상화 레이어를 통해 DB에 종속되지 않는 코드를 작성할 수 있도록 한다
[ 0 ] Room Library build.gradle
plugins {
...
id "kotlin-kapt"
}
dependencies {
val room_version = "2.5.2"
implementation("androidx.room:room-runtime:$room_version")
annotationProcessor("androidx.room:room-compiler:$room_version")
// To use Kotlin annotation processing tool (kapt)
kapt("androidx.room:room-compiler:$room_version")
// To use Kotlin Symbol Processing (KSP)
ksp("androidx.room:room-compiler:$room_version")
// optional - Kotlin Extensions and Coroutines support for Room
implementation("androidx.room:room-ktx:$room_version")
// optional - Test helpers
testImplementation("androidx.room:room-testing:$room_version")
}
dependencies에 kapt 또는 ksp 중 사용하는 APT ( Annotation Processing Tool ) 에 해당하는 dependencies만 추가해주면 된다. Room, Dagger와 같은 라이브러리에서는 kapt가 호환성이 더 좋으므로 kapt를 사용할 것이다.- 2023. 10.9 추가 Room 사용 시 KSP를 사용해도 큰 이슈가 없다고 한다. KSP가 KAPT 보다 좋은 퍼포먼스를 보여주니 KSP를 사용해주도록 하자.
- KAPT, KSP 에 관한 내용은 별도로 다른 글에서 정리할 예정.
[ 1 ] Room Database Library 의 구성요소
[ 1 - 1 ] Entity, Schema
- Entity는 데이터베이스 테이블과 매핑을 통해 데이터베이스의 테이블을 나타내는 개체이다 아래의 표 ( Table ) 는 Liquor라는 카테고리에 해당하는 Jager, Kahlua, Malibu라는 이름으로 Liquor / 이름 = jager 와 같이 대분류 속에 소분류에 해당하는 각각의 값이 있다. 이때 Liquor라는 테이블의 data class 이름( Liquor ), 컬럼( name, alc )과 같이 테이블의 구조와 속성 등 실제로 데이터가 저장되고 관리되는 방식을 정의하는 것을 Schema라고 한다.
- Entity 클래스는 Data class 앞에 @Entity 어노테이션을 통해 정의할 수 있으며 이름, 도수와 같은 테이블의 각 컬럼은 Entity 클래스의 프로퍼티로 포함된다.
- Entity 클래스를 통해 데이터의 저장 및 검색을 위한 객체 지향적인 방식으로 다룰 수 있다.
PrimaryKey
- PrimaryKey는 위와 같은 데이터베이스에서 각 레코드를 고유하게 식별하는 역할을 한다. Primary Key는 (autoGenerate = true) 를 통해 새로운 레코드가 데이터베이스가 삽입될 때, 자동으로 증가한다.
- PrimaryKey는 여러 컬럼에 복합적으로 사용할 수 있으며 복합 PrimaryKey는 각 컬럼앞에 @PrimaryKey 어노테이션을 추가해주면 된다.
- Primary Key는 고유한 식별자로 사용하기 때문에 컬럼안에 동일한 값을 가진 중복된 레코드를 허용하지 않아 중복을 방지할 수 있다. 때문에 이름과 같이 중복된 레코드를 허용하는 컬럼은 PrimaryKey로 설정하면 안된다.
Entity Class 생성
@Entitiy(tableName = "liquor_table")
data class Liquor(
@primaryKey val id : Long,
// autoGenerate 사용
// @primaryKey(autoGenerate = true) val id : Long,
// 복합 PrimaryKey 사용
@primaryKey val name : String,
val alc: Int
)
[ 1 - 2 ] DAO - ( Data Access Object )
- DAO란 Entity를 통해 수행할 CRUD와 같은 개발자가 DB에 접근하고 조작하기 위해 사용하는 데이터베이스 메서드를 정의하는 인터페이스이다.
- 데이터 베이스 메서드는 Annotation을 통해 정의된다
- Create
- DB에 새로운 레코드를 추가하는 메서드로 DAO 인터페이스 내부에 메서드를 추가하고 메서드의 매개변수로 삽입할 Entity 객체를 전달한 뒤 @Insert Annotation을 통해 정의한다.
- Read
- DB의 레코드를 조회하는 메서드로 DAO 내부에 메서드를 추가하고 @Query 내부에 찾으려는 Entity와 PrimaryKey 등의 조회 조건을 지정한 SQL Query 문으로 작성한 Annotation을 사용하여 조회 작업을 정의한다.
- Update
- DB의 레코드를 수정하는 메서드로 DAO 내부에 @Update 를 사용하여 수정할 Entity 객체를 매개변수로 전달하는 메서드를 정의한 뒤 구현부에서 업데이트 할 수 있다.
- Delete
- DB의 레코드를 삭제하는 메서드로 DAO 내부에 @Delete 를 통해 삭제 메서드를 정의할 수 있다. 해당 메서드에도 삭제할 Entity 객체를 메서드의 매개변수로 전달해야한다.
- Create
DAO Interface생성예제
@Dao
interface TestDAO {
// onConflict 를 사용해 동일한 primary key가 이미 존재하는 경우 새로운 레코드가 기존 레코드를 대체하도록 한다
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertLiquor(liquor:Liquor)
// liquor_table의 전체 목록을 가져온다
@Query("SELECT * FROM liquor_table")
fun getAllLiquor(): LiveData<List<Liquor>>
// SELECT 쿼리문을 통해 liquor_table에서 name이 일치하는 레코드들을 가져온다.
@Query("SELECT * FROM liquor_table WHERE name = :sname")
suspend fun getLiquorByName(sname: String): List<Liquor>
// DB 삭제 작업
@Delete
suspend fun deleteLiquor(liquor: Liquor);
}
[ 1 - 3 ] RoomDatabase
- Room Library에서 제공하는 @Database Annotation을 통해 Room Database 클래스를 정의할 수 있다. 이때 room DB 클래스는 abstract class로 생성되어야 한다.
- Room DB 클래스는 Database 접근 시점을 정의하여 제공하며 DAO를 가져올 수 있는 getter 메소드를 만들어 관리한다.
- Entity 클래스들의 목록을 지정하여 데이터베이스 내에 테이블로 매핑되도록 한다.
- version 매개변수를 통해 데이터베이스의 version 번호를 지정한다. version 번호는 스키마가 변경될 시, 증가 시켜줘야 한다.
- ExportSchema 매개변수를 통해 Schema 정보를 파일로 내보낼지 여부를 결정한다. default값은 false로 되어 있으며 true로 설정할 시 Schema 정보를 XML 파일로 내보낸다.
- Room 클래스의 인스턴스가 중복으로 생성되지 않도록 companion object를 사용하여 Room Database 인스턴스를 전역적으로 접근할 수 있도록 한다.
Room DB 클래스 예제
@Database(entities = [Liquor::class, StoreInfo::class, CompanyInfo::class], version = 1)
abstract class MyDB : RoomDatabase() {
abstract fun getTestDao(): TestDAO
companion object {
private var INSTANCE: MyDB? = null
private val MIGRATION_1_2 = object : Migration(1,2){
override fun migrate(database: SupportSQLiteDatabase) {생략}
}
private val MIGRATION_2_3 = object : Migration(2,3){
override funn migrate(database: SupportSQLiteDatabase) {생략}
}
fun getDatabase(context: Context) : MyDB {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(
context, MyDB::class.java, "test_database")
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.build()
)
return INSTANCE as MyDB
}
}
}
}
- @Database Annotation을 사용해 DB 클래스에 포함될 Entity 클래스들의 목록 및 Version 번호를 지정한 DB 클래스를 정의한다.
- 추상 메서드인 getTestDao()를 선언함으로 해당 메서드를 통해 DAO Interface인 TestDAO에 대한 접근을 가능하게 한다.
- 이후 companion object 내부에서 Room DB의 인스턴스를 저장하기 위해 INSTANCE 변수를 선언하여 Room DB 클래스에 대한 싱글톤 패턴을 구현하며 각각 버전 1 에서 2, 2에서 3의 버전 간의 Migration을 처리하는 객체를 정의한다. Migration에 대해서는 간단하게 설명. 추후에 추가로 학습하기로 하고 넘아간다.
- getDatabase() 메서드를 통해 Room DB Instance를 가져와 builder를 통해 DB를 빌드하고, Room DB Instance의 이름과 버전정보를 설정하고 addMigrations() 메서드를 통해 Migration 객체들을 추가한 뒤 build()를 통해 인스턴스를 생성한다.
[오늘 복습한 내용]
1. SQLite 쿼리문에 대해서 간단하게 더 알아보았는데 다음에 쿼리문을 정리한 글을 작성해보면 좋을 것 같다.
[오류,에러 등등]
1. 특별한 오류는 없었다.
[느낀 점]
1. DB부분에 어노테이션,쿼리문이 너무 많이 등장해서 학습하는 게 낯설다
2. 집중시간을 늘리자
3. 쓸데없는 고민을 줄이자
[Reference]
// Room
https://farmerkyh.tistory.com/1149
https://developer.android.com/training/data-storage/room/sqlite-room-migration?hl=ko
https://developer.android.com/training/data-storage/room?hl=ko