Room에 초기 값을 넣으려면 어떻게 해야할까?

DoDoBest

·

2024. 5. 18. 00:56

사진: Unsplash 의 Tobias Fischer

 

사용자가 앱을 처음 설치하거나 저장소, 캐시를 삭제한 후 다시 실행했을 때 Room에 초기 값을 넣으려면 어떻게 해야 할까요? 공식 문서에서 안내하는 여러 방법과 문제점을 알아보겠습니다.

 

RoomDatabase.Callback()

 

Room DatabaseBuilder를 이용해 Database를 생성할 때, builder의 addCallBack 함수의 인자로 RoomDatabase.Callback 클래스를 전달할 수 있습니다.

 

Callback 클래스는 총 3가지 함수로 되어 있습니다.

 

  • onCreate : database가 최초로 생성되는 시점에 호출됩니다. 단, 모든 table이 생성된 이후에 호출된다는 점에 주의해야 합니다.
  • onOpen : database가 open 될 때마다 호출됩니다.
  • onDestructiveMigration : database가 migration 될 때 호출됩니다.

 

 

이 글의 목적인 데이터베이스가 최초로 생성되는 시점에 초기 값을 넣기 위해 onCreate 함수를 이용합니다.

 

@Database(entities = [VideoEntity::class, UserEntity::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
    abstract fun videoEntityDao(): VideoEntityDao
    abstract fun userEntityDao(): UserEntityDao

    private class AppDatabaseCallback(
        private val scope: CoroutineScope,
    ) : RoomDatabase.Callback() {
        override fun onCreate(db: SupportSQLiteDatabase) {
            super.onCreate(db)
            INSTNACE?.let { database ->
                scope.launch {
                    val userDao = database.userEntityDao()

                    userDao.addUserEntity(UserEntity.userEntity)
                }
            }
        }
    }

    companion object {
        @Volatile
        private var INSTNACE: AppDatabase? = null

        fun getInstance(
            context: Context,
            scope: CoroutineScope
        ): AppDatabase {
            return INSTNACE ?: synchronized(this) {
                val instance = INSTNACE ?: Room.databaseBuilder(
                    context,
                    AppDatabase::class.java,
                    "nbc-youtube.db"
                )
                    .addCallback(AppDatabaseCallback(scope))
                    .build()
                INSTNACE = instance
                instance
            }
        }
    }
}

 

class App : Application() {
    
    private val applicationScope = CoroutineScope(SupervisorJob())

    private val db: AppDatabase by lazy {
        AppDatabase.getInstance(applicationContext, applicationScope)
    }

    ...
}

 

 

이 코드가 가진 잠재적 문제는 무엇이 있을까요? Callback 클래스의 onCreate 함수에서 데이터를 넣는 것은 비동기적으로 동작하기 때문에, 데이터베이스에 접근하기 전에 초기 데이터가 넣어졌다는 것을 보장할 수 없다는 점입니다.

 

예를 들어, 초기 화면에서 Room에 입력된 초기 데이터를 읽어와서 UI로 사용자에게 보여주는 코드를 실행한다고 해봅시다.

초기 데이터를 읽는 코드가 초기 데이터를 넣는 코드보다 빠르다면 사용자에게 보여줄 데이터가 없기 때문에, 대응하지 않았다면 runtime exception이 발생하거나, 데이터가 없는 빈 화면이 보일 것입니다.

초기 데이터를 읽는 코드가 초기 데이터를 넣는 코드보다 느리다면 정상적으로 UI가 사용자에게 표시될 것입니다.

 

이 현상은 아래 Repository를 실행해서 직접 확인하실 수 있습니다.

코드를 실행하기 위해 Youtube API Key가 필요하며, local.properties에 아래와 같이 Key를 입력해주시면 됩니다.

 

youtube_api_key="API KEY 값 입력"

 

https://github.com/NBC-YouTube/Youtube/tree/cd8e80a23558268d510f0b694375a1030a16c9be

 

GitHub - NBC-YouTube/Youtube: 내일배움캠프 - 앱개발 심화 프로젝트 과제 - Youtube API 활용

내일배움캠프 - 앱개발 심화 프로젝트 과제 - Youtube API 활용. Contribute to NBC-YouTube/Youtube development by creating an account on GitHub.

github.com

 

Propopulating

 

Room 2.2.0에 도입된 기능으로, 앱이 시작될 때 database에 특정 데이터가 로드되는 것을 보장해줍니다.

사용하는 방법은 Room DatabaseBuilder를 이용해 Database를 생성할 때, Builder의 createFromAsset 함수에 설정할 데이터가 담긴 db 파일의 경로를 지정해주면 됩니다.

db 파일은 assets 하위에 위치해야 합니다.

 

db 파일의 schema와 Room에 설정한 schema가 일치하지 않으면 런타임 exception이 발생합니다. 두 schema가 일치하는지 빌드 과정에서 확인하기 위해 database의 schema를 export 해야 합니다.

 

그러면 순서대로 필요한 작업들을 해보겠습니다.

 

1. db 파일 생성하기

 

초기에 넣을 데이터가 담긴 db 파일을 만들기 위해 python의 sqlite3를 이용하겠습니다. db 파일은 txt 파일로 작성한 후 확장자를 변경하는 방식으로 생성할 수 없기 때문에 sqlite3가 아니더라도 DB 생성 관련 라이브러리를 이용해서 파일을 만들어주세요.

 

import sqlite3
import os

# Define the path to the database file
db_path = 'userEntity.db'

# Delete the database file if it already exists
if os.path.exists(db_path):
    os.remove(db_path)

# Connect to the SQLite database (it will be created if it doesn't exist)
conn = sqlite3.connect(db_path)
c = conn.cursor()

# Create the users table
c.execute('''
CREATE TABLE users (
    name TEXT NOT NULL,
    profile_thumbnail TEXT NOT NULL,
    channel_thumbnail TEXT NOT NULL,
    introduction TEXT NOT NULL,
    id TEXT NOT NULL PRIMARY KEY
)
''')

# Insert the initial data
c.execute('''
INSERT INTO users (name, profile_thumbnail, channel_thumbnail, introduction, id)
VALUES (?, ?, ?, ?, ?)
''', (
    "My PoKemon Channel Dummy Dummy",
    "android.resource://com.nbc.youtube/drawable/sample_user_thumbnail",
    "android.resource://com.nbc.youtube/drawable/sample_channel_thumbnail",
    "Discover the latest Pokémon news, animations, and gameplay on the official Pokémon YouTube channel. Dive into the captivating world of Pokémon and experience everything it has to offer right here!",
    "1"
))

# Commit the changes and close the connection
conn.commit()
conn.close()

print(f"Database '{db_path}' created successfully.")

 

위 코드를 실행하면 db_path로 지정한 경로에 파일이 생성됩니다.

 

2. db 파일을 assets 하위에 옮기기

 

Room database를 생성하는 코드가 있는 모듈을 클릭하고 Directory 생성을 누릅니다.

 

 

src\main\assets를 입력하고 생성합니다.

 

 

 

assets 하위에 database directory를 생성한 후, 1번에서 생성한 db 파일을 넣어줍니다.

 

 

 

 

3. createFromAsset 함수 설정

 

Room DatabaseBuilder의 createFromAsset 함수의 파라미터로 db 파일의 경로를 지정해줍니다.

 

@Database(entities = [VideoEntity::class, UserEntity::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
    abstract fun videoEntityDao(): VideoEntityDao
    abstract fun userEntityDao(): UserEntityDao

    companion object {
        @Volatile
        private var INSTNACE: AppDatabase? = null

        fun getInstance(context: Context): AppDatabase {
            return INSTNACE ?: synchronized(this) {
                val instance = INSTNACE ?: Room.databaseBuilder(
                    context,
                    AppDatabase::class.java,
                    "nbc-youtube.db"
                )
                    .createFromAsset("database/userEntity.db")
                    .build()
                INSTNACE = instance
                instance
            }
        }
    }
}

 

 

4. room schema export

 

Room schema export를 설정하기 위해 Room Gradle plugin은 추가합니다.

kotlin build gradle을 사용하는 경우 아래와 같이 추가합니다.

 

plugins {
  id("androidx.room")
}

android {
    room {
      schemaDirectory("$projectDir/schemas")
    }
    ...
}

 

toml 을 사용하는 경우, 아래와 같이 추가합니다.

 

// toml

[versions]
roomRuntime = "2.6.1"

...

[plugins]
room = { id = "androidx.room", version.ref = "roomRuntime" }

// app 모듈 단의 build.gradle
plugins {
    ...
    alias(libs.plugins.room)
}

 

그러면 모든 설정이 끝났습니다!

 

예제 코드

 

코드를 실행하기 위해 Youtube API Key가 필요하며, local.properties에 아래와 같이 Key를 입력해주시면 됩니다.

 

youtube_api_key="API KEY 값 입력"

 

 

https://github.com/NBC-YouTube/Youtube/tree/69a688d7bf0518707e16affb25a4e9151604f44d

 

GitHub - NBC-YouTube/Youtube: 내일배움캠프 - 앱개발 심화 프로젝트 과제 - Youtube API 활용

내일배움캠프 - 앱개발 심화 프로젝트 과제 - Youtube API 활용. Contribute to NBC-YouTube/Youtube development by creating an account on GitHub.

github.com

 

 

참고자료

https://developer.android.com/codelabs/android-room-with-a-view-kotlin#13

 

뷰를 사용한 Android Room - Kotlin  |  Android 개발자  |  Android Developers

이 Codelab에서는 Kotlin 코루틴과 함께 Android 아키텍처 구성요소(RoomDatabase, Entity, DAO, AndroidViewModel, LiveData)를 사용하는 Android 앱을 Kotlin으로 빌드합니다. 이 샘플 앱은 단어 목록을 Room 데이터베이스

developer.android.com

https://developer.android.com/training/data-storage/room/prepopulate

 

Room 데이터베이스 미리 채우기  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. Room 데이터베이스 미리 채우기 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 때로 특정 데이터 세트

developer.android.com

https://developer.android.com/training/data-storage/room/migrating-db-versions#export-schemas

 

Room 데이터베이스 이전  |  Android Developers

Room 라이브러리를 사용하여 데이터베이스를 안전하게 이전하는 방법 알아보기

developer.android.com