Android-jetpack Compose

jetpack compose - dependency injection, Room 참고 자료-사이트

slow333 2023. 4. 2. 11:48

============== 참고 자료... ======================

https://dunchi.tistory.com/94

 

[Android] 안드로이드 Room으로 로컬 데이터베이스 이용하기

1. Room 이란? 안드로이드에서 데이터를 로컬에 저장하는 방법에는 File, SharedPreference, SQLite 등이 있다. 각각의 특징을 간략하게 알아보자면 File - 텍스트 파일 등의 파일을 생성하여 파일 입출력으

dunchi.tistory.com

안드로이드에서 데이터를 로컬에 저장하는 방법에는 File, SharedPreference, SQLite 등이 있다.

 

각각의 특징을 간략하게 알아보자면

 

File - 텍스트 파일 등의 파일을 생성하여 파일 입출력으로 관리한다.

SharedPreference - Key, Value 쌍으로 데이터를 관리한다.

SQLite - MySQL, PostgreSQL과 같은 DB 시스템이지만 응용 프로그램에서 사용하는 가벼운 DB이다.

 

 Room은 위의 SQLite와 관련이 있다..!

 

 Room은 SQLite에 대한 추상화 레이어를 제공한다고 공식 문서에 나와있다. 예를 들어, SQLite에서는 모든 Query를 직접 작성했어야 했는데 Room에서는 CRUD와 관련된 기본 추상화 메서드를 제공하여 사용할 수 있다. 이 외에도 DB를 사용하는데 필요한 기능들이 잘 구성되어 있기 때문에 SQLite를 사용할 때보다 간편히 사용할 수 있을 것 같다.

 

 또한 데이터가 변경되었을 시 LiveData, Flow 등을 이용해서 실시간으로 UI에 적용할 수도 있고 데이터 캐싱도 자동 지원해준다. 이런 장점들이 많아서 Room을 사용하는 것을 안드로이드 공식문서에서는 적극 권장하고 있다!

2. 사용방법

ORM과 비슷하다.

 1) Table과 매핑할 Entity

 2) DB에 엑세스해서 값을 가져올 Dao

 3) Dao를 관리해서 DB에 접근할 Database

3가지를 구현해주면 된다.

 

 1) Entity

@Entity(tableName = "place")
data class Place (
    @PrimaryKey
    @ColumnInfo(name = "place_id") val placeId: Int,
    @ColumnInfo(name = "place_name") val placeName: String
)

 @Entity 어노테이션으로 선언해준다. tableName 속성을 생략하면 클래스명이 기본 테이블 명이되고 바꾸고 싶다면 명시해주면 된다!

 @PrimaryKey로 기본키를 설정할 수 있으며, @ColumnInfo로 속성을 지정할 수 있다! ColumnInfo도 마찬가지로 name속성을 명기하여 속성명을 지정할 수 있으며 생략하면 변수명이 Default로 들어간다.

 이때, @Ignore를 사용하여 클래스에는 포함되나 Table과 매핑되지 않게 선언할 수도 있다.

 

 Room도 결국 SQLite라 RDB이다. 그런데 보통 ORM에서는 Entity가 서로를 참조할 수 있지만 Room에서는 명시적으로 금지하고 있다. 간략하게 이유를 설명하자면 객체를 참조하게 되면 성능상으로 쿼리를 하는데 오랜 시간이 걸리고 UI 반응속도가 느려지기 때문이다.

 

 그렇기 때문에 우리는 참조하는 형태가 아닌 관계를 정의하는 형태로 사용해야한다!

@Entity
data class Character (
    @PrimaryKey
    @ColumnInfo(name = "character_id") val characterId: Int,
    @ColumnInfo(name = "place_belong_id") val placeBelongId: Int,
    @ColumnInfo(name = "character_name") val characterName: String
)

data class PlaceWithCharacter(
    @Embedded val place: Place,
    @Relation(
        parentColumn = "placeId",
        entityColumn = "placeBelongId"
    )
    val characters: List<Character>
)

이렇게 @Relation을 통해 관계를 정의할 수 있고 이 경우는 place와 character가 1:다 매핑이다.

만약 아래 List<Character>를 Character로 지정해준다면 1:1 매핑이다.

 

 2) Dao

@Dao
interface PlaceDao {

    @Insert
    fun insertPlaces(vararg places: Place)
    
    @Update
    fun updatePlaces(vararg places: Place)
    
    @Delete
    fun deletePlaces(vararg places: Place)
    
    @Query("SELECT * FROM place")
    fun loadAllPlaces(): Array<Place>

    @Query("SELECT place_name FROM place WHERE place_id = :id")
    fun findPlaceWithID(id: Int): String
}

 @Dao 어노테이션으로 선언해준다. Insert, Update, Delete에 대해서 기본적으로 정의가 되어있어 간단하게 사용할 수 있다.

 

 Query 같은 경우는 @Query("SQL") 의 형태로 사용할 수 있으며 함수로 넘겨받은 매개변수를 쿼리에 사용하고 싶다면

":변수"로 변수 앞에 콜론(:)을 붙여서 사용할 수 있다.

 

 3) Database

@Database(entities = arrayOf(Place::class, Character::class), version = 2)
abstract class GameInfoDataBase: RoomDatabase(){
    abstract fun placeDao(): PlaceDao
    abstract fun characterDao(): CharacterDao

    companion object {
        private var INSTANCE: GameInfoDataBase? = null

        fun getInstance(context: Context): GameInfoDataBase? {
            if(INSTANCE == null){
                synchronized(GameInfoDataBase::class){
                    INSTANCE = Room.databaseBuilder(context.applicationContext, GameInfoDataBase::class.java, "nowGame.db")
                        .createFromAsset("game.db")
                        .build()
                }
            }
            return INSTANCE
        }
    }
}

Dao를 관리하고 DB를 매핑하는 클래스이다. DB접근을 한개의 객체로 하기 위해 싱글톤으로 관리하고자 하였다.

 

@Database 어노테이션으로 선언해주고 entities에 접근할 Entity(테이블)을 선언해준다. Version은 DB의 변경 사항을 관리하는 버전이다. 주로 Migration을 할 때 버전을 통해 변경사항을 참고한다.

 

 databaseBuilder와 build를 통해 Room DB를 생성하여 DB를 활용할 수 있다. 만약에 이미 SQLite DB파일 이있는 경우 createFromAsset 혹은 createFromFile을 통해 DB 내용을 가져올 수 있다.

 

 + ADD

미리 데이터를 정의하여 사용하고 싶을 때 위의 툴을 사용하는 것도 좋다. SQLite를 다룰 수 있는 툴이다.

 

================================

https://hydroponicglass.tistory.com/204

 

[Android, Kotlin] Room을 활용한 데이터베이스 구축

Room 오라클, mysql과 같은 데이터베이스를 사용하기 위해서는 외부 데이터베이스 구축과 웹서버가 필요하다. 다시 말해 인터넷이 연결되어있어야 한다. 그러나 로컬 데이터베이스인 SQlite를 활용

hydroponicglass.tistory.com

Room

오라클, mysql과 같은 데이터베이스를 사용하기 위해서는 외부 데이터베이스 구축과 웹서버가 필요하다. 다시 말해 인터넷이 연결되어있어야 한다. 그러나 로컬 데이터베이스인 SQlite를 활용하면 데이터베이스가 앱 내부에 구축되어서 인터넷이 연결되지 않아도 사용할 수 있는 이점이 있다. 그리고 안드로이드는 SQlite를 쉽게 사용할 수 있는 Room을 지원하고 있다.

 

MainThread에서 동작하지 않는 Room

안드로이드는 Room이 메인 스레드를 오래 점유할 것을 염려하여 메인 스레드에서의 Room 동작을 차단하고 있다. 따라서 이 글에서는 메인 스레드에서 Room동작을 허용하는 방법, Coroutine을 활용하는 방법 두 가지를 사용한다. 기본적인 Room 구현을 작성하기 위해 메인 스레드에서 Room동작을 허용하는 방법도 설명하지만 실제 구현은 코루틴과 같은 다른 방법을 사용하기를 권장한다.

코루틴에 대한 개념은 아래 글을 추천한다.

 

 

코틀린 코루틴(coroutine) 개념 익히기 · 쾌락코딩

코틀린 코루틴(coroutine) 개념 익히기 25 Aug 2019 | coroutine study 앞서 코루틴을 이해하기 위한 두 번의 발악이 있었지만, 이번에는 더 원론적인 코루틴에 대해서 알아보려 한다. 코루틴의 개념이 정확

wooooooak.github.io

 

메인 스레드에서 Room 동작을 허용하여 DB구축

이 글은 데이터베이스 구축에 필요한 필수사항만 작성되었다. 추가적인 내용은 아래 글을 참고한다.

https://developer.android.com/reference/android/arch/persistence/room/Entity?hl=en

https://developer.android.com/training/data-storage/room/defining-data?hl=ko

https://codelabs.developers.google.com/codelabs/android-room-with-a-view-kotlin/?hl=ko#0

 

build.gradle(:app)

Room을 사용하기 위해서 build.gradle를 수정한다.

상단에 아래 플러그인을 추가한다.

 

apply plugin: 'kotlin-kapt'

 

또한 dependencies에 항목을 추가한다.

 

dependencies {
    ....
    implementation "androidx.room:room-runtime:2.2.5"
    kapt "androidx.room:room-compiler:2.2.5"
}

 

Entity

아래 글들부터는 모두 MainActivity.kt에 작성되었다.

 

@Entity에서 테이블을 정의한다.

아래 엑셀과 같은 테이블을 만든다.

 

 

@Entity
data class NationalWeatherTable(
    @PrimaryKey val code: Int,
    val name1: String,
    val name2: String,
    val name3: String,
    val x: Int,
    val y: Int
)

 

클래스 이름이 테이블 이름이며 @PrimaryKey는 기본키를 나타낸다.

 

Dao

Dao는 데이터베이스에 접근하는 인터페이스이며 쿼리들을 정의할 수 있다.

각 쿼리 다음 줄에 작성된 함수로 쿼리를 사용한다.

 

@Query, @Insert, @Update, @Delete가 있다.

 

@Dao
interface NationalWeatherInterface {
    @Query("SELECT * FROM NationalWeatherTable")
    fun getAll(): List<NationalWeatherTable>

    @Insert
    fun insert(nationalWeatherTable: NationalWeatherTable)

    @Query("DELETE FROM NationalWeatherTable")
    fun deleteAll()
}

 

데이터베이스 정의

작성한 Entity, Dao를 이용하여 데이터베이스를 정의한다.

아래 코드는 단일 테이블인 경우이다. 여러 테이블이 필요할 경우 위에 소개한 글을 참고한다.

 

@Database(entities = [NationalWeatherTable::class], version = 1)
abstract class AppDatabase: RoomDatabase() {
    abstract fun nationalWeatherInterface(): NationalWeatherInterface
}

 

version이 바뀔 경우 데이터베이스의 내용이 초기화된다. 로컬 데이터베이스이기 때문에 앱을 초기화하여도 마찬가지다.

 

데이터베이스 빌드

정의한 데이터베이스를 생성한다.

이때 .allowMainThreadQueries()를 이용하여 메인 스레드에서 Room을 사용할 수 있게 허용해준다.

 

val NationalWeatherDB = Room.databaseBuilder(this, AppDatabase::class.java,"db").allowMainThreadQueries().build()

 

쿼리 사용과 로그 확인

 

// Query
val input = NationalWeatherTable(1114062500,"seoul", "jongrogu", "dasandong", 60, 126)
NationalWeatherDB.nationalWeatherInterface().deleteAll()
NationalWeatherDB.nationalWeatherInterface().insert(input)
// Log
var output = NationalWeatherDB.nationalWeatherInterface().getAll()[0]
Log.d("db_test", "$output")

 

// 로그캣 결과
D/db_test: NationalWeatherTable(name1=seoul, name2=jongrogu, name3=dasandong, x=60, y=126)

 

MainActivity.kt 전체 코드

 

@Entity
data class NationalWeatherTable(
    @PrimaryKey val code: Int,
    val name1: String,
    val name2: String,
    val name3: String,
    val x: Int,
    val y: Int
)

@Dao
interface NationalWeatherInterface {
    @Query("SELECT * FROM NationalWeatherTable")
    fun getAll(): List<NationalWeatherTable>

    @Insert
    fun insert(nationalWeatherTable: NationalWeatherTable)

    @Query("DELETE FROM NationalWeatherTable")
    fun deleteAll()

}

@Database(entities = [NationalWeatherTable::class], version = 1)
abstract class AppDatabase: RoomDatabase() {
    abstract fun nationalWeatherInterface(): NationalWeatherInterface
}

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val NationalWeatherDB = Room.databaseBuilder(this, AppDatabase::class.java,"db").allowMainThreadQueries().build()

        val input = NationalWeatherTable(1114062500,"seoul", "jongrogu", "dasandong", 60, 126)
        NationalWeatherDB.nationalWeatherInterface().deleteAll()
        NationalWeatherDB.nationalWeatherInterface().insert(input)
        var output = NationalWeatherDB.nationalWeatherInterface().getAll()[0]
        Log.d("db_test", "$output")
    }
}

 

코루틴을 사용한 DB 구축

위의 작성된 코드를 수정하는 방향으로 진행한다.

 

build.gradle(:app)

코루틴을 사용하기 위해서 모듈을 추가한다.

 

dependencies {
    ...
    implementation "androidx.room:room-runtime:2.2.5"
    kapt "androidx.room:room-compiler:2.2.5"
    implementation "androidx.room:room-ktx:2.2.5" // Added
}

 

Dao

작성했던 쿼리 함수에 suspend를 추가한다.

 

@Dao
interface NationalWeatherInterface {
    @Query("SELECT * FROM NationalWeatherTable")
    suspend fun getAll(): List<NationalWeatherTable>

    @Insert
    suspend fun insert(nationalWeatherTable: NationalWeatherTable)

    @Query("DELETE FROM NationalWeatherTable")
    suspend fun deleteAll()
}

 

데이터베이스 빌드

.allowMainThreadQueries()를 제거한다.

 

val NationalWeatherDB = Room.databaseBuilder(this, AppDatabase::class.java,"db").build()

 

쿼리 사용과 로그 확인

쿼리 함수들을 서브루틴에서 실행하기 위해 CoroutineScope(Dispatchers.IO).launch { } 내부로 이동시킨다.

Dispatchers.IO는 서브루틴을 IO스레드에서 실행시킨다.

 

CoroutineScope(Dispatchers.IO).launch {
    NationalWeatherDB.nationalWeatherInterface().deleteAll()
    NationalWeatherDB.nationalWeatherInterface().insert(input)
    var output = NationalWeatherDB.nationalWeatherInterface().getAll()[0]
    Log.d("db_test", "$output")
}

 

 

MainActivity.kt 전체 코드

 

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import android.util.Log
import androidx.room.*
import kotlinx.coroutines.*

@Entity
data class NationalWeatherTable(
    @PrimaryKey val code: Int,
    val name1: String,
    val name2: String,
    val name3: String,
    val x: Int,
    val y: Int
)

@Dao
interface NationalWeatherInterface {
    @Query("SELECT * FROM NationalWeatherTable")
    suspend fun getAll(): List<NationalWeatherTable>

    @Insert
    suspend fun insert(nationalWeatherTable: NationalWeatherTable)

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

}

@Database(entities = [NationalWeatherTable::class], version = 1)
abstract class AppDatabase: RoomDatabase() {
    abstract fun nationalWeatherInterface(): NationalWeatherInterface
}

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val NationalWeatherDB = Room.databaseBuilder(this, AppDatabase::class.java,"db").build()
        val input = NationalWeatherTable(1114062500,"seoul", "jongrogu", "dasandong", 60, 126)
        CoroutineScope(Dispatchers.IO).launch {
            NationalWeatherDB.nationalWeatherInterface().deleteAll()
            NationalWeatherDB.nationalWeatherInterface().insert(input)
            var output = NationalWeatherDB.nationalWeatherInterface().getAll()[0]
            Log.d("db_test", "$output")
        }
    }
}

 

참고 깃허브

https://github.com/HydroponicGlass/2021_Example_Android/tree/main/Room