본문 바로가기

Programming/Android_Kotlin

Room DB 사용하기

Room DB란?

2017년 구글 I/O에서 아키텍쳐 컴포넌트에 소개된 라이브러리로, SQLite의 추상 레이어를 제공하여 SQLite의 객체를 매핑하는 역할을 한다. 다시 말해 SQLite의 모든 기능을 사용할 수 있으며, DB로의 접근을 편하게 도와주는 라이브러리라 할 수 있다.

SQLite와 비교해서 컴파일 도중 SQL에 대한 유효성 검사가 가능하며, schema가 변경될 경우에도 수동으로 업데이트할 필요 없이 쉽게 해결이 가능하다. 특히 getter, setter와 같은 상용구 코드 사용 없이 매핑이 가능해 무의미한 코드 작성 시간을 줄여준다.

학교에서 모바일 프로그래밍 과목을 수강하며 처음 접하게 된 로컬DB였는데 쉽게 따라하고 응용이 간편해 내부DB가 필요한 경우 주로 사용했다. 현재는 Realm이라는 DB를 알게 되면서 그 쪽이 더 간편해 Realm으로 옮겨갔다. 둘의 차이점과 장단점은 나중에 적어볼 예정.

구성요소 

Room DB는 3가지 요소로 구성되어있다.

 

Room DB = Database + Entity + DAO
  1. Database : database holder를 포함하며, 앱에 영구 저장되는 데이터와 기본 연결을 위한 주 액세스 지점. 테이블과 버전을 정의하게 된다.
  2. Entity : database 내의 테이블을 클래스로 나타낸 것. 데이터 모델 클래스.
  3. DAO : Database Access Object. 실질적으로 데이터베이스에 접근해서 INSERT, DELETE 등을 수행하는 메소드를 포함한다.

구현

설치

build.gradle(Module: app)

dependencies {
	...
    implementation 'android.arch.persistence.room:runtime:1.1.1'
    kapt 'android.arch.persistence.room:compiler:1.1.1'
    kaptTest 'android.arch.persistence.room:testing:1.1.1'
    ...
}

 

Room DB 사용을 위해 gradle에 dependencies를 추가해준다.

Entity

엔티티는 class를 만들어 아래와 같이 정의하면 된다.

 

@Entity(tableName = "recipe")
data class Recipe (@PrimaryKey(autoGenerate = true) var id: Long? = null,
              @ColumnInfo(name = "name") var name: String = "",
              @ColumnInfo(name = "g") var g: Int = 0,
              @ColumnInfo(name = "ml") var ml: Int = 0)

 

위는 'recipe'이라는 이름의 Table을 만들고, 해당 table에는 'id, name, g, ml'이라는 이름의 Column이 존재한다.

class는 data class가 아닌 그냥 class로 선언해도 무방하다.

DAO

질의를 수행할 DAO를 만든다. 해당 클레스에서 INSERT, DELETE 등을 수행하게 된다. 질의는 아래와 같이 정의했다.

 

@Dao
interface RecipeDao {
    @Query("SELECT * FROM recipe")
    fun getAll(): List<Recipe>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insert(recipe: Recipe)

    @Query("DELETE FROM recipe WHERE id = :saveId")
    fun deleteData(saveId: Long)

    @Query("DELETE FROM recipe")
    fun deleteAll()
}

 

쿼리는 일반적으로 SQL에서 사용하는 방식으로 입력하면 된다.

INSERT(삽입)

삽입의 경우 @Insert(onConflict = "충돌처리방식")로 정의 후 호출할 메소드를 지정해주면 된다.

 

@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(recipe: Recipe)

 

충돌처리 방식은 아래의 종류가 있다.

 

OnConflictStrategy.ABORT 충돌이 발생할 경우 처리 중단
OnConflictStrategy.FAIL 충돌이 발생할 경우 실패처리
OnConflictStrategy.IGNORE 충돌이 발생할 경우 무시
OnConflictStrategy.REPLACE 충돌이 발생할 경우 덮어쓰기
OnConflictStrategy.ROLLBACK 충돌이 발생할 경우 이전으로 되돌리기

SELECT(조회), DELETE(삭제)

조회와 삭제같은 경우 @Query("SQL문")으로 정의하고 호출할 메소드를 지정하면 된다. 매개변수를 통해 조회, 삭제를 수행하고싶다면 SQL문 내에서 변수명 앞에 콜론을 붙여 사용할 수 있다.

 

//saveId를 인자로 받아 recipe 테이블에서 id가 saveId인 row를 찾아 제거
@Query("DELETE FROM recipe WHERE id = :saveId")
fun deleteData(saveId: Long)

UPDATE(수정)

수정의 경우 @Update로 정의하고 호출할 메소드를 지정해준다. 대상은 Entity Class에서 Primary Key로 정의한 Column의 값이 일치하는 Row를 찾아 수정하게 된다.

 

@Update
fun update(recipe: Recipe)

Database

테이블과 버전을 정의하는 Database 클래스를 정의한다.

 

@Database(entities = [Recipe::class], version = 1)
abstract class RecipeDB: RoomDatabase() {
    abstract fun recipeDao(): RecipeDao

    companion object {
        private var INSTANCE: RecipeDB? = null

	//Singleton pattern
        fun getInstance(context: Context): RecipeDB? {
            if (INSTANCE == null) {
            	//synchronized : 중복 방지
                synchronized(RecipeDB::class) {
                    INSTANCE = Room.databaseBuilder(context.applicationContext,
                        RecipeDB::class.java, "recipe.db")
                        .fallbackToDestructiveMigration()
                        .build()
                }
            }
            return INSTANCE!!
        }
    }
}

사용

이렇게 정의가 끝나면 액티비티에서 직접 사용할 수 있다. 주의할 점은 Room의 경우 Main Thread에서 사용이 불가능하다. Thread, AsyncTask 등을 통해 백그라운드에서 작업해야만 한다.

우선 DB를 사용하기 위해 인스턴스를 불러와야한다.

 

val recipeDB = RecipeDB.getInstance(this)

 

이후 Thread에서 수행할 작업을 호출해주면 된다.

 

...
  Thread {
    //Room에서 DB를 읽어와 list로 저장
    recipeList = recipeDB?.recipeDao()?.getAll()!!
    mAdapter = RecipeAdapter(recipeList)

    recipeRecyclerView.adapter = mAdapter
    recipeRecyclerView.layoutManager = LinearLayoutManager(this)
  }.start()
...
  Thread {
    //데이터 클래스를 생성하여 Room에 저장
    val newRecipe = Recipe(
      null,
      itemSpinner.selectedItem.toString(),
      converted,
      convert)
    recipeDB?.recipeDao()?.insert(newRecipe)
    mAdapter.recipes = recipeDB?.recipeDao()?.getAll()!!
  }.start()
...

 

Room을 사용하면 SQLite 사용 시 귀찮은 작업이나 변수 정의 없이 간단하게 사용이 가능하기 때문에 편리한 작업이 가능하다.


참고자료

hwanine.github.io/android/Android-RoomDB/