본문 바로가기

TIL

[TIL] kotlin Room Database 개념

-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 작업을 수행할 수 있다.
  • 이 외에도 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 객체를 메서드의 매개변수로 전달해야한다.

 

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
            }
        }
    }
}
  1. @Database Annotation을 사용해 DB 클래스에 포함될 Entity 클래스들의 목록 및 Version 번호를 지정한 DB 클래스를 정의한다.
  2. 추상 메서드인 getTestDao()를 선언함으로 해당 메서드를 통해  DAO Interface인 TestDAO에 대한 접근을 가능하게 한다.
  3. 이후 companion object 내부에서 Room DB의 인스턴스를 저장하기 위해 INSTANCE 변수를 선언하여 Room DB 클래스에 대한 싱글톤 패턴을 구현하며 각각 버전 1 에서 2, 2에서 3의 버전 간의 Migration을 처리하는 객체를 정의한다. Migration에 대해서는 간단하게 설명. 추후에 추가로 학습하기로 하고 넘아간다.
  4. 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