관리 메뉴

나만을 위한 블로그

[Android] Jetpack Navigation, Room DB, Flow 같이 사용하기 - 1 - 본문

Android

[Android] Jetpack Navigation, Room DB, Flow 같이 사용하기 - 1 -

참깨빵위에참깨빵_ 2022. 12. 12. 00:18
728x90
반응형

예전에 자바로 같은 내용의 포스팅을 쓴 적이 있다.

 

https://onlyfor-me-blog.tistory.com/290

 

[Android] Room DB 사용법

21.04.13 - 코드 테스트 후 정상 작동 확인, xml 코드 별 어떤 xml의 코드인지 기재 이번 포스팅에선 제트팩 라이브러리에 속하는 룸 DB를 사용한 간단한 CRUD 예제 코드를 기록할 것이다. 먼저 앱 수준

onlyfor-me-blog.tistory.com

 

그러나 1년 전 글이라서 최신화가 필요하다고 생각했다. 그리고 이제 코틀린을 위주로 안드로이드 앱을 만들고 코드랩을 보던 중 Room DB와 Flow를 같이 사용하는 것을 공부해서 기록하려고 포스팅하게 됐다. 참고한 코드랩은 아래 2개다.

 

https://developer.android.com/codelabs/basic-android-kotlin-training-persisting-data-room?hl=ko#0 

 

Room을 사용하여 데이터 유지  |  Android Developers

Android Kotlin 앱에서 Room을 사용하는 방법을 알아보세요. Room은 Android Jetpack의 일부인 지속성 데이터베이스 라이브러리로, SQLite 위에 있는 추상화 레이어입니다. Room은 데이터베이스를 설정하고 구

developer.android.com

 

https://developer.android.com/codelabs/basic-android-kotlin-training-update-data-room?hl=ko#0 

 

Room을 사용하여 데이터 읽기 및 업데이트  |  Android Developers

Room을 사용하여 Android Kotlin 앱에서 데이터를 읽고 업데이트하는 방법을 알아보세요. Room은 Android Jetpack의 일부인 데이터베이스 라이브러리로, 데이터베이스 설정 및 구성과 같은 여러 작업을 처

developer.android.com

 

먼저 이 코드랩은 기반 코드를 깃허브에 공개해 두는데 그걸 바탕으로 직접 이것저것 추가하면서 진행하는 방식이다. 양이 많기 때문에 코드랩과 같은 순서로 포스팅을 쓰려고 한다.

 

  • Room DB 사용 전 기반 코드 작성, 매니페스트 적용
  • Room DB를 사용하기 위한 뷰모델 작성
  • 아이템 추가 기능 구현 및 Database Inspector를 통한 DB 값 추가 확인
  • 리사이클러뷰 + Room DB 연동
  • 아이템 상세보기 기능 구현
  • 아이템 수정, 삭제 기능 구현

 

그리고 아래 링크로 이동해서 레포를 클론한 다음 그걸 바탕으로 진행하는 게 나을 것이다. strings.xml 같은 잡다한 설정파일이나 프래그먼트 만들고 제트팩 네비게이션 처리가 귀찮은데 그 수고를 덜 수 있어서 좋다.

 

https://github.com/google-developer-training/android-basics-kotlin-inventory-app/tree/starter?utm_source=developer.android.com&utm_medium=referral 

 

GitHub - google-developer-training/android-basics-kotlin-inventory-app: App demonstrates how to use Room to save, read, update,

App demonstrates how to use Room to save, read, update, and delete inventory items in a SQLite database. - GitHub - google-developer-training/android-basics-kotlin-inventory-app: App demonstrates ...

github.com

 

이제 Room DB를 사용하기 위한 data class부터 만든다. data 패키지를 만들고 그 안에 아래 클래스를 만든다.

 

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

@Entity(tableName = "item")
data class Item(
    @PrimaryKey(autoGenerate = true)
    val id: Int = 0,
    @ColumnInfo(name = "name")
    val itemName: String,
    @ColumnInfo(name = "price")
    val itemPrice: Double,
    @ColumnInfo(name = "quantity")
    val quantityInStock: Int
)

 

아래는 어노테이션과 PK에 대한 잡담이다.

 

더보기

@Entity는 DB에 테이블을 만들기 위해 사용하는 어노테이션이다. tableName으로 테이블명을 정해준 다음, 생성자로 속성들을 정의하고 값을 넣으면 나중에 DB에 값을 넣을 때 여기서 정의한 속성 개수만큼 컬럼이 만들어지고 데이터들이 입력된다.

이 때 각 값들을 구분할 수 있는 기준으로 사용할 고유한 값인 기본키(Primary Key, 이하 PK)가 필요하다. 갑자기 기본키라는 게 나왔는데 이게 왜 필요한 것인가?

 

해외여행 가려고 호텔의 어느 마음에 드는 방을 예약했는데, 이 때 내가 받은 고객 번호가 111이라고 가정한다. 호텔에 도착해서 방 열쇠를 받으려면 고객 번호를 알려줘야 한다고 직원이 말했는데, 막상 도착하니 나와 같은 고객 번호를 받은 손님이 내가 찜했던 방을 쓰고 있다면? 또는 그 반대의 상황이 발생했다면 어떤가?

이러한 경우를 막기 위해 111이라는 숫자는 아래와 같은 특징을 가져야 한다.

 

  • 유일해야 한다
  • null이면 안 된다
  • 바뀌면 안 된다

 

최소한 이 3가지 조건은 만족해야 위의 예시에서 든 불상사는 피할 수 있을 것이다. 그리고 저 특징들이 기본키로서 성립되기 위한 조건이다.

다시 코드로 돌아와서 어떤 값이 PK로 쓰일 수 있을까? 아이템의 이름, 가격, 재고는 위의 3가지 조건을 모두 만족하지 않는다. 만족하는 것은 id 뿐이기 때문에 Room DB가 제공하는 어노테이션 중 하나인 @PrimaryKey를 id 위에 명시한다.

그리고 테이블에 아이템이 새로 추가될 때마다 자동으로 PK를 붙여줄 수 있도록 autoGenerate를 true로 설정한다. id 외의 다른 값들은 id에 딸린 컬럼들이 될 것이기 때문에 @ColumnInfo를 붙이고, DB에 어떤 이름으로 저장될지 정한다. 여기서 정한 DB상의 이름은 나중에 Room DB 조작 함수를 다룰 때 필요하기 때문에 적절하게 붙여준다.

 


 

이제 Room DB 안의 데이터들을 조작할 함수를 만들어준다. 함수는 DAO(Data Access Object)라고 불리는 인터페이스에 만든다.

 

import androidx.room.*
import kotlinx.coroutines.flow.Flow

@Dao
interface ItemDao {
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun insert(item: Item)

    @Update
    suspend fun update(item: Item)

    @Delete
    suspend fun delete(item: Item)

    @Query("SELECT * FROM item WHERE id = :id")
    fun getItem(id: Int): Flow<Item>

    @Query("SELECT * FROM item ORDER BY name ASC")
    fun getItems(): Flow<List<Item>>
}

 

아래는 DAO에 대한 잡담이다.

 

더보기

interface 키워드 위에 @Dao를 붙여서 이 인터페이스가 DAO라는 걸 컴파일러에게 알려주고 추상 함수를 작성할 때 @Insert, @Update, @Delete 같은 Room이 제공하는 어노테이션을 사용해서 함수를 정의한다.

직접 쿼리를 작성하고 싶다면 @Query를 쓰고 그 안에 문자열로 쿼리를 입력하면 된다. WHERE를 쓸 경우 =의 오른쪽에는 변수 앞에 콜론을 붙인다. 직접 쿼리를 입력한 getItem()의 경우 매개변수로 int값 하나를 받는데 이것을 쿼리 문자열 안에 넣으려면 변수 앞에 콜론을 붙여야 한다.

 


 

엔티티와 DAO를 만들었으니 이제 이걸 사용하는 Database 객체를 만들어야 한다. Room은 그냥 쓰는 게 아니라 Room DB 인스턴스를 싱글톤하게 만들어서 전역 Application에 등록해야만 값을 등록, 수정, 삭제하는 작업을 할 수 있게 된다.

 

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

@Database(entities = [Item::class], version = 1, exportSchema = false)
abstract class ItemRoomDatabase: RoomDatabase() {
    abstract fun itemDao(): ItemDao

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

        fun getDatabase(context: Context): ItemRoomDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    ItemRoomDatabase::class.java,
                    "item_database"
                )
                    .fallbackToDestructiveMigration()
                    .build()
                return instance
            }
        }
    }
}

 

아래는 @Database와 소스코드에 대한 잡담이다.

 

더보기

@Database를 써서 해당 추상 클래스가 Database라는 걸 알려주고 소괄호를 열어서 그 안에 필요한 정보들을 넣어준다.

엔티티는 @Entity를 정의한 Item 클래스를 넣어주고, version과 exportSchema는 적절하게 넣어준다.

 

데이터베이스는 DAO를 알아야 하기 때문에 클래스 본문에서 ItemDao를 리턴하는 추상 함수를 만든다. 이걸 통해 액티비티, 프래그먼트에서 뷰모델을 통해 DAO 안의 함수들을 사용할 수 있게 된다.

그리고 companion object 안에 @Volatile을 명시한 변수를 만든다. 이걸 씀으로써 INSTANCE 변수는 항상 최신값을 유지할 수 있고, 값이 바뀌더라도 모든 쓰레드에 즉시 알려진다.

 

ItemRoomDatabase 추상 클래스를 리턴하는 getDatabse()는 INSTANCE에 값이 있다면 그걸 그대로 리턴하지만, null일 경우 synchronized() {} 안에서 초기화한 다음 리턴하게 한다. 이렇게 하면 한 번에 1개의 실행 쓰레드만 Room DB에 접근해 초기화할 수 있어 쓰레드의 경합 상태로 에러 발생이나 2개의 DB가 생성되는 걸 막을 수 있다.

 


 

이제 Application을 상속하는 클래스를 만들어 매니페스트에 등록해야 한다. Application 클래스는 앱 실행 시 가장 먼저 초기화되는 클래스기 때문에 Room DB를 여기서 초기화할 수 있다.

대신 Room DB를 사용하는 것은 몇 가지 화면을 이동해서인 경우가 있을 수 있기 때문에 lazy 위임을 사용해서 참조가 처음 필요할 때(처음 참조에 접근할 때) 초기화해서 Room DB를 사용할 때 DB가 초기화되도록 한다.

 

import android.app.Application
import com.example.kotlinprac.room.data.ItemRoomDatabase

class InventoryApplication: Application() {
    val database: ItemRoomDatabase by lazy {
        ItemRoomDatabase.getDatabase(this)
    }
}

 

이렇게 만든 Application은 매니페스트에 아래와 같이 등록한다.

 

<application
    android:name=".room.InventoryApplication"

 

이제 나중에 액티비티, 프래그먼트에서 Room DB 조작 함수를 사용할 때 뷰모델을 통해 사용할 건데, 그 때 DAO 인터페이스의 함수를 사용할 수 있게 된다.

반응형
Comments