Android

[Room + Coroutine] 리스트 항목 DB 연동 - 0

범데이 2020. 11. 30. 00:10

 

이 포스팅은 영어로 된 강좌 영상 을 직접 번역하여 재구성하였음을 알려드립니다.

오역이 있거나, 내용중 올바르지 않은 부분의 지적은 감사히 받겠습니다.

 

 


개요

Kotlin과 함께하는 Room database 강좌를 시작하도록 하겠습니다.

 

이 강좌는 5개의 챕터로 구성되어있고,

 

1챕터는 우리 프로젝트를 위한 데이터베이스 스키마를 만들기로 합니다.

혹시 이전에 SQLite를 다뤄보셨다면, 하나의 데이터베이스를 만들고 관리하는것은

정말 어렵다는걸 알 수 있습니다.

 

 


Room 라이브러리의 특징

그러나 room 라이브러리는 모든면에서  더 쉽고,

Room 라이브러리는 SQLite Helper class 위에 지어집니다. 

그리고 주된 이점은, Room 라이브러리는 SQL 쿼리들을 컴파일 시간에 검증하고,

엔티티 어노테이션은 컴파일 타임에 검증을 하여,

우리의 어플리케이션이 런타임 환경에서 박살나는것을 방지해 줍니다.

 

또한 기존 컴파일 타임 테스트 뿐만 아니라, 누락된 테이블도 확인합니다.

다음 이점은 상용구 코드를 줄여 live data와 같은 아키텍쳐 컴포넌트와의 통합을 쉽게 할 수 있습니다.

 

 

Room의 3가지 메인 컴포넌트:

@Entity

데이터베이스 내의 테이블을 나타냅니다.

Room은 @Entity 어노테이션이 있는 각 클래스에 대한 테이블을 작성하며

클래스의 필드는 테이블의 열에 해당합니다.

따라서 엔티티 클래스는 논리를 포함하지 않는 작은 모델 클래스인 경향이 있습니다.

 

@Dao (Data Access Object)

DAO는 데이터베이스에 액세스하는 방법을 정의합니다. 이것은 우리가 쿼리를 작성하는 곳입니다.

(초기 SQLite에서는 cursor object를 사용했지만,

Room은 그것을 필요로 하지 않습니다.

데이터 클래스에서 어노테이션을 사용하여 쿼리들을 간단하게 정의합니다)

 

@Database

데이터베이스 홀더를 포함하고 앱 데이터에 대한 기본 연결을위한 기본 액세스 포인트 역할을합니다.

 

 

 

 


앱 미리보기

이제 우리의 어플리케이션이 어떻게 생겼는지 보여드리겠습니다.

이는 3가지의 프래그먼트를 가지고 있습니다.

 

첫번째 프래그먼트는 모든 데이터를 리스트 형식으로 보여줍니다.

두 번째는 아이템을 데이터베이스에 추가하기 위한 프래그먼트 입니다.

 

보시는 바와 같이 이곳에서 아이템을 추가하여 리스트에서 출력되는것을 볼 수 있습니다.

 

 

 

그리고 그 아이템들중 하나를 클릭하면 세번째 프래그먼트(업데이트 프래그먼트)로 이동할 수 있습니다.

이곳에서는 아이템을 업데이트를 하거나, 데이터베이스에서 해당 아이템을 지울 수 있습니다.

 

 

 

다음으로, James의 나이를 30에서 44로 변경해보겠습니다.

 

 

그러면 리스트에서 이와 같이 데이터가 변경된 것을 확인할 수 있습니다.

 

 

리스트에서 데이터베이스 내 아이템을 지우거나, 

 

 

모든 아이템을 지우는 기능도 할 수 있습니다.

이와같이 앱 기능을 간단히 소개해보았습니다.

 

 


실습

코드를 작성하기에 앞서, 프로젝트에 필요한 의존성을 추가해야합니다.

 

[build.gradle] (Module)

 

Room의 navigation 컴포넌트

    // Navigation Component
    implementation 'androidx.navigation:navigation-fragment-ktx:2.2.2'
    implementation 'androidx.navigation:navigation-ui-ktx:2.2.2'

    // Room components
    implementation "androidx.room:room-runtime:2.2.5"
    kapt "androidx.room:room-compiler:2.2.5"
    implementation "androidx.room:room-ktx:2.2.5"
    androidTestImplementation "androidx.room:room-testing:2.2.5"

Viewmodel이 있는 lifecycle 컴포넌트

    // Lifecycle components
    implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
    implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0"
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"

Coroutines를 위한 컴포넌트

    // Kotlin components
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.72"
    api "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.5"
    api "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.5"

이를 추가해주시고,

 

컴파일 옵션, 코틀린옵션을 추가해주는것을 잊지 마세요.

Java version 1.8입니다.

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    
    kotlinOptions {
        jvmTarget = JavaVersion.VERSION_1_8.toString()
    }

 

또한 상단의 플로그인들을 체크해주세요. 정확히 이를 적용하여 주세요.

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: "androidx.navigation.safeargs.kotlin"

 

또한 minimum SDK버전은 26이 되어야 합니다.

        minSdkVersion 26

 

[build.gradle] (Project)

다른 Gradle 파일입니다. 아래와 같은 navigation safe args 플러그인도 추가해야합니다.

    dependencies {
    	...
        classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.0-rc01"
        ...
    }

 

자 , 이제 프로젝트에 필요한 의존성을 모두 설정해주었습니다.

이제 우리는 데이터베이스 스키마를 만들 수 있습니다.

 

 

처음으로, 패키지를 만들 겁니다.

이 패키지의 이름을 “data”로 지어줍니다.

 

 

그 패키지 안에 새로운 kotlin 클래스를 생성해줍니다.

클래스의 이름은 “User”로 지어줍니다.


[User.kt] ( Entity - 데이터베이스 내의 테이블을 나타냅니다.

package com.example.roomapp.data

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

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

이 클래스를 data class로 만들어 줍니다.

 

그 후, 4가지의 필드들을 추가해 줍니다.

 

이 클래스 위에 Entity 어노테이션을 추가해 줍니다.

그 후 테이블 이름("user_table")을 명세해 줍니다.

 

“id” 위에 @PrimaryKey 어노테이션을 추가해 준 후,

autoGenerate옵션을 true로 줍니다.

(이 뜻은 room 라이브러리가 이 ID 열에 자동으로 숫자를 생성해준다는 뜻입니다.)

 

이렇게해서 entity 클래스 작성이 끝났습니다.

 

 


다음으로 “DAO” 를 작성해줄겁니다.

[UserDao.kt]

( Data Access Object- 데이터베이스에 액세스하는 데 사용되는 메서드를 포함합니다.

 

package com.example.roomapp.data

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

@Dao
interface UserDao {

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun addUser(user: User)

    @Query("SELECT * FROM user_table ORDER BY id ASC")
    fun readAllData(): LiveData<List<User>>

}

인터페이스로 생성해줍니다. 왜냐하면 dao는 인터페이스가 되어,

userDao로 명명해 줄 것이기 때문입니다.

 

위에 미리 DAO의 개념에 대해 설명을 했지만,

기본적으로 이는 데이터베이스에 접근하기위한 모든 메서드를 포함합니다.

 

따라서 이 interface 내에 데이터베이스내에서 실행할 모든 필요 쿼리들을 작성할 것입니다.

 

첫째로, 이 인터페이스 위에 @Dao 어노테이션을 추가해 줍니다.

 

그 후 이 인터페이스 내부에 "addUser" 라는 함수를 추가해 줍니다.

파라미터로 "user" Entity를 넘겨줄 것입니다.

 

이 함수 상단에 @Insert 어노테이션을 추가해줍니다.

그리고 conflict strategy에 대해 명세해 줍니다. 이번같은경우에는 ignore로 설정해 줄게요.

(이 의미는 동일한 user 데이터를 insert할 시 이를 그냥 무시한다는 뜻입니다.)

 

그 후 , 함수 좌측에 suspend 키워드를 삽입해줍니다.

왜냐하면 나중에 튜토리얼에 koroutines을 사용하기 위해서 입니다.

 

그 뒤, "readAllData" 라는 함수를 추가로 생성해 줍니다. 

이는 user의 리스트 타입이 되겠습니다. (우리는 이미 “user" model을 생성해주었었습니다.)

그 뒤 이를 livedata object로 감싸줍니다.

 

이또한 @Query어노테이션을 붙여줍니다. 이 안에 custom 쿼리를 작성해줍니다.

 

위와 같이 user_table의 모든 아이템을 select하는 쿼리를 작성해줍니다.

(정렬은 id를 기준으로 오름차순으로 해줍니다.)

 

이는 livedata로 감싸진 user의 리스트 형식으로 리턴하게 합니다.

아직 이 개념에 대해 헷갈리더라도, 걱정하지마세요

이를 계속 따라온다면, 튜토리얼의 나중 부분에서 다 이해하게 될 것입니다.

자, 이제 데이터를 읽고, 삽입하기 위한 두개의 쿼리를 작성했습니다.

 

 


다음으로, 데이터베이스를 생성해 줄 겁니다.

[UserDatabase.kt]

( Database - 데이터베이스 홀더를 포함하고 기본 역할을합니다.

앱의 지속되는 관계형 데이터에 대한 기본 연결에 대한 액세스 포인트. )

 

data 패키지 안에 user database라는 클래스를 생성해줍니다.

user database 클래스는 Room 라이브러리의 데이터베이스를 대표하여 보여줍니다.

 

package com.example.roomapp.data

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

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

    abstract fun userDao(): UserDao

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

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

}

 

처음으로, 클래스 앞에 abstract키워드를 붙여줍니다.

또한 RoomDatabase를 extend해줍니다.

 

클래스의 상단에는 @Database라는 어노테이션을 붙여줍니다.

 

어노테이션 내부에는 엔티티들을 명세해줍니다.

우리는 한개의 엔티티 (user class)를 사용할 예정이니, 이를 적어주고,

 

두번째 파라미터에는 데이터베이스 버전 "1"을 적어주고,  "exportSchema"설정을 false로 해줍니다. 

( 이 설정은 기본적으로 true로, 코드베이스에 스키마의 버전 기록을 갖는 것이 좋은 습관임을 알려줍니다.

하지만 지금은 필요하지 않으니, false로 설정해줍니다. )

 

기본적으로 모든 키워드들에 관하여 문서 정보를 읽어볼 수 있고, Ctrl+Q를 눌러 더 많은 정보를 읽을 수 있습니다.

 

이제 클래스 내부에 user Dao 혹은 미리 생성한 data access object 를 반환하는 function과

abstract function를 생성해 줍니다.

 

이제 companion object를 생성해 줍니다.

(따라서 이 companion object 내에 있는 모든 것들은 기본적으로 다른 클래스에 보여집니다.)

 

그리고 싱글톤 user 데이터베이스 class를 만들고자 합니다.

즉, user 데이터베이스에는 그 클래스에 단 한개의 인스턴스만 사용하게 됩니다.

 

※꼭 기억하세요 ! Room database는 항상 같은 인스턴스만 사용합니다.

room database에서 여러 인스턴스를 갖는것은 성능에 굉장히 막대한 비용을 초래합니다.

 

다음으로 instance라는 volatile 변수를 만들고, 초기 값을 null로 설정해줍니다.

(volatile로 설정한다는 것은, 휘발성을 의미하므로

기본적으로 이 필드에 대한 권한이 다른 스레드에 즉시 표시됨을 의미합니다.)

 

아래에서 우리는 컨텍스트를 추가하려는 매개 변수로 데이터베이스 함수를 생성하고

User데이터베이스를 확장 할 것입니다.

 

그 후 이 안에 tempInstance라는 변수를 추가해주고, 인스턴스를 새로운 변수에 할당해줍니다.

그리고 이 블록에서는 이 tempinstance null 체크를 하여

이 과정은 tempinstance가 이미 존재하는지, 존재한다면 해당 instance를 반환하게 합니다.

 

그리고 synchronized 블록입니다. 우리는 인스턴스가 없을 시 새 인스턴스를 만듭니다.

synchronized 블록 - 이 블록 내에 있는 것은 멀티 스레드로, 현재 실행으로부터 보호됩니다.

 

그리고 기본적으로 Room 데이터베이스의 인스턴스를 생성합니다.

그래서 컨텍스트와 데이터베이스, 우리의 database 이름을 전달합니다.

그리고 새로운 인스턴스에 이 인스턴스를 할당해줍니다.

 

이와 같이 우리의 싱글톤 room 데이터베이스를 만드는 방법을 보았습니다.

 

 


다음으로 repository를 만들 겁니다.

[UserRepository.kt]

( Repository - 저장소 클래스는 여러 데이터 소스에 대한 액세스를 추상화합니다.

저장소는 아키텍처 구성 요소 라이브러리의 일부가 아닙니다.

그러나 코드 분리 및 아키텍처를위한 권장 모범 사례입니다.)

따라서 repository를 사용할 필요는 없지만 숙달하기 위해 항상 사용하는 것이 좋습니다.

 

 

data 패키지 내에 user repository라는 새로운 kotlin 클래스를 만들어줍니다.

package com.example.roomapp.data

import androidx.lifecycle.LiveData

class UserRepository(private val userDao: UserDao) {

    val readAllData: LiveData<List<User>> = userDao.readAllData()

    suspend fun addUser(user: User){
        userDao.addUser(user)
    }

}

사전에 만들었던 "UserDao" 파라미터를 추가해줍니다.

 

"readAllData" 라는 새 변수를 만들어줍니다.

그리고 이 변수는 User의 리스트 타입을 가집니다.

이 모든것을 livadata Object로 감싸줍니다.

 

기본적으로 "userDao" 로부터 데이터를 읽고자 합니다.

 

"AddUser"함수를 만들어줍니다. 파라미터로 추가하고자 하는 user를 넣을수 있게 해줍니다.

그리고 "userDao"에 있는 "addUser" 함수에 접근할 수 있게 해줍니다.

 

그리고 나중에 view Model에 있는 coroutine을 사용할 것이기 때문에,

이 addUser 앞에 "suspend" 키워드를 붙여줍니다.

 

 

 


[UserViewModel.kt]

(ViewModel - ViewModel의 역할은 UI에 데이터를 제공하고 구성 변경을 유지하는 것입니다.

ViewModel은 저장소와 UI 사이의 커뮤니케이션 센터 역할을합니다.)

 

이제 동일한 View Model을 만들어 줄 것입니다.

그리고 이 View Model에서 dao에 있는 모든 쿼리에 접근할 것입니다.

package com.example.roomapp.data

import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

class UserViewModel(application: Application): AndroidViewModel(application) {

    private val readAllData: LiveData<List<User>>
    private val repository: UserRepository

    init {
        val userDao = UserDatabase.getDatabase(application).userDao()
        repository = UserRepository(userDao)
        readAllData = repository.readAllData
    }

    fun addUser(user: User){
        viewModelScope.launch(Dispatchers.IO) {
            repository.addUser(user)
        }
    }

}

View Model에 대해 더 알고 싶다면, 이 영상 을 참고해 주시기 바랍니다.

 

이 클래스는 Android View Model을 확장 합니다.

Android View Model은 application reference를 포함한다는 점에서 일반적인 ViewModel과 다릅니다.

 

자 이제 "readAllData" 라는 변수를 만들어줍니다.

이 변수의 타입은 LiveData object로 감싸진 "User" 의 리스트 입니다.

 

그뒤, init block을 생성해줍니다.

(이 init block은 항상 UserViewModel이 호출 되었을때 가장 먼저 실행됩니다.)

 

따라서 userDao라는 변수를 생성해줍니다.

이는 "UserDatabase" 의 "getDatabase" 를 호출하고, 보이는 바와 같이 application을 파라미터로 전달해 주고,

"UserDatabase"의 "userDao"에 접근합니다.

 

다음으로 "repository" 변수를 만들고, init block 안에 repository를 초기화해줍니다.

그리고 "repository" 내에 한개의 파라미터로 이미 만들어진 "userDao"를 전달해주어야 합니다.

 

또한 우리는 "repository" 의 "readAllData" 를 호출할 수 있어야 하기에 이를 작성해 줍니다.

 

이제 아래에서 "user" 를 추가하기위해 하나의 함수를 더 만들어 줍니다.

 

지금은 "readAllData", "addUser "메소드만 가지고 있지만,

이 튜토리얼의 나중에는 더 함수를 추가해 줄 예정입니다.

 

보시다시피 coroutine의 일부인 viewModel 스코프를 사용하고 있습니다.

그리고 Dispatcher IO를 사용합니다.

(이는 이 코드를 백그라운드 스레드에서 실행시키고자 한다는 의미입니다.)

 

그래서 repository의 addUser 함수를 이 안에 작성하여, 이를 worker thread 혹은,

백그라운드 스레드에서 실행되게 합니다. 따라서 코루틴은 매우 유용하고, 

항상 데이터베이스를 메인 스레드에서 실행하는 것은 나쁜 습관이므로,

백그라운드 스레드 내에서 실행될 수 있게 해주어야 합니다.

 

 


1부 튜토리얼은 이와 같은 내용으로 준비해보았습니다!

우리는 데이터베이스 스키마를 생성하여서

Entity, Dao, Database, Repository, ViewModel을 생성해보게 되었습니다.

따라서 모든것이 준비되었고, 다음 강좌에서는 데이터베이스에 데이터를 삽입하는 것을 다뤄볼 예정입니다.

 

반응형

'Android' 카테고리의 다른 글

Android Studio, Thread.sleep()사용시 UI도 멈춘다.  (0) 2022.01.17