본문 바로가기

[Android/Kotlin] 안드로이드 Room Database 사용하기(1)

꿈꾸는블로그왕 2021. 1. 30.

안녕하세요.  Room Datebase를 사용하는 방법에 대해서 알아보도록 하겠습니다.

 

먼저 Room Database는 안드로이드에 내장된 SQLite를 통한 데이터베이스 사용에 도움을 주는 AAC 라이브러리입니다.

 

Room은 SQLite에 대한 추상화 레이어를 제공하여 데이터베이스 사용을 쉽게 해줍니다.

여기서 말하는 데이터베이스는 Local 데이터베이스로, 사용자의 폰에 저장하는 형태입니다.

간단한 데이터를 처리하는 경우 쉽고 빠르게 처리할 수 있는 장점이 있습니다.

 

Room은 아래와 같이 세가지 주요 구성요소가 있습니다. (공식문서)

 

1. Entity: 데이터베이스 내의 테이블을 나타냅니다. POJO클래스에 @Entity 어노테이션을 추가하여 생성합니다.

2. Dao: 데이터베이스에 액세스하는데 사용되는 메서드가 포함되어 있습니다. @Dao 어노테이션을 인터페이스에 사용합니다.

3. Database데이터베이스 홀더를 포함하며 앱의 지속적인 관계형 데이터의 기본 연결을 위한 기본 액세스 포인트 역할을 합니다. 데이터베이스 클래스를 만드는 조건은 아래 괕습니다.

  • RoomDatabase를 상속한 추상 클래스여야 합니다.
  • 데이터베이스에 포함될 Entity 목록을 포함해야 합니다.
  • 매개변수가 없으며며 @Dao 클래스를 반환하는 추상 메서드를 포함해야 합니다.

Room architecture diagram

 

Room을 이용한 간단한 예제를 만들어 봤습니다.

 

 

STEP01. build.gragle 추가하기

[build.gradle/app]

implementation "androidx.room:room-runtime:2.2.6"
kapt "androidx.room:room-compiler:2.2.6"
// Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:2.2.6"

// ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"

// Coroutines
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1'

 

STEP02. Entity 정의하기

기본 데이터 클래스에 @Entity 어노테이션을 붙이고, 필드를 선언하는 것으로 정의할 수 있습니다. 이 Entity 클래스는 데이터베이스 인스턴스가 생성될 때 테이블로 생성됩니다.

Entity 클래스는 반드시 한개의 기본키가 있어야 합니다. @PrimaryKey 어노테이션을 사용하여 해당 필드를 기본키로 만들어 줍니다. autoGenerate 속성을 사용하여 자동으로 값이 증가하게 해줍니다.

데이터베이스내 테이블 이름을 변경하려면 tableName 속성에 값을 지정해줍니다. 저는 여기에서 exepnse_table 이라고 지정했습니다. 테이블 내 필드이름을 변경하려면 @ColumnInfo 어노테이션의 name 속성값을 지정해줍니다.

 

[Expense.kt]

@Entity(
    tableName = "expense_table"
)
data class Expense(
    var amount: Int? = 0,
    var title: String? = "",
    var description: String? = "",
    @ColumnInfo(name = "reg_date")
    var regDate: Date = Date()
) {
    @PrimaryKey(autoGenerate = true)
    var id: Int? = null
}

STEP03. Dao 인터페이스 정의하기

Room Library를 사용하여 데이터베이스에 액세스 하려면 Dao를 이용해야 합니다. 따라서 Dao 내에는 데이터베이스에 대한 접근을 제공하는 추상메소드가 있어야합니다.

Room은 UI를 처리하는 메인 스레드에서 접근하는 것을 허용하지 않습니다. 따라서 Coroutine으로 비동기 처리를 위해 suspend를 사용했습니다.

 

[ExpenseDao.kt]

@Dao
interface ExpenseDao {

    @Query("SELECT * FROM expense_table")
    suspend fun getAllExpenses(): List<Expense>

    @Insert
    suspend fun insertExpense(expense: Expense)

    @Update
    suspend fun updateExpense(expense: Expense)

    @Delete
    suspend fun deleteExpense(expense: Expense)

}

 

STEP04. Database 클래스 정의하기

@Database 어노테이션이 붙은 RomDatabase을 상속한 추상클래스를 만들어줍니다. entities 속성에는 Entity 클래스를 입력해줍니다. version 속성은 데이터베이스 변경되면 변경되어야 합니다. @TypeConverters 어노테이션을 추가하여 Room 사용할 수 있도록 지정해줍니다.

getDatabase() 메소드를 통해서 ExpenseDatabase 인스턴스를 생성합니다.

 

[ExpenseDatabase.kt]

@Database(
    entities = [Expense::class],
    version = 1,
    exportSchema = false
)
@TypeConverters(Converters::class)
abstract class ExpenseDatabase : RoomDatabase() {

    abstract fun expenseDao(): ExpenseDao

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

        fun getDatabase(context: Context): ExpenseDatabase {
            val tempInstance = INSTANCE
            if (tempInstance != null) return tempInstance
            synchronized(this)  {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    ExpenseDatabase::class.java,
                    "expense_database"
                ).build()
                INSTANCE = instance
                return instance
            }
        }
    }

}

Room 데이터베이스에 Date 형식을 저장하기 위해서는 Long 타입으로 변환하는 타입 컨버터가 필요합니다.

컨버터가 없으면 아래와 같은 에러가 발생합니다.

그래서 아래와 같이 @TypeConverter 어노테이션이 붙은 Converters 클래스를 만들었습니다.

[Converters.kt]

class Converters {
    @TypeConverter
    fun fromTimestamp(value: Long?): Date? {
        return value?.let { Date(it) }
    }

    @TypeConverter
    fun dateToTimestamp(date: Date?): Long? {
        return date?.time
    }
}

STEP05. Repository 클래스 만들기

[ExpenseRepository.kt]

class ExpenseRepository(
    private val expenseDao: ExpenseDao
) {
    suspend fun getAllExpenses() = expenseDao.getAllExpenses()

    suspend fun insertExpense(expense: Expense) = expenseDao.insertExpense(expense)

    suspend fun updateExpense(expense: Expense) = expenseDao.insertExpense(expense)

    suspend fun deleteExpense(expense: Expense) = expenseDao.insertExpense(expense)
}

STEP06. ViewModel 클래스 만들기

[MainViewModel.kt]

class MainViewModel(
    application: Application
) : ViewModel() {

    private val repository: ExpenseRepository

    init {
        val expenseDao = ExpenseDatabase.getDatabase(application).expenseDao()
        repository = ExpenseRepository(expenseDao)
    }

    fun insertExpense(expense: Expense) =
        viewModelScope.launch(Dispatchers.IO) {
            repository.insertExpense(expense)
        }

    class Factory(
        private val application: Application
    ) : ViewModelProvider.Factory {
        override fun <T : ViewModel?> create(modelClass: Class<T>): T {
            return MainViewModel(application) as T
        }
    
    }
}

STEP07. 저장

[MainActivity.kt]

class MainActivity : AppCompatActivity() {

    private var _binding: ActivityMainBinding? = null
    private val binding get() = _binding!!

    private lateinit var mainViewModel: MainViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        _binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        mainViewModel = ViewModelProvider(this, MainViewModel.Factory(application)).get(MainViewModel::class.java)

        binding.btnSave.setOnClickListener {
            val expense = Expense().apply {
                amount = binding.edtAmount.text.toString().trim().toInt()
                title = binding.edtTitle.text.toString().trim()
                description = binding.edtDescription.text.toString().trim()
            }

            mainViewModel.insertExpense(expense)
        }
    }


    override fun onDestroy() {
        super.onDestroy()
        _binding = null
    }

}

 

[activity_main.kt]

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="16dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" >

        <EditText
            android:id="@+id/edt_amount"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="Amount"
            android:inputType="number" />

        <EditText
            android:id="@+id/edt_title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="Title"
            android:inputType="text" />

        <EditText
            android:id="@+id/edt_description"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="Description"
            android:inputType="text" />

        <Button
            android:id="@+id/btn_save"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="저장하기" />

    </LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

 

결과: Database Inspector를 통해 확인해 볼 수 있습니다. 데이터 가 잘 들어간 모습입니다.

댓글