Android Jetpack

책 검색결과 저장을 위한 Room DB 구현해보기

wdadaww 2023. 1. 27. 16:52

 책 검색 결과를 통해 마음에 드는 책이 있으면 Room 을 통해 SQLite 에 저장하고 불러오는 기능을 구현해보겟습니다.

 


  • Room 

-> Room 라이브러리는 엔티티(Entity) , 데이터 접근 객체(DAO) , 데이터베이스(DB) 3가지 구성요소가 있습니다.


 


출처: Room을 사용하여 로컬 데이터베이스에 데이터 저장  |  Android 개발자  |  Android Developers

 

Room을 사용하여 로컬 데이터베이스에 데이터 저장  |  Android 개발자  |  Android Developers

Room 라이브러리를 사용하여 더 쉽게 데이터를 유지하는 방법 알아보기

developer.android.com

 

  • build.gradle (앱 모듈)
//Room
implementation 'androidx.room:room-runtime:2.5.0'
kapt 'androidx.room:room-compiler:2.5.0'
implementation 'androidx.room:room-ktx:2.5.0'

 

코틀린에서는  어노테이션 처리기를 위해 kapt를 추가해준다.

plugins {
    id 'kotlin-kapt'

 

  • Entity  : DB내의 테이블, 즉 DB에 저장할 데이터 형식으로 class의 변수들이 컬럼이 되어 테이블을 구성한다

- Annotation

@Entity(tableName = " ")

테이블 이름을 선언한다.( 기본적으로 entity class 이름을 database table 이름으로 인식한다.)

 

@Primarykey

각 Entity는 1개의 Primary key를 가져야 한다. 일반적으로  고유한 id 값으로 설정한다.

 

@ColumInfo

table 내 column을 변수 와 매칭한다.

package com.example.kakaobook.ui.view.data.model


import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import kotlinx.parcelize.Parcelize

@Parcelize
@JsonClass(generateAdapter = true)
@Entity
data class Book(
    @Json(name = "authors")
    val authors: List<String>,
    @Json(name = "contents")
    val contents: String,
    @Json(name = "datetime")
    val datetime: String,
    @PrimaryKey(autoGenerate = false)
    @Json(name = "isbn")
    val isbn: String,
    @Json(name = "price")
    val price: Int,
    @Json(name = "publisher")
    val publisher: String,
    @ColumnInfo(name = "sale_price")
    @Json(name = "sale_price")
    val salePrice: Int,
    @Json(name = "status")
    val status: String,
    @Json(name = "thumbnail")
    val thumbnail: String,
    @Json(name = "title")
    val title: String,
    @Json(name = "translators")
    val translators: List<String>,
    @Json(name = "url")
    val url: String
) : Parcelable

기존에 카카오 책 검색에서 가져오던 Book 클래스에 @Entity를 통해 데이터베이스에 사용할 엔티티로 만들어준다. 또한 위에서 설명한 대로 데이터의 고유값인 isbn에 @PrimaryKey(autoGenerate = false)를 통해  PrimaryKey로 만들어주고 salePrice 즉 카멜 케이스 형태로 저장된 책 가격을 @ColumnInfo 어노테이션을 통해 스네이크 케이스로 변환해준다.

 

 

  • DAO: 데이터베이스에 접근하여 수행할 작업을 메서드 형태로 정의하는 부분(SQL 쿼리도 지정 가능)

@Insert

"onConflict = OnConflictStrategy.REPLACE"  로 만일 동일한 PrimaryKey가 있을 경우 덮어쓸 수 있다.

 

@Update
Entity 업데이트. Return 값으로 업데이트된 행 수를 받을 수 있다.

 

@Delete
Entity 삭제. Return 값으로 삭제된 행 수를 받을 수 있다.

 

@Query
@Query를 사용하여 DB를 조회할 수 있다.

package com.example.kakaobook.ui.view.data.db

import androidx.lifecycle.LiveData
import androidx.room.*
import com.example.kakaobook.ui.view.data.model.Book



@Dao
interface BookSearchDAO {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertBook(book: Book)

    @Delete
    suspend fun deleteBook(book: Book)

    @Query("SELECT * FROM books")
    fun getFavoriteBooks(): LiveData<List<Book>>


}
  • 데이터를 추가할 때는 기본적으로 @Insert 어노테이션을 사용하고 onConflict = OnConflictStrategy.REPLACE를 통해 동일한 isbn을 가지고 있는 아이템이 존재한다면 덮어쓰도록 구현한다.
  • 데이터를 삭제할 때는 @Delete를 통해 간단하게 구현 가능하다.
  • 데이터를 조회할 때는  @Query를 써서 sql 쿼리문을 작성하고 위 코드에서는 "SELECT * FROM books"를 써서 books 테이블에 있는 모든 데이터를 가져오도록 했다. 이때 반환받는 데이터를 LiveData로 하엿습니다
  • Read 기능을 구현하는 @Query를 제외한 Create, Update, Delete 작업은 시간이 걸리는 작업이기 때문에 코루틴 안에서 비동기적으로 수행하기로 했다. 따라서 suspend 키워드를 함수 앞에 추가해준다.

다음은 DAO와 엔티티(Entity)의 동작을 수행해주는  데이터베이스 클래스를 만들어준다.

 

  • 데이터베이스 : 데이터베이스의 생성 및 버전 관리를 하는 클래스이다. Room DB에서 DAO를 가져와 객체를 통해 데이터를 CRUD 한다.

@Database
Class가 Database임을 알려주는 어노테이션.

  • entities
    : 이 DB에 어떤 테이블들이 있는지 명시한다.
  • version
    : Scheme가 바뀔 때 이 version도 바뀌어야 한다.
  • exportSchema
    : Room의 Schema 구조를 폴더로 Export 할 수 있다.
package com.example.kakaobook.ui.view.data.db

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import com.example.kakaobook.ui.view.data.model.Book

@Database(
    entities = [Book::class],
    version = 1,
    exportSchema = false
)
abstract class BookSearchDatabase : RoomDatabase() {

    abstract fun bookSearchDao(): BookSearchDAO

    companion object {
        @Volatile
        private var INSTANCE: BookSearchDatabase? = null

        private fun buildDataBase(context: Context): BookSearchDatabase =
            Room.databaseBuilder(
                context.applicationContext,
                BookSearchDatabase::class.java, "favorite-books"
            ).build()

        fun getInstance(context: Context): BookSearchDatabase =
            INSTANCE ?: synchronized(this) {
                INSTANCE ?: buildDataBase(context).also { INSTANCE = it }
            }
    }
}
  • 클래스에는 데이터베이스와 연결된 데이터 항목을 모두 나열하는 entities 배열이 포함된 @Database 주석이 달려야 합니다.
  • 클래스는 RoomDatabase 를 확장하는 추상 클래스여야 합니다.
  •  데이터베이스 클래스는 DAO 클래스의 인스턴스를 반환하는 추상 메서드를 정의해야 합니다.
  • 데이터베이스의 여러 인스턴스가 동시에 열리는 것을 막기 위해 BookSearchDatabase 싱글톤 으로 정의했습니다

  • TypeConverter
  • 우리가 Entity로 사용할 Book 클래스에는 authors라는 변수가 있다. 이 authors는 List<String>타입이다.
  • Room은 기본 유형과 박싱된 유형 간 변환을 위한 기능을 제공하지만 항목 간 객체 참조는 허용하지 않습니다. 본 문서에서는 유형 변환기를 사용하는 방법 및 Room이 객체 참조를 지원하지 않는 이유를 설명합니다.
  • 출처: Room을 사용하여 복잡한 데이터 참조  |  Android 개발자  |  Android Developers
 

Room을 사용하여 복잡한 데이터 참조  |  Android 개발자  |  Android Developers

Room을 사용하여 복잡한 데이터 참조 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Room은 기본 유형과 박싱된 유형 간 변환을 위한 기능을 제공하지만 항목

developer.android.com

-> 따라서 우리는 Type Converter를 사용하여 authors의 List<String>을 일반 String 타입으로 변환하여 저장할 것이다.

이를 위해 데이터 직렬화를 해야 하는데 간단하게 사용할 수 있는 Kotlinx Serilaization 을 이용해보겟습니다.

 

  • build.gradle(project)
plugins {
    ...
    id 'org.jetbrains.kotlin.plugin.serialization' version '1.6.10' apply false
}
  • build.gradle(app module)
dependencies {

    // Kotlin serialization
    implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3'
}
  • List<String>과 String을 상호 변환하는 @TypeConverter를 추가합니다.
package com.example.kakaobook.ui.view.data.db

import androidx.room.TypeConverter
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

class OrmConverter {
    @TypeConverter
    fun fromList(value: List<String>) = Json.encodeToString(value)
    @TypeConverter
    fun toList (value: String) = Json.decodeFromString<List<String>>(value)
}

List<String>이 들어오면 encodeToString을 통해 String으로 encode 해주고 String을 받으면  List<String>으로 decodeFromString 해주는 형식이다. 

 

  •   데이터베이스에 @TypeConverters(OrmConverter::class)를 추가해주면 컨버터가 필요한 상황에 알아서 TypeConverter를 사용하게 된다.
package com.example.kakaobook.ui.view.data.db

import android.content.Context
import androidx.room.*
import com.example.kakaobook.ui.view.data.model.Book

@Database(
    entities = [Book::class],
    version = 1,
    exportSchema = false
)
@TypeConverters(OrmConverter::class)
abstract class BookSearchDatabase : RoomDatabase() {

    abstract fun bookSearchDao(): BookSearchDAO

    companion object {
        @Volatile
        private var INSTANCE: BookSearchDatabase? = null

        private fun buildDataBase(context: Context): BookSearchDatabase =
            Room.databaseBuilder(
                context.applicationContext,
                BookSearchDatabase::class.java, "favorite-books"
            ).build()

        fun getInstance(context: Context): BookSearchDatabase =
            INSTANCE ?: synchronized(this) {
                INSTANCE ?: buildDataBase(context).also { INSTANCE = it }
            }
    }
}