[Android] Room

Minji Jeong·2022년 6월 13일
0

Android

목록 보기
22/39
post-thumbnail

Room


Room은 안드로이드 애플리케이션에서 로컬 데이터베이스(SQLite)에 보다 쉽게 접근할 수 있도록 기능들을 제공하는 jetpack 라이브러리다.

SQLite를 직접 사용했던 기존의 방식은 쿼리문을 직접 작성해야 했다. 따라서 쿼리문이 길어질수록 복잡해졌는데, 예를 들어 아래와 같이 테이블을 생성할 때도 정의할 속성이 많아질수록 쿼리문이 복잡해져 가독성 면에서 좋지 않은 모습을 보였다. 또한 스키마가 변경되면 데이터베이스를 수동으로 업데이트 해야했다.

class DBHelper(context: Context?, db_name: String?, factory: CursorFactory?, version: Int) : SQLiteOpenHelper(context, db_name, factory, version) {
    
    override fun onCreate(db: SQLiteDatabase?) {

        val sql = "CREATE TABLE if not exists user" + "(" 
            	+ ID + " INTEGER," 
                + NAME + " TEXT," 
                + AGE + " TEXT,"
                + ADDRESS + "TEXT,"
                + PHONE_NUM + "TEXT,");"
        db!!.execSQL(sql) 
    }
    
    override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
        db!!.execSQL(SQL_DELETE_ENTRIES)
        onCreate(db)
    }
    
    override fun onDowngrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
        onUpgrade(db, oldVersion, newVersion)
    }
    
    fun insert(db: SQLiteDatabase, user: User) { 
        db.insert(TABLE_NAME, null, user)
    }
    
    companion object {
    	private const val ID = "user_id"
        private const val NAME = "user_name"
        private const val AGE = "user_age"
       	private const val ADDRESS = "user_address"
        private const val PHONE_NUM = "user_phoneNum"
    }
}


class MainActivity : AppCompatActivity() {
	
    private lateinit var db : SQLiteDatabase
    
    companion object {
    	const val DB_NAME = "user_database"
        const val DB_VERSION = 1
    }
    
    override fun onCreate(saveInstanceState: Bundle?){
    
    	btn.setOnClickListener{
        	var dbHelper = DBHelper(this, DB_NAME, null, DB_VERSION)
            db = dbHelper.writableDatabase
            dbHelper.onCreate(db)
            dbHelper.insert(db, User(id, name, age))
        }
    }
}

하지만 Room을 사용하면 database, entity, interface에 대한 어노테이션을 사용하여 간단하게 코드를 작성할 수 있다. 또한 각 컴포넌트를 분리해서 코드를 작성하기 때문에 더 직관적이고, 스키마 변경시 별다른 코드를 작성할 필요 없이 자동으로 데이터베이스를 업데이트 할 수 있다.

@Database(entities = [User::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
    abstract val scheduleDao : Dao
}

@Entity(tableName = "user")
data class User(
	@PrimaryKey(autoGenerate = true)
    var id : Int,
    var name : String,
    var age : String
    var address : String,
    var phoneNum : String
)

@Dao
interface Dao {
	@Insert
    fun addUser(user: User)
}

또한 AAC 의 구성요소로써, ViewModel과 연계하여 사용할 수도 있다.

class ViewModel(private val dao: Dao) : ViewModel() {
	
    fun addUser(user: User){
    	dao.addUser(user)
    }
}

SQLite를 직접 사용했던 기존의 방식은 복잡한 쿼리, 수동 업데이트 뿐만 아니라 다음과 같은 단점들이 있었다.

  • SQL 코드와 Java 데이터 개체 간의 변환에는 많은 보일러 플레이트 코트가 발생할 수 있다.
  • 컴파일 시간에 쿼리에 대한 유효성 검사를 하지 않는다.

이와 같은 단점들을 해결하기 위해 구글에선 Room을 사용하는 것을 매우 권장하고 있다.

Components

Room은 db, table, interface 역할을 하는 3개의 주요 컴포넌트들을 제공하며, 각 컴포넌트는 특정 어노테이션을 붙여서 사용할 수 있다. 각 컴포넌트를 어떻게 사용해야 하는지는 실습을 진행하며 알아보자.

먼저 build.gradle(Module)에 다음의 종속 항목을 추가하자.

dependencies {
	def roomVersion = "2.4.2"
    
    implementation "androidx.room:room-runtime:$roomVersion"
    kapt "androidx.room:room-compiler:$roomVersion"
    implementation "androidx.room:room-ktx:$roomVersion"
}

나는 MY_DB라는 이름의 데이터베이스를 만들고, id(primary key), name, age 속성으로 구성된 user 테이블을 만들 것이다.

1. Database
데이터베이스 객체에 대한 추상 클래스로, @Database 어노테이션으로 표현할 수 있다. 반드시 RoomDatabase를 상속해야 하고, 런타임에 Room.databaseBuilder 또는 Room.inMemoryDatabaseBuilder로 인스턴스를 만들어서 사용할 수 있다. 데이터베이스 객체는 생성시 많은 리소스를 소모하므로 객체 생성은 싱글톤 패턴으로 구현하자.

@Database(entities = [User::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
    abstract val dao : UserDao

    companion object{
        private var instance: AppDatabase?=null

        @Synchronized
        fun getInstance(context: Context): AppDatabase? {
            if (instance == null){
                synchronized(AppDatabase::class){
                    instance=Room.databaseBuilder(
                        context.applicationContext,
                        AppDatabase::class.java,
                        "MY_DB"
                    ).build()
                }
            }
            return instance
        }
    }
}

@Database : 데이터베이스를 표현하는 어노테이션이다. 데이터베이스를 구성하는 엔터티들과 버전을 명시하고, exportSchema로 데이터베이스 스키마를 폴더로 내보내거나 내보내지 않을 수 있다.
@Synchronized : 여러개의 인스턴스가 생성되는 것을 방지한다(하나의 인스턴스를 공유해서 사용). 이 어노테이션을 사용하지 않으면 각각의 스레드에서 새 데이터베이스 인스턴스를 생성할 수 있게 되고, 이는 많은 리소스 소모를 초래한다.

2. Entity
데이터베이스 내의 테이블로, @Entity 어노테이션으로 표현할 수 있다.

@Entity(tableName = "user_table") 
data class User(
    @PrimaryKey(autoGenerate = true)
    var id : Int,
    var name : String,
    var age : Int
)

@PrimaryKey : 테이블의 Primary Key를 정의한다. autoGenerate true 설정 시 데이터 삽입 시마다 값이 자동으로 증가된다.
@ColumnInfo : Room은 기본적으로 필드 이름을 데이터베이스의 열 이름으로 사용한다. 열 이름을 다르게 하려면 @ColumnInfo 주석을 필드에 추가하고 name 속성을 정의하면 된다.

@ColomnInfo(name = "user_name") val userName : String

3. Dao
데이터베이스에 액세스하는데 사용되는 메서드가 정의된 인터페이스로, @Dao 어노테이션으로 표현할 수 있다. Insert, Delete, Update 와 같은 기본적인 기능들은 SQL 작성 없이 어노테이션만 붙여서 사용하면 되기 때문에 매우 편리하며, 만약 다른 기능을 구현하고 싶다면 @Query 어노테이션으로 원하는 동작에 대한 sql을 작성하면 된다.

@Dao
interface UserDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun addUser(user : User)

    @Delete
    fun deleteUser(id : Int)

    @Query("select * from user_table")
    fun getAll() : LiveData<List<User>>
}

onConflict = OnConflictStrategy.REPLACE : 데이터 삽입 시 primary key가 겹치는 것이 있으면 덮어쓴다.

Room + ViewModel + Koin

나는 ViewModel을 사용해 뷰(fragment)와 모델(데이터베이스에 저장된 데이터) 간의 의존성을 없애고, 메인 쓰레드에서 Koin으로 뷰모델에 대한 의존성을 주입해 사용했다.

ViewModel.kt

class ViewModel(private val dao: Dao) : ViewModel() {
	
    fun getAll() : LiveData<List<User>> = dao.getAll()
    
    fun addUser(user : User){
    	dao.addUser(user)
    }
    
    fun deleteUser(id : Int){
    	dao.deleteUser(id)
    }
}

module.kt

val userModule = module {

	// Database 클래스의 인스턴스 생성 코드를 module 내부에 작성한 것
    fun provideDatabase(application: Application) : AppDatabase {
        return Room.databaseBuilder(
        	application, AppDatabase::class.java, "MY_DB")
            .fallbackToDestructiveMigration()
            .build()
    }

    fun provideDao(database: AppDatabase) : Dao {
        return database.Dao
    }

    single {
        provideDatabase(androidApplication())
    }

    single {
        provideDao(get())
    }
}

val viewModel = module {
    viewModel {
        ViewModel(get())
    }
}

App.kt

class App : Application() {

    companion object{
        private lateinit var app : App
    }

    override fun onCreate() {
        super.onCreate()
        app = this
        startKoin {
            androidContext(this@App)
            modules(
                module,
                viewModel
            )
        }
    }
}

MainFragment.kt

class MainFragment : Fragment(){

	private val viewModel : ViewModel by inject()
    
    ...
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   	super.onViewCreated(view, savedInstanceState)
    
    viewModel.getAll().observe(this, androidx.lifecycle.Observer{
    	...
       })
    }
}

References

https://developer.android.com/reference/androidx/room/package-summary
https://roomedia.tistory.com/entry/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-RoomDatabase-in-Java
https://developer.android.com/training/data-storage/room/accessing-data?hl=ko
https://stackoverflow.com/questions/50650077/sqlite-database-vs-room-persistence-library
https://developer.android.com/training/data-storage/sqlite

profile
Mobile Software Engineer

0개의 댓글