1. 시작하기 전에
대부분의 프로덕션 품질 앱에는 앱에서 유지해야 하는 데이터가 있습니다. 예를 들어 앱은 노래 재생목록이나 할 일 목록의 항목, 수입 및 지출 기록, 별자리 카탈로그, 개인 정보 기록을 저장할 수 있습니다. 이러한 사용 사례에서는 이 영구 데이터를 저장하는 데 데이터베이스를 사용합니다.
Room은 Android Jetpack의 일부인 지속성 라이브러리입니다. Room은 SQLite 데이터베이스 위에 있는 추상화 레이어입니다. SQLite는 특수 언어(SQL)를 사용하여 데이터베이스 작업을 실행합니다. SQLite를 직접 사용하는 대신 Room은 데이터베이스 설정, 구성, 앱과의 상호작용과 같은 작업을 간소화합니다. Room은 SQLite 문의 컴파일 시간 확인도 제공합니다.
추상화 레이어는 기본 구현/복잡성을 숨기는 함수 집합입니다. 기존 기능 세트에 인터페이스를 제공합니다(여기서는 SQLite).
아래 이미지는 데이터 소스로서 Room이 이 과정에서 권장하는 전체 아키텍처에 얼마나 적합한지 보여줍니다. Room은 데이터 소스입니다.
기본 요건
- Jetpack Compose를 사용하여 Android 앱의 기본 사용자 인터페이스(UI)를 빌드하는 능력
Text
,Icon
,IconButton
,LazyColumn
과 같은 컴포저블을 사용하는 능력NavHost
컴포저블을 사용하여 앱에서 경로와 화면을 정의하는 능력NavHostController
를 사용하여 화면 간에 이동하는 능력- Android 아키텍처 구성요소
ViewModel
에 관한 지식.ViewModelProvider.Factory
를 사용하여 ViewModel을 인스턴스화하는 능력 - 동시 실행 기본사항에 관한 지식
- 장기 실행 작업에 코루틴을 사용하는 능력
- SQLite 데이터베이스 및 SQL 언어에 관한 기본 지식
학습할 내용
- Room 라이브러리를 사용하여 SQLite 데이터베이스를 만들고 상호작용하는 방법
- 항목, 데이터 액세스 객체(DAO), 데이터베이스 클래스를 만드는 방법
- DAO를 사용하여 Kotlin 함수를 SQL 쿼리에 매핑하는 방법
빌드할 항목
- SQLite 데이터베이스에 인벤토리 항목을 저장하는 Inventory 앱을 빌드합니다.
필요한 항목
- Inventory 앱의 시작 코드
- Android 스튜디오가 설치된 컴퓨터
- API 수준 26 이상을 실행하는 기기 또는 에뮬레이터
2. 앱 개요
이 Codelab에서는 Inventory 앱의 시작 코드로 작업하고 Room 라이브러리를 사용하여 데이터베이스 레이어를 앱에 추가합니다. 앱의 최종 버전에는 인벤토리 데이터베이스의 항목 목록이 표시됩니다. 사용자는 새 항목을 추가하고 기존 항목을 업데이트하며 인벤토리 데이터베이스에서 항목을 삭제할 수 있습니다. 이 Codelab에서는 항목 데이터를 Room 데이터베이스에 저장합니다. 다음 Codelab에서 앱의 나머지 기능을 완료합니다.
3. 시작 앱 개요
이 Codelab의 시작 코드 다운로드
시작하려면 시작 코드를 다운로드하세요.
GitHub 저장소를 클론하여 코드를 가져와도 됩니다.
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-inventory-app.git $ cd basic-android-kotlin-compose-training-inventory-app $ git checkout starter
Inventory app
GitHub 저장소에서 코드를 찾아볼 수 있습니다.
시작 코드 개요
- Android 스튜디오에서 시작 코드가 있는 프로젝트를 엽니다.
- Android 기기나 에뮬레이터에서 앱을 실행합니다. 에뮬레이터 또는 연결된 기기가 API 수준 26 이상을 사용하여 실행되는지 확인합니다. Database Inspector는 API 수준 26 이상을 실행하는 에뮬레이터/기기에서 작동합니다.
- 앱에 인벤토리 데이터가 표시되지 않습니다.
- 플로팅 작업 버튼(FAB)을 탭하면 데이터베이스에 새 항목을 추가할 수 있습니다.
앱이 새 화면으로 이동하고 여기서 새 항목의 세부정보를 입력할 수 있습니다.
시작 코드의 문제
- Add Item 화면에서 항목의 이름, 가격, 수량과 같은 항목의 세부정보를 입력합니다.
- Save를 탭합니다. Add Item 화면이 닫히지 않지만 뒤로 키를 사용하여 뒤로 이동할 수 있습니다. 저장 기능이 구현되지 않아 항목 세부정보가 저장되지 않습니다.
앱이 완성되지 않아 Save(저장) 버튼 기능이 구현되지 않았습니다.
이 Codelab에서는 Room을 사용하여 SQLite 데이터베이스에 인벤토리 세부정보를 저장하는 코드를 추가합니다. Room 지속성 라이브러리를 사용하여 SQLite 데이터베이스와 상호작용합니다.
코드 둘러보기
다운로드한 시작 코드에는 화면 레이아웃이 미리 디자인되어 있습니다. 이 개발자 과정에서는 데이터베이스 로직을 구현하는 데 중점을 둡니다. 다음 섹션에서는 시작하는 데 도움이 되는 몇 가지 파일을 간략히 살펴봅니다.
ui/home/HomeScreen.kt
이 파일은 홈 화면 또는 앱의 첫 번째 화면으로, 인벤토리 목록을 표시하는 컴포저블이 포함되어 있습니다. 목록에 새 항목을 추가하는 FAB 도 있습니다. 나중에 이 개발자 과정에서 목록의 항목을 표시합니다.
ui/item/ItemEntryScreen.kt
이 화면은 ItemEditScreen.kt
와 비슷합니다. 둘 다 항목 세부정보에 관한 텍스트 필드가 있습니다. 이 화면은 홈 화면에서 FAB를 탭하면 표시됩니다. ItemEntryViewModel.kt
는 이 화면에 상응하는 ViewModel
입니다.
ui/navigation/InventoryNavGraph.kt
이 파일은 전체 애플리케이션의 탐색 그래프입니다.
4. Room의 기본 구성요소
Kotlin은 데이터 클래스를 통해 데이터 작업을 쉽게 할 수 있는 방법을 제공합니다. 데이터 클래스를 사용하여 메모리 내 데이터로 쉽게 작업할 수 있지만 데이터 유지와 관련해서는 이 데이터를 데이터베이스 저장소와 호환되는 형식으로 변환해야 합니다. 이렇게 하려면 데이터를 저장할 테이블과 데이터에 액세스하고 데이터를 수정할 쿼리가 있어야 합니다.
다음과 같은 세 가지 Room 구성요소를 통해 이러한 워크플로가 원활해집니다.
- Room 항목은 앱 데이터베이스의 테이블을 나타냅니다. 이를 사용하여 테이블의 행에 저장된 데이터를 업데이트하고 삽입할 새 행을 만듭니다.
- Room DAO는 앱이 데이터베이스에서 데이터를 검색, 업데이트, 삽입, 삭제하는 데 사용하는 메서드를 제공합니다.
- Room Database 클래스는 앱에 해당 데이터베이스와 연결된 DAO 인스턴스를 제공하는 데이터베이스 클래스입니다.
Codelab 후반부에서 이러한 구성요소를 구현하며 자세히 알아봅니다. 다음 다이어그램은 Room 구성요소가 함께 작동하여 데이터베이스와 상호작용하는 방법을 보여줍니다.
Room 종속 항목 추가
이 작업에서는 필요한 Room 구성요소 라이브러리를 Gradle 파일에 추가합니다.
- 모듈 수준 gradle 파일
build.gradle.kts (Module: InventoryApp.app)
을 엽니다. dependencies
블록에서 다음 코드와 같이 Room 라이브러리의 종속 항목을 추가합니다.
//Room
implementation("androidx.room:room-runtime:${rootProject.extra["room_version"]}")
ksp("androidx.room:room-compiler:${rootProject.extra["room_version"]}")
implementation("androidx.room:room-ktx:${rootProject.extra["room_version"]}")
KSP는 Kotlin 주석을 파싱하는 강력하면서도 간단한 API입니다.
5. 항목 Entity 만들기
Entity 클래스는 테이블을 정의하고 이 클래스의 각 인스턴스는 데이터베이스 테이블의 행을 나타냅니다. 항목 클래스에는 데이터베이스의 정보를 표시하고 상호작용하는 방법을 Room에 알려주는 매핑이 있습니다. 앱에서 항목에는 항목 이름, 항목 가격, 사용 가능한 항목 수량 등 인벤토리 항목에 관한 정보가 포함됩니다.
@Entity
주석은 클래스를 데이터베이스 Entity 클래스로 표시합니다. 각 Entity 클래스에서 앱은 항목을 보관할 데이터베이스 테이블을 만듭니다. Entity의 각 필드는 달리 표시되지 않는 한 데이터베이스에서 열로 표시됩니다(자세한 내용은 Entity 문서 참고). 데이터베이스에 저장된 모든 항목 인스턴스에는 기본 키가 있어야 합니다. 기본 키는 데이터베이스 테이블의 모든 레코드/항목을 고유하게 식별하는 데 사용됩니다. 앱이 기본 키를 할당한 후에는 수정할 수 없습니다. 기본 키는 데이터베이스에 존재하는 한 항목 객체를 나타냅니다.
이 작업에서는 Entity 클래스를 만들고 기본 키를 저장할 Int
, 항목 이름을 저장할 String
, 항목 가격을 저장할 double
, 재고 수량을 저장할 Int
등 각 항목의 인벤토리 정보를 저장할 필드를 정의합니다.
- Android 스튜디오에서 시작 코드를 엽니다.
com.example.inventory
기본 패키지에서data
패키지를 엽니다.data
패키지 내에서 앱의 데이터베이스 항목을 나타내는Item
Kotlin 클래스를 엽니다.
// No need to copy over, this is part of the starter code
class Item(
val id: Int,
val name: String,
val price: Double,
val quantity: Int
)
Data 클래스
Data 클래스는 주로 Kotlin에서 데이터를 보유하는 데 사용됩니다. 키워드 data
로 정의됩니다. Kotlin 데이터 클래스 객체에는 추가 이점이 있습니다. 예를 들어 컴파일러는 toString()
, copy()
, equals()
와 같은 비교, 출력, 복사를 위한 유틸리티를 자동으로 생성합니다.
예:
// Example data class with 2 properties.
data class User(val firstName: String, val lastName: String){
}
생성된 코드의 일관성과 의미 있는 동작을 보장하기 위해 데이터 클래스는 다음 요구사항을 충족해야 합니다.
- 기본 생성자에는 매개변수가 하나 이상 있어야 합니다.
- 모든 기본 생성자 매개변수는
val
또는var
이어야 합니다. - 데이터 클래스는
abstract
나open
,sealed
일 수 없습니다.
Data 클래스에 관한 자세한 내용은 Data 클래스 문서를 참고하세요.
Item
클래스 정의 앞에data
키워드를 붙여 데이터 클래스로 변환합니다.
data class Item(
val id: Int,
val name: String,
val price: Double,
val quantity: Int
)
Item
클래스 선언 위에서 데이터 클래스에@Entity
주석을 답니다.tableName
인수를 사용하여items
을 SQLite 테이블 이름으로 설정합니다.
import androidx.room.Entity
@Entity(tableName = "items")
data class Item(
...
)
id
속성을@PrimaryKey
로 주석 처리하여id
를 기본 키로 설정합니다. 기본 키는Item
테이블의 모든 레코드/항목을 고유하게 식별하는 ID입니다.
import androidx.room.PrimaryKey
@Entity(tableName = "items")
data class Item(
@PrimaryKey
val id: Int,
...
)
id
에 기본값0
을 할당합니다. 이는id
가id
값을 자동으로 생성하는 데 필요합니다.autoGenerate
매개변수를@PrimaryKey
주석에 추가하여 기본 키 열을 자동 생성해야 하는지 지정합니다.autoGenerate
가true
로 설정되면 새 항목 인스턴스가 데이터베이스에 삽입될 때 Room에서 자동으로 기본 키 열의 고유한 값을 생성합니다. 이렇게 하면 기본 키 열에 값을 수동으로 할당할 필요 없이 각 항목 인스턴스가 고유 식별자를 갖게 됩니다.
data class Item(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
// ...
)
좋습니다. Entity 클래스를 만들었으므로 이제 데이터 액세스 객체(DAO)를 만들어 데이터베이스에 액세스할 수 있습니다.
6. 항목 DAO 만들기
데이터 액세스 객체(DAO)는 추상 인터페이스를 제공하여 지속성 레이어를 애플리케이션의 나머지 부분과 분리하는 데 사용할 수 있는 패턴입니다. 이러한 분리는 이전 Codelab에서 확인한 단일 책임 원칙을 따릅니다.
DAO의 기능은 애플리케이션의 나머지 부분과 별도로 기본 지속성 레이어에서 데이터베이스 작업 실행과 관련된 모든 복잡성을 숨기는 것입니다. 이를 통해 데이터를 사용하는 코드와 관계없이 데이터 레이어를 변경할 수 있습니다.
이 작업에서는 Room의 DAO를 정의합니다. DAO는 데이터베이스에 액세스하는 인터페이스를 정의하는 Room의 기본 구성요소입니다.
생성한 DAO는 데이터베이스 쿼리/검색, 삽입, 삭제, 업데이트를 위한 편의 메서드를 제공하는 맞춤 인터페이스입니다. Room은 컴파일 시간에 이 클래스의 구현을 생성합니다.
Room
라이브러리는 SQL 문을 작성하지 않고도 간단한 삽입, 삭제, 업데이트를 실행하는 메서드를 정의할 수 있도록 @Insert
, @Delete
, @Update
와 같은 편의성 주석을 제공합니다.
좀 더 복잡한 삽입, 삭제, 업데이트 작업을 정의해야 하거나 데이터베이스의 데이터를 쿼리해야 하는 경우에는 @Query
주석을 사용하세요.
또 다른 이점은 Android 스튜디오에서 쿼리를 작성할 때 컴파일러가 SQL 쿼리에 문법 오류가 있는지 확인한다는 것입니다.
Inventory 앱의 경우 다음 작업을 할 수 있는 기능이 필요합니다.
- 새 항목을 삽입하거나 추가합니다.
- 기존 항목을 업데이트하여 이름과 가격, 수량을 업데이트합니다.
- 기본 키인
id
에 기반하여 특정 항목을 가져옵니다. - 모든 항목을 가져와서 표시할 수 있습니다.
- 데이터베이스의 항목을 삭제합니다.
앱에서 항목 DAO를 구현하려면 다음 단계를 완료하세요.
data
패키지에서 Kotlin 인터페이스ItemDao.kt
를 만듭니다.
ItemDao
인터페이스에@Dao
주석을 답니다.
import androidx.room.Dao
@Dao
interface ItemDao {
}
- 인터페이스 본문에
@Insert
주석을 추가합니다. @Insert
아래에서Entity
클래스item
의 인스턴스를 인수로 사용하는insert()
함수를 추가합니다.- 함수를
suspend
키워드로 표시하여 별도의 스레드에서 실행되도록 합니다.
데이터베이스 작업을 실행하는 데는 시간이 오래 걸릴 수 있으므로 별도의 스레드에서 실행해야 합니다. Room은 기본 스레드에서 데이터베이스 액세스를 허용하지 않습니다.
import androidx.room.Insert
@Insert
suspend fun insert(item: Item)
데이터베이스에 항목을 삽입할 때 충돌이 발생할 수 있습니다. 예를 들어, 코드 여러 곳에서 다른 충돌하는 값(예: 같은 기본 키)을 사용하여 항목을 업데이트하려는 경우입니다. 항목은 DB의 행입니다. Inventory 앱에서는 Add Item 화면의 한 위치에만 항목을 삽입하므로 충돌이 예상되지 않으며 충돌 전략을 Ignore로 설정할 수 있습니다.
onConflict
인수를 추가하고OnConflictStrategy.
IGNORE
값을 할당합니다.
onConflict
인수는 충돌이 발생하는 경우 Room에 실행할 작업을 알려줍니다. OnConflictStrategy.
IGNORE
전략은 새 항목을 무시합니다.
사용 가능한 충돌 전략에 관한 자세한 내용은 OnConflictStrategy
문서를 참고하세요.
import androidx.room.OnConflictStrategy
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(item: Item)
이제 Room
이 item
을 데이터베이스에 삽입하는 데 필요한 모든 코드를 생성합니다. Room 주석으로 표시된 DAO 함수를 호출하면 Room은 데이터베이스에서 상응하는 SQL 쿼리를 실행합니다. 예를 들어 Kotlin 코드에서 위 메서드 insert()
를 호출하면 Room
은 SQL 쿼리를 실행하여 데이터베이스에 항목을 삽입합니다.
Item
을 매개변수로 사용하는@Update
주석이 있는 새 함수를 추가합니다.
업데이트된 항목에는 전달된 항목과 같은 기본 키가 있습니다. 항목의 다른 속성 일부나 전부를 업데이트할 수 있습니다.
insert()
메서드와 마찬가지로 이 함수를suspend
키워드로 표시합니다.
import androidx.room.Update
@Update
suspend fun update(item: Item)
@Delete
주석이 있는 다른 함수를 추가하여 항목을 삭제하고 정지 함수로 만듭니다.
import androidx.room.Delete
@Delete
suspend fun delete(item: Item)
나머지 기능에는 편의 주석이 없으므로 @Query
주석을 사용하여 SQLite 쿼리를 제공해야 합니다.
- 주어진
id
에 기반하여 항목 테이블에서 특정 항목을 검색하는 SQLite 쿼리를 작성합니다. 다음 코드는items
에서 모든 열을 선택하는 샘플 쿼리를 제공합니다. 여기서id
는 특정 값과 일치하므로id
는 고유 식별자입니다.
예:
// Example, no need to copy over
SELECT * from items WHERE id = 1
@Query
주석을 추가합니다.- 이전 단계의 SQLite 쿼리를
@Query
주석의 문자열 매개변수로 사용합니다. String
매개변수를 SQLite 쿼리인@Query
에 추가하여 항목 테이블에서 항목을 검색합니다.
이제 쿼리는 items
에서 모든 열을 선택하라고 지시합니다. 여기서 id
는 :id
인수와 일치합니다. :id
는 쿼리에서 콜론 표기법을 사용하여 함수의 인수를 참조합니다.
@Query("SELECT * from items WHERE id = :id")
@Query
주석 뒤에Int
인수를 사용하고Flow<Item>
을 반환하는getItem()
함수를 추가합니다.
import androidx.room.Query
import kotlinx.coroutines.flow.Flow
@Query("SELECT * from items WHERE id = :id")
fun getItem(id: Int): Flow<Item>
지속성 레이어에서 Flow
를 사용하는 것이 좋습니다. Flow
를 반환 유형으로 사용하면 데이터베이스의 데이터가 변경될 때마다 알림을 받게 됩니다. Room
은 이 Flow
를 자동으로 업데이트하므로 명시적으로 한 번만 데이터를 가져오면 됩니다. 이 설정은 다음 Codelab에서 구현하는 인벤토리 목록을 업데이트하는 데 유용합니다. Flow
반환 유형으로 인해 Room은 백그라운드 스레드에서도 쿼리를 실행합니다. 이를 명시적으로 suspend
함수로 만들고 코루틴 범위 내에서 호출할 필요는 없습니다.
getAllItems()
함수와 함께@Query
를 추가합니다.- SQLite 쿼리가
item
테이블의 모든 열을 오름차순으로 반환하도록 합니다. getAllItems()
가Item
항목의 목록을Flow
로 반환하도록 합니다.Room
은 이Flow
를 자동으로 업데이트하므로 명시적으로 한 번만 데이터를 가져오면 됩니다.
@Query("SELECT * from items ORDER BY name ASC")
fun getAllItems(): Flow<List<Item>>
ItemDao
완료됨:
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
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 items WHERE id = :id")
fun getItem(id: Int): Flow<Item>
@Query("SELECT * from items ORDER BY name ASC")
fun getAllItems(): Flow<List<Item>>
}
- 눈에 띄는 변경사항은 표시되지 않지만 앱을 빌드하여 오류가 없는지 확인합니다.
7. 데이터베이스 인스턴스 만들기
이 작업에서는 이전 작업의 Entity
및 DAO를 사용하는 RoomDatabase
를 만듭니다. 데이터베이스 클래스는 항목 및 DAO 목록을 정의합니다.
Database
클래스는 정의된 DAO의 인스턴스를 앱에 제공합니다. 결과적으로 앱은 DAO를 사용하여 데이터베이스의 데이터를 연결된 데이터 항목 객체의 인스턴스로 검색할 수 있습니다. 앱은 정의된 데이터 항목을 사용하여 상응하는 테이블의 행을 업데이트하거나 삽입할 새 행을 만들 수도 있습니다.
추상 RoomDatabase
클래스를 만들고 @Database
주석을 달아야 합니다. 이 클래스에는 데이터베이스가 없으면 RoomDatabase
의 기존 인스턴스를 반환하는 메서드가 하나 있습니다.
다음은 RoomDatabase
인스턴스를 가져오는 일반적인 프로세스입니다.
RoomDatabase
를 확장하는public abstract
클래스를 만듭니다. 정의한 새 추상 클래스는 데이터베이스 홀더 역할을 합니다. 정의한 클래스는 추상 클래스입니다.Room
이 구현을 만들기 때문입니다.- 클래스에
@Database
주석을 답니다. 인수에서 데이터베이스의 항목을 나열하고 버전 번호를 설정합니다. ItemDao
인스턴스를 반환하는 추상 메서드나 속성을 정의하면Room
에서 구현을 생성합니다.- 전체 앱에
RoomDatabase
인스턴스 하나만 있으면 되므로RoomDatabase
를 싱글톤으로 만듭니다. Room
의Room.databaseBuilder
를 사용하여 (item_database
) 데이터베이스를 만듭니다(없는 경우에만). 있다면 기존 데이터베이스를 반환합니다.
데이터베이스 만들기
data
패키지에서 Kotlin 클래스InventoryDatabase.kt
를 만듭니다.InventoryDatabase.kt
파일에서InventoryDatabase
클래스를RoomDatabase
를 확장하는abstract
클래스로 만듭니다.- 클래스에
@Database
주석을 답니다. 다음 단계에서 해결하므로 누락된 매개변수 오류는 무시합니다.
import androidx.room.Database
import androidx.room.RoomDatabase
@Database
abstract class InventoryDatabase : RoomDatabase() {}
@Database
주석에는 Room
이 데이터베이스를 빌드할 수 있도록 인수가 여러 개 필요합니다.
Item
을entities
목록이 있는 유일한 클래스로 지정합니다.version
을1
로 설정합니다. 데이터베이스 테이블의 스키마를 변경할 때마다 버전 번호를 높여야 합니다.- 스키마 버전 기록 백업을 유지하지 않도록
exportSchema
를false
로 설정합니다.
@Database(entities = [Item::class], version = 1, exportSchema = false)
- 데이터베이스가 DAO에 관해 알 수 있도록
ItemDao
를 반환하는 추상 함수를 클래스 본문 내에서 선언합니다.
abstract fun itemDao(): ItemDao
- 추상 함수 아래에서, 데이터베이스를 만들거나 가져오는 메서드에 대한 액세스를 허용하고 클래스 이름을 한정자로 사용하는
companion object
를 정의합니다.
companion object {}
companion
객체 내에서 데이터베이스에 관한 null을 허용하는 비공개 변수Instance
를 선언하고null
로 초기화합니다.
Instance
변수는 데이터베이스가 만들어지면 데이터베이스 참조를 유지합니다. 이를 통해 주어진 시점에 열린 데이터베이스의 단일 인스턴스를 유지할 수 있습니다. 데이터베이스는 만들고 유지하는 데 비용이 많이 듭니다.
Instance
에@Volatile
주석을 답니다.
휘발성 변수의 값은 캐시되지 않으며 모든 읽기와 쓰기는 기본 메모리에서 이루어집니다. 이러한 기능을 사용하면 Instance
값이 항상 최신 상태로 유지되고 모든 실행 스레드에 동일하게 유지됩니다. 즉, 한 스레드에서 Instance
를 변경하면 다른 모든 스레드에 즉시 표시됩니다.
@Volatile
private var Instance: InventoryDatabase? = null
Instance
아래 그리고 여전히companion
객체 내에서 데이터베이스 빌더에 필요한Context
매개변수를 사용하여getDatabase()
메서드를 정의합니다.InventoryDatabase
유형을 반환합니다.getDatabase()
에서 아직 아무것도 반환하지 않아 오류 메시지가 표시됩니다.
import android.content.Context
fun getDatabase(context: Context): InventoryDatabase {}
여러 스레드에서 동시에 데이터베이스 인스턴스를 요청할 수 있어 하나가 아닌 두 개의 데이터베이스가 생성됩니다. 이 문제를 경합 상태라고 합니다. 코드를 래핑하여 synchronized
블록 내에 데이터베이스를 가져오면 한 번에 한 실행 스레드만 이 코드 블록에 들어갈 수 있으므로 데이터베이스가 한 번만 초기화됩니다. 경합 상태를 방지하려면 synchronized{}
블록을 사용합니다.
getDatabase()
내에서Instance
변수를 반환하거나Instance
가 null이면synchronized{}
블록 내에서 초기화합니다. elvis 연산자(?:
)를 사용하면 됩니다.- 컴패니언 객체
this
를 전달합니다. 이 오류는 이후 단계에서 수정합니다.
return Instance ?: synchronized(this) { }
- 동기화된 블록 내에서 데이터베이스 빌더를 사용하여 데이터베이스를 가져옵니다. 다음 단계에서 해결하므로 오류는 계속 무시하세요.
import androidx.room.Room
Room.databaseBuilder()
synchronized
블록 내에서 데이터베이스 빌더를 사용하여 데이터베이스를 가져옵니다. 애플리케이션 컨텍스트, 데이터베이스 클래스, 데이터베이스 이름item_database
를Room.databaseBuilder()
에 전달합니다.
Room.databaseBuilder(context, InventoryDatabase::class.java, "item_database")
Android 스튜디오에서는 유형 불일치 오류가 발생합니다. 이 오류를 삭제하려면 다음 단계에서 build()
를 추가해야 합니다.
- 필요한 이전 전략을 빌더에 추가합니다.
.
fallbackToDestructiveMigration()
을 사용하세요.
.fallbackToDestructiveMigration()
- 데이터베이스 인스턴스를 만들려면
.build()
를 호출합니다. 이 호출을 통해 Android 스튜디오 오류가 삭제됩니다.
.build()
build()
뒤에also
블록을 추가하고Instance = it
을 할당하여 최근에 만들어진 db 인스턴스에 대한 참조를 유지합니다.
.also { Instance = it }
synchronized
블록 끝에instance
를 반환합니다. 최종 코드는 다음 코드와 같습니다.
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
/**
* Database class with a singleton Instance object.
*/
@Database(entities = [Item::class], version = 1, exportSchema = false)
abstract class InventoryDatabase : RoomDatabase() {
abstract fun itemDao(): ItemDao
companion object {
@Volatile
private var Instance: InventoryDatabase? = null
fun getDatabase(context: Context): InventoryDatabase {
// if the Instance is not null, return it, otherwise create a new database instance.
return Instance ?: synchronized(this) {
Room.databaseBuilder(context, InventoryDatabase::class.java, "item_database")
.build()
.also { Instance = it }
}
}
}
}
- 코드를 빌드하여 오류가 없는지 확인합니다.
8. 저장소 구현
이 작업에서는 ItemsRepository
인터페이스와 OfflineItemsRepository
클래스를 구현하여 데이터베이스에서 get
, insert
, delete
, update
항목을 제공합니다.
data
패키지에서ItemsRepository.kt
파일을 엽니다.- DAO 구현에 매핑되는 다음 함수를 인터페이스에 추가합니다.
import kotlinx.coroutines.flow.Flow
/**
* Repository that provides insert, update, delete, and retrieve of [Item] from a given data source.
*/
interface ItemsRepository {
/**
* Retrieve all the items from the the given data source.
*/
fun getAllItemsStream(): Flow<List<Item>>
/**
* Retrieve an item from the given data source that matches with the [id].
*/
fun getItemStream(id: Int): Flow<Item?>
/**
* Insert item in the data source
*/
suspend fun insertItem(item: Item)
/**
* Delete item from the data source
*/
suspend fun deleteItem(item: Item)
/**
* Update item in the data source
*/
suspend fun updateItem(item: Item)
}
data
패키지에서OfflineItemsRepository.kt
파일을 엽니다.ItemDao
유형의 생성자 매개변수를 전달합니다.
class OfflineItemsRepository(private val itemDao: ItemDao) : ItemsRepository
OfflineItemsRepository
클래스에서ItemsRepository
인터페이스에 정의된 함수를 재정의하고ItemDao
에서 상응하는 함수를 호출합니다.
import kotlinx.coroutines.flow.Flow
class OfflineItemsRepository(private val itemDao: ItemDao) : ItemsRepository {
override fun getAllItemsStream(): Flow<List<Item>> = itemDao.getAllItems()
override fun getItemStream(id: Int): Flow<Item?> = itemDao.getItem(id)
override suspend fun insertItem(item: Item) = itemDao.insert(item)
override suspend fun deleteItem(item: Item) = itemDao.delete(item)
override suspend fun updateItem(item: Item) = itemDao.update(item)
}
AppContainer 클래스 구현
이 작업에서는 데이터베이스를 인스턴스화하고 DAO 인스턴스를 OfflineItemsRepository
클래스에 전달합니다.
data
패키지에서AppContainer.kt
파일을 엽니다.ItemDao()
인스턴스를OfflineItemsRepository
생성자에 전달합니다.- 컨텍스트를 전달하는
InventoryDatabase
클래스에서getDatabase()
를 호출하여 데이터베이스 인스턴스를 인스턴스화하고.itemDao()
를 호출하여Dao
인스턴스를 만듭니다.
override val itemsRepository: ItemsRepository by lazy {
OfflineItemsRepository(InventoryDatabase.getDatabase(context).itemDao())
}
이제 Room을 사용하기 위한 모든 구성요소가 준비되었습니다. 이 코드는 컴파일되고 실행되지만 실제로 작동하는지는 알 수 없습니다. 따라서 지금 데이터베이스를 테스트하는 것이 좋습니다. 테스트를 완료하려면 ViewModel
이 데이터베이스와 통신해야 합니다.
9. 저장 기능 추가
지금까지 데이터베이스를 만들었고 UI 클래스는 시작 코드의 일부였습니다. 앱의 일시적인 데이터를 저장하고 데이터베이스에도 액세스하려면 ViewModel
을 업데이트해야 합니다. ViewModel
은 DAO를 통해 데이터베이스와 상호작용하여 UI에 데이터를 제공합니다. 모든 데이터베이스 작업은 기본 UI 스레드에서 벗어나 실행되어야 합니다. 코루틴과 viewModelScope
를 사용하면 됩니다.
UI 상태 클래스 둘러보기
ui/item/ItemEntryViewModel.kt
파일을 엽니다. ItemUiState
데이터 클래스는 항목의 UI 상태를 나타냅니다. ItemDetails
데이터 클래스는 단일 항목을 나타냅니다.
시작 코드는 다음과 같은 세 가지 확장 함수를 제공합니다.
ItemDetails.toItem()
확장 함수.ItemUiState
UI 상태 객체를Item
항목 유형으로 변환합니다.Item.toItemUiState()
확장 함수.Item
Room 항목 객체를ItemUiState
UI 상태 유형으로 변환합니다.Item.toItemDetails()
확장 함수.Item
Room 항목 객체를ItemDetails
로 변환합니다.
// No need to copy, this is part of starter code
/**
* Represents Ui State for an Item.
*/
data class ItemUiState(
val itemDetails: ItemDetails = ItemDetails(),
val isEntryValid: Boolean = false
)
data class ItemDetails(
val id: Int = 0,
val name: String = "",
val price: String = "",
val quantity: String = "",
)
/**
* Extension function to convert [ItemDetails] to [Item]. If the value of [ItemDetails.price] is
* not a valid [Double], then the price will be set to 0.0. Similarly if the value of
* [ItemDetails.quantity] is not a valid [Int], then the quantity will be set to 0
*/
fun ItemDetails.toItem(): Item = Item(
id = id,
name = name,
price = price.toDoubleOrNull() ?: 0.0,
quantity = quantity.toIntOrNull() ?: 0
)
fun Item.formatedPrice(): String {
return NumberFormat.getCurrencyInstance().format(price)
}
/**
* Extension function to convert [Item] to [ItemUiState]
*/
fun Item.toItemUiState(isEntryValid: Boolean = false): ItemUiState = ItemUiState(
itemDetails = this.toItemDetails(),
isEntryValid = isEntryValid
)
/**
* Extension function to convert [Item] to [ItemDetails]
*/
fun Item.toItemDetails(): ItemDetails = ItemDetails(
id = id,
name = name,
price = price.toString(),
quantity = quantity.toString()
)
뷰 모델에서 위의 클래스를 사용하여 UI를 읽고 업데이트합니다.
ItemEntry ViewModel 업데이트
이 작업에서는 저장소를 ItemEntryViewModel.kt
파일에 전달합니다. Add Item 화면에 입력한 항목 세부정보도 데이터베이스에 저장합니다.
ItemEntryViewModel
클래스의validateInput()
비공개 함수를 확인하세요.
// No need to copy over, this is part of starter code
private fun validateInput(uiState: ItemDetails = itemUiState.itemDetails): Boolean {
return with(uiState) {
name.isNotBlank() && price.isNotBlank() && quantity.isNotBlank()
}
}
위 함수는 name
, price
, quantity
가 비어 있는지 확인합니다. 이 함수를 사용하여 사용자 입력을 확인한 후 데이터베이스의 항목을 추가하거나 업데이트합니다.
ItemEntryViewModel
클래스를 열고ItemsRepository
유형의private
기본 생성자 매개변수를 추가합니다.
import com.example.inventory.data.ItemsRepository
class ItemEntryViewModel(private val itemsRepository: ItemsRepository) : ViewModel() {
}
ui/AppViewModelProvider.kt
에서 항목 뷰 모델의initializer
를 업데이트하고 저장소 인스턴스를 매개변수로 전달합니다.
object AppViewModelProvider {
val Factory = viewModelFactory {
// Other Initializers
// Initializer for ItemEntryViewModel
initializer {
ItemEntryViewModel(inventoryApplication().container.itemsRepository)
}
//...
}
}
ItemEntryViewModel.kt
파일로 이동하고ItemEntryViewModel
클래스 끝에saveItem()
이라는 정지 함수를 추가하여 Room 데이터베이스에 항목을 삽입합니다. 이 함수는 비차단 방식으로 데이터를 데이터베이스에 추가합니다.
suspend fun saveItem() {
}
- 함수 내에서
itemUiState
가 유효한지 확인하고Item
유형으로 변환하여 Room에서 데이터를 이해할 수 있도록 합니다. itemsRepository
에서insertItem()
을 호출하고 데이터를 전달합니다. UI는 이 함수를 호출하여 항목 세부정보를 데이터베이스에 추가합니다.
suspend fun saveItem() {
if (validateInput()) {
itemsRepository.insertItem(itemUiState.itemDetails.toItem())
}
}
항목을 데이터베이스에 추가하는 데 필요한 모든 함수를 추가했습니다. 다음 작업에서는 위 함수를 사용하도록 UI를 업데이트합니다.
ItemEntryBody() 컴포저블 둘러보기
ui/item/ItemEntryScreen.kt
파일에서ItemEntryBody()
컴포저블은 시작 코드의 일부로 부분적으로 구현됩니다.ItemEntryScreen()
함수 호출에서ItemEntryBody()
컴포저블을 살펴보세요.
// No need to copy over, part of the starter code
ItemEntryBody(
itemUiState = viewModel.itemUiState,
onItemValueChange = viewModel::updateUiState,
onSaveClick = { },
modifier = Modifier
.padding(innerPadding)
.verticalScroll(rememberScrollState())
.fillMaxWidth()
)
- UI 상태와
updateUiState
람다는 함수 매개변수로 전달됩니다. UI 상태가 업데이트되는 방식은 함수 정의에서 확인하세요.
// No need to copy over, part of the starter code
@Composable
fun ItemEntryBody(
itemUiState: ItemUiState,
onItemValueChange: (ItemUiState) -> Unit,
onSaveClick: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
// ...
) {
ItemInputForm(
itemDetails = itemUiState.itemDetails,
onValueChange = onItemValueChange,
modifier = Modifier.fillMaxWidth()
)
Button(
onClick = onSaveClick,
enabled = itemUiState.isEntryValid,
shape = MaterialTheme.shapes.small,
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(R.string.save_action))
}
}
}
이 컴포저블에서는 ItemInputForm
및 Save 버튼이 표시됩니다. ItemInputForm()
컴포저블에서는 텍스트 필드 3개를 표시합니다. Save는 텍스트 필드에 텍스트를 입력한 경우에만 사용 설정됩니다. isEntryValid
값은 모든 텍스트 필드의 텍스트가 비어 있지 않은 경우 true입니다.
- 구성 가능한
ItemInputForm()
함수 구현을 살펴보고onValueChange
함수 매개변수를 확인합니다.itemDetails
값을 사용자가 텍스트 필드에 입력한 값으로 업데이트합니다. Save 버튼이 사용 설정될 때까지itemUiState.itemDetails
에는 저장해야 하는 값이 있습니다.
// No need to copy over, part of the starter code
@Composable
fun ItemEntryBody(
//...
) {
Column(
// ...
) {
ItemInputForm(
itemDetails = itemUiState.itemDetails,
//...
)
//...
}
}
// No need to copy over, part of the starter code
@Composable
fun ItemInputForm(
itemDetails: ItemDetails,
modifier: Modifier = Modifier,
onValueChange: (ItemUiState) -> Unit = {},
enabled: Boolean = true
) {
Column(modifier = modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(16.dp)) {
OutlinedTextField(
value = itemUiState.name,
onValueChange = { onValueChange(itemDetails.copy(name = it)) },
//...
)
OutlinedTextField(
value = itemUiState.price,
onValueChange = { onValueChange(itemDetails.copy(price = it)) },
//...
)
OutlinedTextField(
value = itemUiState.quantity,
onValueChange = { onValueChange(itemDetails.copy(quantity = it)) },
//...
)
}
}
Save 버튼에 클릭 리스너 추가
모든 것을 연결하려면 클릭 핸들러를 Save 버튼에 추가합니다. 클릭 핸들러 내에서 코루틴을 실행하고 saveItem()
을 호출하여 Room 데이터베이스에 데이터를 저장합니다.
ItemEntryScreen.kt
의 구성 가능한ItemEntryScreen
함수 내에서, 구성 가능한rememberCoroutineScope()
함수를 사용하여coroutineScope
라는val
을 만듭니다.
import androidx.compose.runtime.rememberCoroutineScope
val coroutineScope = rememberCoroutineScope()
ItemEntryBody
()
함수 호출을 업데이트하고onSaveClick
람다 내에서 코루틴을 실행합니다.
ItemEntryBody(
// ...
onSaveClick = {
coroutineScope.launch {
}
},
modifier = modifier.padding(innerPadding)
)
ItemEntryViewModel.kt
파일에서saveItem()
함수 구현을 살펴보고itemUiState
가 유효한지 확인하여itemUiState
를Item
유형으로 변환하고itemsRepository.insertItem()
을 사용하여 데이터베이스에 삽입합니다.
// No need to copy over, you have already implemented this as part of the Room implementation
suspend fun saveItem() {
if (validateInput()) {
itemsRepository.insertItem(itemUiState.itemDetails.toItem())
}
}
ItemEntryScreen.kt
의 구성 가능한ItemEntryScreen
함수 내 코루틴에서viewModel.saveItem()
을 호출하여 항목을 데이터베이스에 저장합니다.
ItemEntryBody(
// ...
onSaveClick = {
coroutineScope.launch {
viewModel.saveItem()
}
},
//...
)
ItemEntryViewModel.kt
파일에서 saveItem()
에 viewModelScope.launch()
를 사용하지 않았지만 이는 저장소 메서드를 호출할 때 ItemEntryBody
()
에는 필요합니다. 코루틴이나 다른 정지 함수에서만 정지 함수를 호출할 수 있습니다. viewModel.saveItem()
함수는 정지 함수입니다.
- 앱을 빌드하고 실행합니다.
- + FAB를 탭합니다.
- Add Item 화면에서 항목 세부정보를 추가하고 Save를 탭합니다. Save 버튼을 탭해도 Add Item 화면이 닫히지 않습니다.
onSaveClick
람다에서viewModel.saveItem()
호출 뒤에navigateBack()
호출을 추가하여 이전 화면으로 다시 돌아갑니다.ItemEntryBody()
함수는 다음 코드와 같습니다.
ItemEntryBody(
itemUiState = viewModel.itemUiState,
onItemValueChange = viewModel::updateUiState,
onSaveClick = {
coroutineScope.launch {
viewModel.saveItem()
navigateBack()
}
},
modifier = modifier.padding(innerPadding)
)
- 앱을 다시 실행하고, 데이터를 입력하고 저장하는 동일한 단계를 실행합니다. 이번에는 앱이 Inventory 화면으로 다시 이동합니다.
이 작업으로 데이터가 저장되지만 앱에서 인벤토리 데이터를 확인할 수는 없습니다. 다음 작업에서는 저장한 데이터를 확인하는 데 Database Inspector를 사용합니다.
10. Database Inspector를 사용하여 데이터베이스 콘텐츠 보기
Database Inspector를 사용하면 앱 실행 중에 앱의 데이터베이스를 검사하고 쿼리하고 수정할 수 있습니다. 이 기능은 데이터베이스 디버깅에 특히 유용합니다. Database Inspector는 SQLite를 기반으로 빌드된 라이브러리(예: Room) 및 일반 SQLite를 사용한 작업을 지원합니다. Database Inspector는 API 수준 26을 실행하는 에뮬레이터/기기에서 가장 잘 작동합니다.
- API 수준 26 이상을 실행하는 에뮬레이터 또는 연결된 기기에서 앱을 실행합니다(아직 실행하지 않은 경우).
- Android 스튜디오의 메뉴 바에서 View > Tool Windows > App Inspection을 선택합니다.
- Database Inspector 탭을 선택합니다.
- Database Inspector 창의 드롭다운 메뉴에서
com.example.inventory
를 선택합니다(아직 선택하지 않은 경우). Inventory 앱의 item_database가 Databases 창에 표시됩니다.
- Databases 창에서 item_database 노드를 펼치고 검사할 Item을 선택합니다. Databases 창이 비어 있으면 에뮬레이터에서 Add Item 화면을 사용하여 항목을 데이터베이스에 추가합니다.
- Database Inspector에서 Live updates 체크박스를 선택하여 에뮬레이터나 기기에서 실행 중인 앱과 상호작용할 때 표시되는 데이터를 자동으로 업데이트합니다.
축하합니다 Room을 사용하여 데이터를 유지할 수 있는 앱을 만들었습니다. 다음 Codelab에서는 lazyColumn
을 앱에 추가하여 데이터베이스의 항목을 표시하고 새로운 기능(예: 항목을 삭제하고 업데이트하는 기능)을 앱에 추가합니다. 다음 Codelab에서 뵙겠습니다!
11. 솔루션 코드 가져오기
이 Codelab의 솔루션 코드는 GitHub 저장소에 있습니다. 완료된 Codelab의 코드를 다운로드하려면 다음 git 명령어를 사용하세요.
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-inventory-app.git $ cd basic-android-kotlin-compose-training-inventory-app $ git checkout room
또는 ZIP 파일로 저장소를 다운로드한 다음 압축을 풀고 Android 스튜디오에서 열어도 됩니다.
이 Codelab의 솔루션 코드는 GitHub에서 확인하세요.
12. 요약
- 테이블을
@Entity
주석이 달린 데이터 클래스로 정의합니다.@ColumnInfo
주석이 달린 속성을 테이블의 열로 정의합니다. - 데이터 액세스 객체(DAO)를
@Dao
주석이 달린 인터페이스로 정의합니다. DAO는 Kotlin 함수를 데이터베이스 쿼리에 매핑합니다. - 주석을 사용하여
@Insert
,@Delete
,@Update
함수를 정의합니다. - SQLite 쿼리 문자열과 함께
@Query
주석을 다른 쿼리의 매개변수로 사용합니다. - Database Inspector를 사용하여 Android SQLite 데이터베이스에 저장된 데이터를 확인합니다.
13. 자세히 알아보기
Android 개발자 문서
블로그 게시물
동영상
기타 문서 및 도움말