[Android] 로컬 데이터베이스 Room 사용법 총정리

2022. 1. 22. 22:57개발을 파헤치다/Android

반응형

Room 데이터베이스

Room은 안드로이드에서 손쉽게 사용할 수 있는 로컬 데이터베이스입니다. Realm에 비해 굉장히 낮은 용량(약 64kb)을 차지하면서도 꽤 괜찮은 성능과 쉽게 사용할 수 있다는 장점이 있습니다. 안드로이드 클라이언트에서 데이터를 처리해야 한다면 이 가이드를 보고 약 30분 정도면 적용이 가능하리라 생각합니다.

설치하기

App 레벨의 build.gradle에 아래와 같이 추가해줍니다.

// Room Database
implementation 'androidx.room:room-runtime:2.4.1'
kapt 'androidx.room:room-compiler:2.4.1'

Kotlin에서는 kapt 컴파일러 플러그인과 함께 사용하도록 되어있기때문에 아래와 같이 플러그인 설정도 추가해주어야 합니다.

// App 레벨 build.gradle 최상단
plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-kapt'
}

 

기본구조



Room은 새로운 로컬 데이터베이스가 아닙니다. 기존에 안드로이드에서 사용하던 SQLite를 좀 더 쉽고 객체지향적으로 사용할 수 있게 한 겹 덮어 씌운 ORM(Object Relation Mapping)이라고 할 수 있습니다.
Room을 사용하려면 아래 요소들을 구현해야 합니다.

  • Entity : 데이터베이스의 테이블에 해당됩니다. 클래스 형태로 정의하며 클래스 내 속성 값들이 곧 테이블의 칼럼이 됩니다.
  • DAO(Data Access Object) : 테이블의 데이터에 접근할 수 있는 인터페이스를 정의한 클래스입니다. 데이터 가져오기, 삽입하기, 삭제하기 등의 메서드를 정의합니다.
  • Database : 데이터베이스를 생성하고 관리하는 총괄 클래스입니다. Entity 클래스와 Dao 클래스들을 등록해서 데이터베이스를 사용할 수 있게 해 줍니다. 싱글톤 클래스로 구현하는 게 일반적입니다.

 

Entity 클래스 구현

@Entity(tableName = "local_search_keyword")
data class SearchKeyword(
    @PrimaryKey(autoGenerate = true)
    var id: Int = 0,
    var keyword: String,
    var createdDateTime: ZonedDateTime = ZonedDateTime.now()
)

데이터베이스에 테이블을 만들어준다고 생각하면 됩니다.
테이블 이름을 따로 설정하고 싶으면 Entity 어노테이션에 tableName 속성 값을 지정해주면 됩니다.
테이블에는 반드시 Primary Key가 설정이 돼야 합니다. 따로 값이 있는 경우 어노테이션만 적용하고
자동 생성 값으로 하려면 autoGenerate 속성에 true로 값을 설정하면 됩니다.

Dao 인터페이스 구현

@Dao
interface SearchKeywordDao {

    // 최근 검색어 저장    
    @Insert    
    fun insertKeyword(keyword: SearchKeyword)

    // 최근 검색어 조회    
    @Query("select * from local_search_keyword order by createdDateTime DESC LIMIT :perPage OFFSET :index * :perPage")
    fun getKeywordList(index: Int = 0, perPage: Int = 10): List<SearchKeyword>

    // 최근 검색어 삭제    
    @Delete    
    fun deleteKeyword(keyword: SearchKeyword)

    // 검색어 가져오기    
    @Query("select * from local_search_keyword where keyword=:keyword")
    fun getKeyword(keyword: String): SearchKeyword?
}

이제 테이블에서 어떻게 데이터를 가져올 것인지 데이터베이스에 알려줘야 합니다.
위와 같이 Dao 어노테이션을 붙인 인터페이스를 구현해줍니다. 
데이터를 삽입 및 삭제할 경우에는 어노테이션을 붙여주면 간단하게 해결할 수 있습니다.
다만 조회의 경우 조건이 들어갈 수 있기 때문에 Query 어노테이션을 활용해 직접 쿼리문을 작성해줄 수 있습니다.
SQL에 변수를 지정할 수 있는데요. 함수의 파라미터 변수 앞에 :을 붙이면 SQL에 변숫값이 적용되어 실행됩니다.

Database 클래스 구현

@Database(entities = [SearchKeyword::class, ProductEntity::class], version=2, exportSchema = false)
@TypeConverters(LocalDataTypeConverter::class)
abstract class LocalDatabase : RoomDatabase() {
    // 최근 검색어 Interface
    abstract fun SearchKeywordDao() : SearchKeywordDao
    // 찜 상품 Interface
    abstract fun ProductEntityDao(): ProductEntityDao


    // Singleton 클래스 구현
    companion object{
        // DB 이름
        private const val dbName = "localDb"
        private var INSTANCE: LocalDatabase? = null

        fun getInstance(context: Context): LocalDatabase?{
            if(INSTANCE == null){
                synchronized(LocalDatabase::class){
                    INSTANCE = Room.databaseBuilder(context.applicationContext,
                    LocalDatabase::class.java, dbName)
                        .fallbackToDestructiveMigration()
                        .build()
                }
            }
            return INSTANCE
        }
    }
}

이제 데이터베이스 사용을 위한 총괄 클래스를 구현해줍니다.
Database 어노테이션에 구현한 Entity 클래스들을 모두 넣어줍니다.
version의 경우 데이터베이스 구조가 바뀔 때마다 버전을 업시켜줘야 하는데요. 기존 테이블 변경이 있는 경우에는 Migration 코드를 직접 구현해줘야 합니다. export Schema의 경우 true로 설정하면 데이터베이스 테이블 스키마가 파일 형태로 저장됩니다.

Kotlin이나 Java에서 사용하는 데이터 타입이 Room의 데이터 타입과 맞지 않아 Type 관련된 에러가 발생할 수 있는데요. 이럴 때에는 해당 데이터 타입을 데이터베이스에 맞게 변경해주는 Converter를 구현해야 합니다. 구현한 Converter는 어노테이션을 통해 적용할 수 있습니다.

데이터에 접근할 수 있도록 Dao 클래스들도 정의해줍니다. 
사용할 때에는 getInstance 메서드를 통해 빌드된 Room 객체를 받아와서 사용하면 됩니다.

Type Converter 구현

class LocalDataTypeConverter {

    companion object{
        @TypeConverter
        @JvmStatic
        fun fromZonedDateTime(value: ZonedDateTime): Long{
            return value.toEpochSecond()
        }

        @TypeConverter
        @JvmStatic
        fun toZonedDateTime(value: Long): ZonedDateTime{
            val instant = Instant.ofEpochSecond(value)
            return ZonedDateTime.ofInstant(instant, ZoneId.of("Asia/Seoul"))
        }
    }
}

Java 8의 ZonedDateTime의 경우 데이터베이스에 저장하려고 하면 에러가 발생합니다.
이럴 때에는 Epoch Second로 형태를 변경한 뒤 저장하고 불러올 때는 다시 ZonedDateTime으로 바꾸도록 구현해야 하는데요. 이렇게 TypeConverter를 구현한 뒤 Database에 어노테이션으로 적용할 수 있습니다.

사용하기

// 상품 찜하기 Local API 호출
    private fun registerWishListLocalCall(product: Product, callback: BaseNetworkInterface.SimpleDataResponseCallback){
        // 로컬 저장용 ProductEntity를 생성한다
        val productEntity = ProductEntity(
            id = product.id,
            thumbnailUrl = thumbnailUrl,
            name = product.name,
            price = product.price
        )
        // Room에 상품을 저장한다
        val db = ServiceLocator.provideLocalDatabase()
        // Coroutine을 통해 다른 Thread에서 처리되도록 설정
        CoroutineScope(Dispatchers.IO).launch {
            try{
                db.ProductEntityDao().insertProduct(productEntity)
                withContext(Dispatchers.Main){
                    // Main Thread에서 호출되도록 설정 
                    callback.onSuccess()
                }

            }catch (e: SQLiteException){
                Log.d(TAG, "Room Insert Error: ".plus(e.toString()))
                withContext(Dispatchers.Main){
                    callback.onFailure()
                }
            }

        }
    }

데이터베이스 작업은 Main Thread에서 처리할 수 없습니다. 그렇기 때문에 Coroutine이나 Thread를 사용해서 작업해야 하는데요. 위처럼 Coroutine을 활용해서 Room 객체의 Dao 클래스를 통해 데이터에 접근할 수 있습니다.
가져온 데이터를 콜백 함수를 통해 넘겨주거나, View 레벨의 메서드를 통해 UI 업데이트를 할 때에는 반드시 Main Thread에서 처리해줘야 합니다. 이는 Coroutine 안에서 withContext를 활용해서 해결할 수 있습니다.
에러가 발생하면 try catch을 통해 처리할 수 있습니다. SQLiteException을 발생시키기 때문에 이에 따라 로직을 구현하면 됩니다.

반응형