깨알 개념/Android

[Android] Room - Entity, DAO, Database

interfacer_han 2024. 2. 24. 19:37

#1 이전 글

 

[Android] Room - 기초, INSERT와 DELETE 연습

#1 Room 소개 Room을 사용하여 로컬 데이터베이스에 데이터 저장 | Android Developers Room 라이브러리를 사용하여 더 쉽게 데이터를 유지하는 방법 알아보기 developer.android.com SQLite는 모바일 기기를 위한

kenel.tistory.com

본 게시글은 Room의 기본을 담은 앱을 만들기 위한 이전 게시글에서 이어진다. 여기서 Room의 3가지 핵심 클래스인 Entity 클래스, DAO 클래스, Database 클래스의 구현을 다룬다. 이전 게시글에서 모듈 수준 build.gradle.kts에서 필요한 라이브러리를 다운로드해야 본 게시글을 진행할 수 있다.

#2 Entity, DAO, (Room) Database의 관계

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

Room Database는 자신과 연결된 DAO 인스턴스를 Application에 제공한다. 그러면 Application에서 DAO를 사용하여 데이터베이스의 데이터의 구분 단위인 Entity의 인스턴스를 참조할 수 있게 된다. 즉, INSERT, UPDATE, DELETE 명령을 수행할 수 있다는 말이다.
 

#3 Database에서 사용할 테이블 (Entity 디자인하기)

 user_id user_name user_email
... ... ...

user_id가 기본키이고 총 3개의 Column을 가지는 테이블이다. 
 

#4 Entity 클래스 생성하기 (User.kt)

// package com.example.roombasics.db

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "user_data_table")
data class User(

    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "user_id")
    val id: Int,

    @ColumnInfo(name = "user_name")
    var name: String,

    @ColumnInfo(name = "user_email")
    var email: String
)

#3의 정보를 토대로 Room Entity 클래스를 만들면 위 코드와 같다. 기본키인 id는 한번 할당되면 이후로 바꿀 일이 없으므로 val 변수로 둔다. 반면, name이나 email의 경우는 UPDATE 시에 변경될 여지가 있다. 따라서 var 변수로 설정한다.

 

@PrimaryKey의 autoGenerate 속성이 true로 설정되어있으면, 해당 Entity에 기본키로서의 id값을 명시하여 INSERT하지 않아도 알아서 id값을 생성한다. 
 

#5 DAO(Database Access Object Interface) 클래스 (UserDAO.kt)

#5-1 개요

// package com.example.roombasics.db

import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update

@Dao
interface UserDAO {

    @Insert
    suspend fun insertUser(user: User): Long

    @Insert(onConflict = OnConflictStrategy.REPLACE) // 충돌 시 동작 정의
    suspend fun insertUser2(user: User): Long

    @Insert
    suspend fun insertUser2(user: List<User>): List<Long> // 메소드 오버로딩

    @Insert
    suspend fun insertUser3(user: List<User>): Array<Long>

    @Update
    suspend fun updateUser(user: User)

    @Delete
    suspend fun deleteUser(user: User)

    @Query("INSERT INTO user_data_table (user_name, user_email) VALUES (:name, :email)")
    suspend fun insertUser4(name: String, email: String): Long

    @Query("DELETE FROM user_data_table")
    suspend fun deleteAll()

    @Query("SELECT * FROM user_data_table")
    fun getAllUsers(): LiveData<List<User>>
}

#4에서 구성한 데이터베이스 테이블에 접근하기 위한 인터페이스다. Room은 Annotation을 식별해 작동하기 때문에, 실제 함수 이름은 Room의 동작에 영향을 미치지 않는다. 예를 들어, @Insert 어노테이션이 붙은 insertUser() 함수 이름을 abcdefg()와 같이 아무렇게나 바꿔도 agcdefg() 함수는 계속 @Insert의 동작을 수행한다. 또, 위 코드에서 보듯 @Insert를 메소드 오버로딩할 수도 있다. 오버로딩된 함수는 List<User>를 매개변수로 전달받는데, 이러한 형태는 여러 개의 Entity를 Insert할 때 사용된다.

또, 함수들에 suspend 키워드가 붙은 이유가 있다 (suspend 키워드가 붙지 않은 getAllUsers()는 #5-5에서 설명함). Room은 Main 스레드에서 Database로의 접근을 지원하지 않는다. 해당 작업이 Main 스레드의 주요 역할인 UI 갱신을 Block할 수도 있기 때문이다. 그래서 우리는 이 함수를 Background에서 돌려야 한다 즉, 코루틴을 이용해 멀티스레딩을 구현해야 한다.
 

#5-1 @Insert 어노테이션

Insert는 setter와 비슷한 느낌이지만, return이 존재할 수 있다. 가령, Insert한 User의 id값을 반환하는 식으로 말이다. 이 경우 반환형은 Room의 설계 상 Long 또는 List<Long>이어야 한다고 한다. List<Long>의 경우 2개 이상의 Entity를 넣은 경우의 반환형이다. return을 생략(Unit형)해도 된다. 물론 이러면 Insert한 User의 id값을 참조할 수 없을 것이다.
 

 

OnConflictStrategy  |  Android Developers

androidx.appsearch.builtintypes.properties

developer.android.com

@Insert와 @Update 어노테이션에는 onConfilct라는 속성이 있다. 이는 데이터베이스의 무결성을 보장하기 위한 속성으로, 데이터베이스에서 어떤 충돌이 일어났을 때의 동작을 정의한다. 이 속성에 OnConflictStrategy.REPLACE를 할당하면, (기본키 비교로) 이미 존재하는 Entity를 넣었을 때, 원래의 Entity를 삭제하고 새로 넣은 Entity가 대신 들어간다. OnConflictStrategy.IGNORE는 충돌을 무시한다. 즉, 새로 넣은 Entity는 무시되고 원래 있던 Entity는 계속 살아있게 된다. 그리고 -1이 반환된다. OnConflictStrategy.ABORT는 충돌이 일어나면 Transaction을 Rollback한다.
 

#5-2 @Update 어노테이션

@Insert의 설명에서와 마찬가지로, List를 매개변수로 받아 2개 이상의 Entity를 한 번에 Update할 수도 있고 Update가 수행된 Entity의 기본키 값을 return받을 수 있다.
 

#5-3 @Delete 어노테이션

@Insert의 설명에서와 마찬가지로, List를 매개변수로 받아 2개 이상의 Entity를 한 번에 Delete할 수도 있고 Delete가 수행된 Entity의 기본키 값을 return받을 수 있다.
 

#5-4 @Query 어노테이션

#5-1 ~ #5-3은 SQLite의 Query문을 간편히 쓸 수 있게 가공된 함수들의 나열이었다. 예를 들어, @Insert 어노테이션이 달린 InsertUser()는 "INSERT INTO user_data_table (user_name, user_email) VALUES (:name, :email)"라는 Query문을 수행하는 InsertUser4()와 그 동작이 같다. 즉, 복잡하고 번거로울 수 있는 기본 동작을 @Insert라는 어노테이션으로 퉁친 것이다.
 
@Query 어노테이션은 가공되지 않은 날 것의 Query문 자체를 의미한다. 따라서 조금 더 지엽적인, 조금 더 기본적이지 않은 동작을 @Query 어노테이션에 정의한다. 위 코드에서는 테이블을 삭제함으로써 그 속에 담긴 모든 Entity를 Delete하는 동작 등을 정의했다.
 

#5-5 LiveData와 suspend 키워드의 생략

getAllUsers()처럼 반환형이 LiveData라면, suspend 키워드를 생략한다. 생략하지 않으면 컴파일 에러가 나는데, 그 내용은 아래와 같다.
 

Dao functions that have a suspend modifier must not return a deferred/async type (androidx.lifecycle.LiveData). Most probably this is an error. Consider changing the return type or removing the suspend modifier.

에러 메시지의 내용은 한 마디로, LiveData를 반환하는 함수에 suspend 키워드를 달지 말라는 것이다. 왜냐하면, LiveData는 데이터가 아니라 데이터 스트림이기 때문이다. 데이터 스트림은 데이터의 연속적인 흐름을 나타내는 개념이다. LiveData라는 객체 자체가 이미 그 자체로 비동기적(병렬적)인 동작을 독립적으로 내재하고있기 때문에, 함수에 suspend를 붙일 필요가 없는 것이다.
 

#6 Room 데이터베이스 클래스 (UserDatabse.kt)

// package com.example.roombasics.db

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase

@Database(entities = [User::class], version = 1)
abstract class UserDatabase : RoomDatabase() {

    abstract val userDAO: UserDAO

    companion object {
        /* @Volatile 어노테이션은 클래스의 어떤 프로퍼티(필드)가 Update됐을 때,
         * 그 갱신된 값을 다른 스레드에서 바로 읽게(= 캐시가 아닌 메모리를 읽게) 한다.
         * 이 어노테이션이 없다면, 다른 스레드에서 갱신 이전의 값으로 잘못 읽을 수도 있다.
         * 캐시에는 시차가 존재하기 때문이다.
         */
        @Volatile
        private var INSTANCE: UserDatabase? = null
        fun getInstance(context: Context): UserDatabase {
            synchronized(this) {
                var instance = INSTANCE
                if (instance == null) {
                    instance = Room.databaseBuilder(
                        context.applicationContext,
                        UserDatabase::class.java,
                        "user_data_database"
                    ).build()
                    INSTANCE = instance
                }
                return instance
            }
        }
    }
}

Entity 클래스와 해당 Entity를 다루는 DAO 클래스를 구현했다. 남은 것은 Application에 탑재된 실제 Database를 대변하는 데이터베이스 클래스를 구현해본다. 먼저 @Database 어노테이션과 함께, 해당 데이터베이스에서 사용할 Entity 클래스 그리고 데이터베이스의 버전을 적는다. 데이터베이스 버전은 데이터베이스를 유지보수하면서 하나씩 올려가는 숫자다. 지금은 처음 만들었으므로 1이라고 적었다. 프로퍼티로 DAO도 추가한다. DAO는 이 Database 클래스에 접근하기 위한 진입문 역할을 한다.
 
companion object { ... }를 보면 이 Database 클래스를 Singleton 패턴으로 구현하고 있음을 알 수 있다. Database 객체는 굳이 하나 이상 만들어질 필요가 없기 때문이다. 이전 문장의 위키백과 링크에서, 코틀린에서의 싱글톤 패턴 예시가 object 키워드를 이용해 구현한 코드로 적혀있다. 하지만, @Database는 abstract class로 object가 아니다. 따라서 companion object { ... }를 이용해 싱글톤 패턴을 구현했다. object 키워드 버전에 비해 더 복잡하지만, 어쩔 수 없다.
 
위 코드는 Room의 @Database를 구현하는 다른 많은 프로젝트들도 똑같이 사용된다고 한다. 복사/붙여넣기를 자주하게 될 코드로 보인다.
 

#7 요약

Entity는 데이터, Database는 말 그대로 Base, DAO는 그 둘을 잇는 관문이다.
 

#8 다시 이전글로

 

[Android] Room - 기초, INSERT와 DELETE 연습

#1 Room 소개 Room을 사용하여 로컬 데이터베이스에 데이터 저장 | Android Developers Room 라이브러리를 사용하여 더 쉽게 데이터를 유지하는 방법 알아보기 developer.android.com SQLite는 모바일 기기를 위한

kenel.tistory.com

다시 이전 글로 돌아가 나머지 부분을 구현해나간다.