Room을 사용하여 데이터 유지

대부분의 프로덕션 품질 앱에는 사용자가 앱을 닫은 후에도 저장해야 하는 데이터가 있습니다. 예를 들어 앱은 노래 재생목록이나 할 일 목록의 항목, 수입 및 지출 기록, 별자리 카탈로그, 개인 정보 기록을 저장할 수 있습니다. 이러한 대부분의 사례에서 이 영구 데이터를 저장하는 데 데이터베이스를 사용합니다.

Room은 Android Jetpack의 일부인 지속성 라이브러리입니다. Room은 SQLite 데이터베이스 위에 있는 추상화 레이어입니다. SQLite는 특수 언어(SQL)를 사용하여 데이터베이스 작업을 실행합니다. SQLite를 직접 사용하는 대신 Room은 데이터베이스 설정과 구성, 상호작용 작업을 간소화합니다. Room은 SQLite 문의 컴파일 시간 확인도 제공합니다.

아래 이미지는 Room이 이 과정에서 권장하는 전체 아키텍처에 어떻게 부합하는지 보여줍니다.

7521165e051cc0d4.png

기본 요건

  • Android 앱의 기본 사용자 인터페이스(UI) 빌드 방법을 알아야 합니다.
  • 활동과 프래그먼트, 뷰 사용 방법을 알아야 합니다.
  • 프래그먼트 간 이동 방법을 알고 Safe Args를 사용하여 프래그먼트 간 데이터를 전달해야 합니다.
  • Android 아키텍처 구성요소 ViewModel, LiveData, Flow를 잘 알아야 하고 ViewModelProvider.Factory를 사용하여 ViewModel을 인스턴스화하는 방법을 알아야 합니다.
  • 동시 실행 기본사항을 잘 알아야 합니다.
  • 장기 실행 작업에 코루틴을 사용하는 방법을 알아야 합니다.
  • SQL 데이터베이스와 SQLite 언어의 기본사항을 알아야 합니다.

학습할 내용

  • Room 라이브러리를 사용하여 SQLite 데이터베이스를 만들고 상호작용하는 방법
  • 항목, DAO, 데이터베이스 클래스를 만드는 방법
  • 데이터 액세스 객체(DAO)를 사용하여 Kotlin 함수를 SQL 쿼리에 매핑하는 방법

빌드할 항목

  • SQLite 데이터베이스에 인벤토리 항목을 저장하는 Inventory 앱을 빌드합니다.

필요한 항목

  • Inventory 앱의 시작 코드
  • Android 스튜디오가 설치된 컴퓨터

이 Codelab에서는 Inventory 앱이라는 시작 앱을 사용하고, Room 라이브러리를 사용하여 데이터베이스 레이어를 앱에 추가합니다. 최종 버전의 앱은 RecyclerView를 사용하여 인벤토리 데이터베이스의 목록 항목을 표시합니다. 사용자는 새 항목을 추가하거나 기존 항목을 업데이트하거나 인벤토리 데이터베이스에서 항목을 삭제할 수 있습니다. 다음 Codelab에서 앱의 기능을 완성합니다.

다음은 최종 버전 앱의 스크린샷입니다.

52087556378ea8db.png

이 Codelab의 시작 코드 다운로드

이 Codelab은 시작 코드를 제공합니다. 이 Codelab에서 학습한 기능을 사용하여 시작 코드를 확장할 수 있습니다. 시작 코드에는 이전 Codelab을 통해 익숙한 코드와 이후 Codelab에서 학습할 익숙하지 않은 코드가 포함되어 있을 수 있습니다.

GitHub의 시작 코드를 사용하는 경우 폴더 이름은 android-basics-kotlin-inventory-app-starter입니다. Android 스튜디오에서 프로젝트를 열 때 이 폴더를 선택하세요.

이 Codelab의 코드를 가져와서 Android 스튜디오에서 열려면 다음을 실행합니다.

코드 가져오기

  1. 제공된 URL을 클릭합니다. 브라우저에서 프로젝트의 GitHub 페이지가 열립니다.
  2. 프로젝트의 GitHub 페이지에서 Code 버튼을 클릭하여 대화상자를 엽니다.

5b0a76c50478a73f.png

  1. 대화상자에서 Download ZIP 버튼을 클릭하여 컴퓨터에 프로젝트를 저장합니다. 다운로드가 완료될 때까지 기다립니다.
  2. 컴퓨터에서 파일을 찾습니다(예: Downloads 폴더).
  3. ZIP 파일을 더블클릭하여 압축을 해제합니다. 프로젝트 파일이 포함된 새 폴더가 만들어집니다.

Android 스튜디오에서 프로젝트 열기

  1. Android 스튜디오를 시작합니다.
  2. Welcome to Android Studio 창에서 Open an existing Android Studio project를 클릭합니다.

36cc44fcf0f89a1d.png

참고: Android 스튜디오가 이미 열려 있는 경우 File > New > Import Project 메뉴 옵션을 대신 선택합니다.

21f3eec988dcfbe9.png

  1. Import Project 대화상자에서 압축 해제된 프로젝트 폴더가 있는 위치로 이동합니다(예: Downloads 폴더).
  2. 프로젝트 폴더를 더블클릭합니다.
  3. Android 스튜디오가 프로젝트를 열 때까지 기다립니다.
  4. Run 버튼 11c34fc5e516fb1c.png을 클릭하여 앱을 빌드하고 실행합니다. 예상대로 작동하는지 확인합니다.
  5. Project 도구 창에서 프로젝트 파일을 살펴보고 앱이 구현된 방식을 확인합니다.

시작 코드 개요

  1. Android 스튜디오에서 시작 코드가 있는 프로젝트를 엽니다.
  2. Android 기기나 에뮬레이터에서 앱을 실행합니다. 에뮬레이터나 연결된 기기가 API 수준 26 이상을 실행하는지 확인합니다. Database Inspector는 API 수준 26을 실행하는 에뮬레이터/기기에서 가장 잘 작동합니다.
  3. 앱에 표시되는 인벤토리 데이터가 없습니다. 데이터베이스에 새 항목을 추가하는 FAB를 확인합니다.
  4. FAB를 클릭합니다. 앱이 새 화면으로 이동하고 여기서 새 항목의 세부정보를 입력할 수 있습니다.

9c5e361a89453821.png

시작 코드의 문제

  1. Add Item 화면에서 항목의 세부정보를 입력합니다. Save를 탭합니다. 항목 추가 프래그먼트가 닫히고 사용자는 이전 프래그먼트로 다시 이동합니다. 새 항목이 저장되지 않아 인벤토리 화면에 나열되지 않습니다. 앱이 완성되지 않아 Save 버튼 기능이 구현되지 않았습니다.

f0931dab5089a14f.png

이 Codelab에서는 SQLite 데이터베이스에 인벤토리 세부정보를 저장하는 앱의 데이터베이스 부분을 추가합니다. Room 지속성 라이브러리를 사용하여 SQLite 데이터베이스와 상호작용합니다.

코드 둘러보기

다운로드한 시작 코드에는 화면 레이아웃이 미리 디자인되어 있습니다. 이 과정에서는 데이터베이스 로직을 구현하는 데 중점을 둡니다. 시작하는 데 도움이 되는 다음과 같은 몇 가지 파일을 둘러보겠습니다.

main_activity.xml

앱에 있는 다른 모든 프래그먼트를 호스팅하는 기본 활동입니다. onCreate() 메서드는 NavHostFragment에서 NavController를 검색하고 NavController와 함께 사용할 작업 모음을 설정합니다.

item_list_fragment.xml

앱에 표시되는 첫 번째 화면입니다. 주로 RecyclerView와 FAB가 포함됩니다. 나중에 이 과정에서 RecyclerView를 구현합니다.

fragment_add_item.xml

이 레이아웃에는 추가할 새 인벤토리 항목의 세부정보를 입력하는 텍스트 필드가 포함되어 있습니다.

ItemListFragment.kt

이 프래그먼트에는 대부분 상용구 코드가 포함되어 있습니다. onViewCreated() 메서드에서 클릭 리스너는 항목 추가 프래그먼트로 이동하는 FAB에 설정됩니다.

AddItemFragment.kt

이 프래그먼트는 데이터베이스에 새 항목을 추가하는 데 사용됩니다. onCreateView() 함수는 바인딩 변수를 초기화하고 onDestroyView() 함수는 프래그먼트를 제거하기 전에 키보드를 숨깁니다.

Kotlin은 데이터 클래스를 도입하여 쉽게 데이터를 처리하는 방법을 제공합니다. 이 데이터는 함수 호출을 사용하여 액세스하고 수정도 할 수 있습니다. 그러나 데이터베이스 환경에서는 데이터에 액세스하여 수정하려면 테이블쿼리가 있어야 합니다. 다음과 같은 Room 구성요소를 통해 이러한 워크플로가 원활해집니다.

Room에는 다음과 같은 세 가지 주요 구성요소가 있습니다.

  • 데이터 항목은 앱 데이터베이스의 테이블을 나타냅니다. 테이블의 행에 저장된 데이터를 업데이트하고 삽입할 새 행을 만드는 데 사용됩니다.
  • 데이터 액세스 객체(DAO)는 앱이 데이터베이스의 데이터를 검색 및 업데이트, 삽입, 삭제하는 데 사용하는 메서드를 제공합니다.
  • 데이터베이스 클래스는 데이터베이스를 보유하며, 기본 앱 데이터베이스 연결을 위한 기본 액세스 포인트입니다. 데이터베이스 클래스는 앱에 데이터베이스와 연결된 DAO 인스턴스를 제공합니다.

Codelab 뒷부분에서 이러한 구성요소를 구현하며 자세히 알아봅니다. 다음 다이어그램은 Room 구성요소가 함께 작동하여 데이터베이스와 상호작용하는 방법을 보여줍니다.

33a193a68c9a8e0e.png

Room 라이브러리 추가

이 작업에서는 필요한 Room 구성요소 라이브러리를 Gradle 파일에 추가합니다.

  1. 모듈 수준 gradle 파일 build.gradle (Module: InventoryApp.app)을 엽니다. dependencies 블록에서 Room 라이브러리의 다음 종속 항목을 추가합니다.
    // Room
    implementation "androidx.room:room-runtime:$room_version"
    kapt "androidx.room:room-compiler:$room_version"
    implementation "androidx.room:room-ktx:$room_version"

항목 클래스는 테이블을 정의하고 이 클래스의 각 인스턴스는 데이터베이스 테이블의 행을 나타냅니다. 항목 클래스에는 데이터베이스의 정보를 표시하고 상호작용하는 방법을 Room에 알려주는 매핑이 있습니다. 앱에서 항목은 항목 이름, 항목 가격, 사용할 수 있는 재고 등 인벤토리 항목에 관한 정보를 보유합니다.

8c9f1659ee82ca43.png

@Entity 주석은 클래스를 데이터베이스 Entity 클래스로 표시합니다. 각 Entity 클래스에서 데이터베이스 테이블이 항목을 보유하려고 만들어집니다. Entity의 각 필드는 달리 표시되지 않는 한 데이터베이스에서 열로 표시됩니다(자세한 내용은 Entity 문서 참고). 데이터베이스에 저장된 모든 항목 인스턴스에는 기본 키가 있어야 합니다. 기본 키는 데이터베이스 테이블의 모든 레코드/항목을 고유하게 식별하는 데 사용됩니다. 일단 할당되면 기본 키는 수정할 수 없고 데이터베이스에 존재하는 한 항목 객체를 나타냅니다.

이 작업에서는 Entity 클래스를 만듭니다. 각 항목의 다음 인벤토리 정보를 저장할 필드를 정의합니다.

  • 기본 키를 저장할 Int
  • 항목 이름을 저장할 String
  • 항목 가격을 저장할 double
  • 재고 수량을 저장할 Int
  1. Android 스튜디오에서 시작 코드를 엽니다.
  2. com.example.inventory 기본 패키지에서 data라는 패키지를 만듭니다.

be39b42484ba2664.png

  1. data 패키지에서 Item이라는 새 Kotlin 클래스를 만듭니다. 이 클래스는 앱의 데이터베이스 항목을 나타냅니다. 다음 단계에서는 상응하는 필드를 추가하여 인벤토리 정보를 저장합니다.
  2. 다음 코드를 사용하여 Item 클래스 정의를 업데이트합니다. Int 유형의 idString, 유형의 itemName, Double 유형의 itemPrice, Int 유형의 quantityInStock을 기본 생성자의 매개변수로 선언합니다. 기본값 0id에 할당합니다. 기본 키인 ID는 Item 테이블의 모든 레코드/항목을 고유하게 식별합니다.
class Item(
   val id: Int = 0,
   val itemName: String,
   val itemPrice: Double,
   val quantityInStock: Int
)

data 클래스

데이터 클래스는 주로 Kotlin에서 데이터를 보유하는 데 사용됩니다. 키워드 data로 표시됩니다. Kotlin 데이터 클래스 객체에는 추가 이점이 있습니다. 컴파일러가 toString(), copy(), equals()와 같은 비교, 인쇄, 복사를 위한 유틸리티를 자동으로 생성합니다.

예:

// Example data class with 2 properties.
data class User(val first_name: String, val last_name: String){
}

생성된 코드의 일관성과 의미 있는 동작을 보장하기 위해 데이터 클래스는 다음 요구사항을 충족해야 합니다.

  • 기본 생성자에는 매개변수가 하나 이상 있어야 합니다.
  • 모든 기본 생성자 매개변수는 val이나 var로 표시되어야 합니다.
  • 데이터 클래스는 abstractopen, sealed, inner일 수 없습니다.

데이터 클래스에 관한 자세한 내용은 문서를 참고하세요.

  1. 클래스 정의 앞에 data 키워드를 배치하여 Item 클래스를 데이터 클래스로 변환합니다.
data class Item(
   val id: Int = 0,
   val itemName: String,
   val itemPrice: Double,
   val quantityInStock: Int
)
  1. Item 클래스 선언 위에서 데이터 클래스에 @Entity 주석을 답니다. tableName 인수를 사용하여 item을 SQLite 테이블 이름으로 지정합니다.
@Entity(tableName = "item")
data class Item(
   ...
)
  1. id를 기본 키로 식별하려면 id 속성에 @PrimaryKey 주석을 답니다. 매개변수 autoGeneratetrue로 설정하여 Room에서 각 항목의 ID를 생성하도록 합니다. 이렇게 하면 각 항목의 ID가 고유해집니다.
@Entity(tableName = "item")
data class Item(
   @PrimaryKey(autoGenerate = true)
   val id: Int = 0,
   ...
)
  1. 나머지 속성에 @ColumnInfo 주석을 답니다. ColumnInfo 주석은 특정 필드와 연결된 열을 맞춤설정하는 데 사용됩니다. 예를 들어 name 인수를 사용할 때 변수 이름이 아닌 다른 열 이름을 필드에 지정할 수 있습니다. 아래와 같이 매개변수를 사용하여 속성 이름을 맞춤설정합니다. 이 방식은 tableName을 사용하여 데이터베이스에 다른 이름을 지정하는 것과 비슷합니다.
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity
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
)

데이터 액세스 객체(DAO)

데이터 액세스 객체(DAO)는 추상 인터페이스를 제공하여 지속성 레이어를 애플리케이션의 나머지 부분과 분리하는 데 사용되는 패턴입니다. 이러한 격리는 이전 Codelab에서 확인한 단일 책임 원칙을 따릅니다.

DAO의 기능은 애플리케이션 나머지 부분의 기본 지속성 레이어에서 데이터베이스 작업 실행과 관련된 모든 복잡성을 숨기는 것입니다. 이를 통해 데이터를 사용하는 코드와는 상관없이 데이터 액세스 레이어를 변경할 수 있습니다.

dcef2fc739d704e5.png

이 작업에서는 Room의 데이터 액세스 객체(DAO)를 정의합니다. 데이터 액세스 객체는 데이터베이스에 액세스하는 인터페이스를 정의하는 Room의 기본 구성요소입니다.

여기서 만드는 DAO는 데이터베이스 쿼리/검색, 삽입, 삭제, 업데이트를 위한 편의 메서드를 제공하는 맞춤 인터페이스입니다. Room은 컴파일 시간에 이 클래스의 구현을 생성합니다.

일반적인 데이터베이스 작업의 경우 Room 라이브러리는 @Insert, @Delete, @Update와 같은 편의 주석을 제공합니다. 그 외 모든 경우에는 @Query 주석이 있습니다. SQLite에서 지원하는 모든 쿼리를 작성할 수 있습니다.

또 다른 이점은 Android 스튜디오에서 쿼리를 작성할 때 컴파일러가 SQL 쿼리에 구문 오류가 있는지 확인한다는 것입니다.

인벤토리 앱의 경우 다음 작업을 할 수 있어야 합니다.

  • 새 항목을 삽입하거나 추가합니다.
  • 기존 항목을 업데이트하여 이름과 가격, 수량을 업데이트합니다.
  • 기본 키인 id에 기반하여 특정 항목을 가져옵니다.
  • 모든 항목을 가져와서 표시할 수 있습니다.
  • 데이터베이스의 항목을 삭제합니다.

bb381857d5fba511.png

이제 앱에서 DAO 항목을 구현합니다.

  1. data 패키지에서 Kotlin 클래스 ItemDao.kt를 만듭니다.
  2. 클래스 정의를 interface로 변경하고 @Dao 주석을 답니다.
@Dao
interface ItemDao {
}
  1. 인터페이스 본문에 @Insert 주석을 추가합니다. @Insert 아래에서 Entity 클래스 item의 인스턴스를 인수로 사용하는 insert() 함수를 추가합니다. 데이터베이스 작업은 실행에 시간이 오래 걸릴 수 있으므로 별도의 스레드에서 실행해야 합니다. 함수를 정지 함수로 만들어 코루틴에서 이 함수를 호출할 수 있도록 합니다.
@Insert
suspend fun insert(item: Item)
  1. OnConflict 인수를 추가하고 OnConflictStrategy.IGNORE 값을 할당합니다. OnConflict 인수는 충돌이 발생하는 경우 Room에 실행할 작업을 알려줍니다. OnConflictStrategy.IGNORE 전략은 기본 키가 이미 데이터베이스에 있으면 새 항목을 무시합니다. 사용 가능한 충돌 전략에 관한 자세한 내용은 문서를 참고하세요.
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(item: Item)

이제 Roomitem을 데이터베이스에 삽입하는 데 필요한 모든 코드를 생성합니다. Kotlin 코드에서 insert()를 호출하면 Room은 SQL 쿼리를 실행하여 데이터베이스에 항목을 삽입합니다. 참고: 함수 이름은 원하는 대로 지정할 수 있습니다. insert()가 아니어도 됩니다.

  1. itemupdate() 함수와 함께 @Update 주석을 추가합니다. 업데이트된 항목에는 전달된 항목과 같은 키가 있습니다. 항목의 다른 속성 일부나 전부를 업데이트할 수 있습니다. insert() 메서드와 마찬가지로 다음 update() 메서드를 suspend로 만듭니다.
@Update
suspend fun update(item: Item)
  1. delete() 함수와 함께 @Delete 주석을 추가하여 항목을 삭제합니다. 정지 메서드로 만듭니다. @Delete 주석은 한 항목이나 항목 목록을 삭제합니다. 참고: 삭제할 항목을 전달해야 합니다. 항목이 없으면 delete() 함수를 호출하기 전에 가져와야 할 수 있습니다.
@Delete
suspend fun delete(item: Item)

나머지 기능에는 편의 주석이 없으므로 @Query 주석을 사용하여 SQLite 쿼리를 제공해야 합니다.

  1. 주어진 id에 기반하여 항목 테이블에서 특정 항목을 검색하는 SQLite 쿼리를 작성합니다. 그런 다음 Room 주석을 추가하고 이후 단계에서 수정된 버전의 다음 쿼리를 사용합니다. 다음 단계에서는 Room을 사용하여 이를 DAO 메서드로 변경합니다.
  2. item에서 모든 열을 선택합니다.
  3. WHERE id는 특정 값과 일치합니다.

예:

SELECT * from item WHERE id = 1
  1. 위 SQL 쿼리를 변경하여 Room 주석 및 인수와 함께 사용합니다. @Query 주석을 추가하고 쿼리를 @Query 주석에 문자열 매개변수로 제공합니다. String 매개변수를 SQLite 쿼리인 @Query에 추가하여 항목 테이블에서 항목을 검색합니다.
  2. item에서 모든 열을 선택합니다.
  3. WHERE id는 :id 인수와 일치합니다. :id를 확인합니다. 쿼리에서 콜론 표기법을 사용하여 함수의 인수를 참조합니다.
@Query("SELECT * from item WHERE id = :id")
  1. @Query 주석 아래에 Int 인수를 사용하고 Flow<Item>을 반환하는 getItem() 함수를 추가합니다.
@Query("SELECT * from item WHERE id = :id")
fun getItem(id: Int): Flow<Item>

FlowLiveData를 반환 유형으로 사용하면 데이터베이스의 데이터가 변경될 때마다 알림을 받을 수 있습니다. 지속성 레이어에서 Flow를 사용하는 것이 좋습니다. Room은 이 Flow를 자동으로 업데이트하므로 명시적으로 한 번만 데이터를 가져오면 됩니다. 이는 다음 Codelab에서 구현할 인벤토리 목록을 업데이트하는 데 유용합니다. Flow 반환 유형으로 인해 Room은 백그라운드 스레드에서도 쿼리를 실행합니다. 이를 명시적으로 suspend 함수로 만들고 코루틴 범위 내에서 호출할 필요는 없습니다.

kotlinx.coroutines.flow.Flow에서 Flow를 가져와야 할 수도 있습니다.

  1. getItems() 함수와 함께 @Query를 추가합니다.
  2. SQLite 쿼리가 item 테이블의 모든 열을 오름차순으로 반환하도록 합니다.
  3. getItems()Item 항목의 목록을 Flow로 반환하도록 합니다. Room은 이 Flow를 자동으로 업데이트하므로 명시적으로 한 번만 데이터를 가져오면 됩니다.
@Query("SELECT * from item ORDER BY name ASC")
fun getItems(): Flow<List<Item>>
  1. 눈에 띄는 변경사항은 표시되지 않지만 앱을 실행하여 오류가 없는지 확인합니다.

이 작업에서는 이전 작업에서 만든 Entity와 DAO를 사용하는 RoomDatabase를 만듭니다. 데이터베이스 클래스는 항목 및 데이터 액세스 객체의 목록을 정의합니다. 기본 연결의 기본 액세스 포인트이기도 합니다.

Database 클래스는 개발자가 정의한 DAO의 인스턴스를 앱에 제공합니다. 결과적으로 앱은 DAO를 사용하여 데이터베이스의 데이터를 연결된 데이터 항목 객체의 인스턴스로 검색할 수 있습니다. 앱은 정의된 데이터 항목을 사용하여 상응하는 테이블의 행을 업데이트하거나 삽입할 새 행을 만들 수도 있습니다.

@Database 주석이 달린 추상 RoomDatabase 클래스를 만들어야 합니다. 이 클래스에는 RoomDatabase의 인스턴스를 만들거나(없는 경우) RoomDatabase의 기존 인스턴스를 반환하는 메서드가 하나 있습니다.

다음은 RoomDatabase 인스턴스를 가져오는 일반적인 프로세스입니다.

  • RoomDatabase를 확장하는 public abstract 클래스를 만듭니다. 정의한 새 추상 클래스는 데이터베이스 홀더 역할을 합니다. 정의한 클래스는 추상 클래스입니다. Room이 구현을 만들기 때문입니다.
  • 클래스에 @Database 주석을 답니다. 인수에서 데이터베이스의 항목을 나열하고 버전 번호를 설정합니다.
  • ItemDao 인스턴스를 반환하는 추상 메서드나 속성을 정의하면 Room이 구현을 생성합니다.
  • 전체 앱에 RoomDatabase 인스턴스 하나만 있으면 되므로 RoomDatabase를 싱글톤으로 만듭니다.
  • RoomRoom.databaseBuilder를 사용하여 (item_database) 데이터베이스를 만듭니다(없는 경우에만). 있다면 기존 데이터베이스를 반환합니다.

데이터베이스 만들기

  1. data 패키지에서 Kotlin 클래스 ItemRoomDatabase.kt를 만듭니다.
  2. ItemRoomDatabase.kt 파일에서 ItemRoomDatabase 클래스를 RoomDatabase를 확장하는 abstract 클래스로 만듭니다. 클래스에 @Database 주석을 답니다. 다음 단계에서 누락된 매개변수 오류를 수정합니다.
@Database
abstract class ItemRoomDatabase : RoomDatabase() {}
  1. @Database 주석에는 Room이 데이터베이스를 빌드할 수 있도록 인수가 여러 개 필요합니다.
  • Itementities 목록이 있는 유일한 클래스로 지정합니다.
  • version1로 설정합니다. 데이터베이스 테이블의 스키마를 변경할 때마다 버전 번호를 높여야 합니다.
  • 스키마 버전 기록 백업을 유지하지 않도록 exportSchemafalse로 설정합니다.
@Database(entities = [Item::class], version = 1, exportSchema = false)
  1. 데이터베이스는 DAO를 알아야 합니다. 클래스 본문 내에서 ItemDao를 반환하는 추상 함수를 선언합니다. DAO는 여러 개가 있을 수 있습니다.
abstract fun itemDao(): ItemDao
  1. 추상 함수 아래에서 companion 객체를 정의합니다. 컴패니언 객체를 통해 클래스 이름을 한정자로 사용하여 데이터베이스를 만들거나 가져오는 메서드에 액세스할 수 있습니다.
 companion object {}
  1. companion 객체 내에서 데이터베이스에 관한 null을 허용하는 비공개 변수 INSTANCE를 선언하고 null로 초기화합니다. INSTANCE 변수는 데이터베이스가 만들어지면 데이터베이스 참조를 유지합니다. 이를 통해 주어진 시점에 열린 데이터베이스의 단일 인스턴스를 유지할 수 있습니다. 데이터베이스는 만들고 유지하는 데 비용이 많이 듭니다.

@VolatileINSTANCE 주석을 답니다. 휘발성 변수의 값은 캐시되지 않고 모든 쓰기와 읽기는 기본 메모리에서 실행됩니다. 이렇게 하면 INSTANCE 값이 항상 최신 상태로 유지되고 모든 실행 스레드에서 같은지 확인할 수 있습니다. 즉, 한 스레드에서 INSTANCE를 변경하면 다른 모든 스레드에 즉시 표시됩니다.

@Volatile
private var INSTANCE: ItemRoomDatabase? = null
  1. INSTANCE 아래에서(여전히 companion 객체 내) 데이터베이스 빌더에 필요한 Context 매개변수를 사용하여 getDatabase() 메서드를 정의합니다. ItemRoomDatabase 유형을 반환합니다. getDatabase()에서 아직 아무것도 반환하지 않아 오류가 표시됩니다.
fun getDatabase(context: Context): ItemRoomDatabase {}
  1. 여러 스레드가 잠재적으로 경합 상태로 실행되고 동시에 데이터베이스 인스턴스를 요청하여 하나가 아닌 두 개의 데이터베이스가 생성될 수 있습니다. 코드를 래핑하여 synchronized 블록 내에 데이터베이스를 가져오면 한 번에 한 실행 스레드만 이 코드 블록에 들어갈 수 있으므로 데이터베이스가 한 번만 초기화됩니다.

getDatabase() 내에서 INSTANCE 변수를 반환하거나 INSTANCE가 null이면 synchronized{} 블록 내에서 초기화합니다. elvis 연산자(?:)를 사용하면 됩니다. 함수 블록 내에서 잠그려는 컴패니언 객체를 this에 전달합니다. 다음 단계에서 오류를 수정합니다.

return INSTANCE ?: synchronized(this) { }
  1. synchronized 블록 내에서 val 인스턴스 변수를 만들고 데이터베이스 빌더를 사용하여 데이터베이스를 가져옵니다. 여전히 발생하는 오류는 다음 단계에서 수정합니다.
val instance = Room.databaseBuilder()
  1. synchronized 블록 끝에 instance를 반환합니다.
return instance
  1. synchronized 블록 내에서 instance 변수를 초기화하고 데이터베이스 빌더를 사용하여 데이터베이스를 가져옵니다. 애플리케이션 컨텍스트, 데이터베이스 클래스, 데이터베이스 이름 item_databaseRoom.databaseBuilder()에 전달합니다.
val instance = Room.databaseBuilder(
   context.applicationContext,
   ItemRoomDatabase::class.java,
   "item_database"
)

Android 스튜디오에서 유형 불일치 오류가 발생합니다. 이 오류를 삭제하려면 다음 단계에서 이전 전략과 build()를 추가해야 합니다.

  1. 필요한 이전 전략을 빌더에 추가합니다. .fallbackToDestructiveMigration()을 사용합니다.

일반적으로 스키마 변경 시점에 관한 이전 전략과 함께 이전 객체를 제공해야 합니다. 이전 객체는 데이터가 손실되지 않도록 이전 스키마의 모든 행을 가져와 새 스키마의 행으로 변환하는 방법을 정의하는 객체입니다. 이전은 이 Codelab의 범위를 벗어납니다. 간단한 해결 방법은 데이터베이스를 제거했다가 다시 빌드하는 것입니다. 즉, 데이터가 손실됩니다.

.fallbackToDestructiveMigration()
  1. 데이터베이스 인스턴스를 만들려면 .build()를 호출합니다. 이렇게 하면 Android 스튜디오 오류가 삭제됩니다.
.build()
  1. synchronized 블록 내에서 INSTANCE = instance를 할당합니다.
INSTANCE = instance
  1. synchronized 블록 끝에 instance를 반환합니다. 최종 코드는 다음과 같이 표시됩니다.
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()
               INSTANCE = instance
               return instance
           }
       }
   }
}
  1. 코드를 빌드하여 오류가 없는지 확인합니다.

Application 클래스 구현

이 작업에서는 Application 클래스에서 데이터베이스 인스턴스를 인스턴스화합니다.

  1. InventoryApplication.kt를 열고 ItemRoomDatabase 유형의 database라는 val을 만듭니다. 컨텍스트를 전달하는 ItemRoomDatabase에서 getDatabase()를 호출하여 database 인스턴스를 인스턴스화합니다. lazy 위임을 사용하므로 참조가 처음 필요하거나 처음 참조에 액세스할 때(앱이 시작될 때가 아니라) database 인스턴스가 느리게 만들어집니다. 이렇게 하면 처음 액세스할 때 데이터베이스(디스크의 물리적 데이터베이스)가 만들어집니다.
import android.app.Application
import com.example.inventory.data.ItemRoomDatabase

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

나중에 ViewModel 인스턴스를 만들 때 Codelab에서 이 database 인스턴스를 사용합니다.

이제 Room 사용에 필요한 구성요소가 모두 준비되었습니다. 이 코드는 컴파일되고 실행되지만 실제로 작동하는지는 알 수 없습니다. 따라서 이제 새 항목을 인벤토리 데이터베이스에 추가하여 데이터베이스를 테스트하는 것이 좋습니다. 데이터베이스와 통신하는 ViewModel이 있으면 됩니다.

지금까지 데이터베이스를 만들었고 UI 클래스는 시작 코드의 일부였습니다. 앱의 임시 데이터를 저장하고 데이터베이스에도 액세스하려면 ViewModel이 있어야 합니다. 인벤토리 ViewModel은 DAO를 통해 데이터베이스와 상호작용하여 UI에 데이터를 제공합니다. 모든 데이터베이스 작업은 기본 UI 스레드에서 벗어나 실행되어야 하고 코루틴과 viewModelScope를 사용하면 됩니다.

91298a7c05e4f5e0.png

인벤토리 ViewModel 만들기

  1. com.example.inventory 패키지에서 Kotlin 클래스 파일 InventoryViewModel.kt를 만듭니다.
  2. ViewModel 클래스에서 InventoryViewModel 클래스를 확장합니다. ItemDao 객체를 매개변수로 기본 생성자에 전달합니다.
class InventoryViewModel(private val itemDao: ItemDao) : ViewModel() {}
  1. 클래스 외부에서 InventoryViewModel.kt 파일 끝에 InventoryViewModelFactory 클래스를 추가하여 InventoryViewModel 인스턴스를 인스턴스화합니다. ItemDao 인스턴스인 InventoryViewModel과 동일한 생성자 매개변수를 전달합니다. ViewModelProvider.Factory 클래스에서 클래스를 확장합니다. 다음 단계에서 구현되지 않은 메서드와 관련된 오류를 수정합니다.
class InventoryViewModelFactory(private val itemDao: ItemDao) : ViewModelProvider.Factory {
}
  1. 빨간색 전구를 클릭하고 멤버 구현을 선택하거나 다음과 같이 ViewModelProvider.Factory 클래스 내에서 create() 메서드를 재정의할 수 있습니다. 그러면 클래스 유형을 인수로 사용하여 ViewModel 객체를 반환합니다.
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
   TODO("Not yet implemented")
}
  1. create() 메서드를 구현합니다. modelClassInventoryViewModel 클래스와 같은지 확인하고 그 인스턴스를 반환합니다. 같지 않으면 예외가 발생합니다.
if (modelClass.isAssignableFrom(InventoryViewModel::class.java)) {
   @Suppress("UNCHECKED_CAST")
   return InventoryViewModel(itemDao) as T
}
throw IllegalArgumentException("Unknown ViewModel class")

ViewModel 채우기

이 작업에서는 InventoryViewModel 클래스를 채워 데이터베이스에 인벤토리 데이터를 추가합니다. 인벤토리 앱에서 Item 항목과 Add Item 화면을 확인합니다.

@Entity
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
)

85c644aced4198c5.png

항목을 데이터베이스에 추가하려면 특정 항목의 이름과 가격, 사용할 수 있는 재고가 필요합니다. Codelab의 뒷부분에서 Add Item 화면을 사용하여 사용자에게서 이러한 세부정보를 가져옵니다. 현재 작업에서는 세 가지 문자열을 ViewModel의 입력으로 사용하고 이를 Item 항목 인스턴스로 변환한 후 ItemDao 인스턴스를 사용하여 데이터베이스에 저장합니다. 이제 구현해보겠습니다.

  1. InventoryViewModel 클래스에서 Item 객체를 가져오고 비차단 방식으로 데이터를 데이터베이스에 추가하는 insertItem()이라는 private 함수를 추가합니다.
private fun insertItem(item: Item) {
}
  1. 기본 스레드 밖에서 데이터베이스와 상호작용하려면 코루틴을 시작하고 그 안에서 DAO 메서드를 호출합니다. insertItem() 메서드 내에서 viewModelScope.launch를 사용하여 ViewModelScope에서 코루틴을 시작합니다. 시작 함수 내에서 item을 전달하는 itemDao에서 정지 함수 insert()를 호출합니다. ViewModelScopeViewModel이 소멸될 때 하위 코루틴을 자동으로 취소하는 ViewModel 클래스의 확장 속성입니다.
private fun insertItem(item: Item) {
   viewModelScope.launch {
       itemDao.insert(item)
   }
}

다음을 가져옵니다. kotlinx.coroutines.launch, androidx.lifecycle.viewModelScope

com.example.inventory.data.Item(자동으로 가져오지 않는 경우)

  1. InventoryViewModel 클래스에서 문자열 세 개를 가져오고 Item 인스턴스를 반환하는 또 다른 비공개 함수를 추가합니다.
private fun getNewItemEntry(itemName: String, itemPrice: String, itemCount: String): Item {
   return Item(
       itemName = itemName,
       itemPrice = itemPrice.toDouble(),
       quantityInStock = itemCount.toInt()
   )
}
  1. 여전히 InventoryViewModel 클래스에서 항목 세부정보 문자열을 세 개 가져오는 addNewItem()이라는 공개 함수를 추가합니다. 항목 세부정보 문자열을 getNewItemEntry() 함수에 전달하고 반환된 값을 newItem이라는 val에 할당합니다. insertItem을 전달하는 insertItem()을 호출하여 새 항목을 데이터베이스에 추가합니다. 이는 항목 세부정보를 데이터베이스에 추가하려고 UI 프래그먼트에서 호출됩니다.
fun addNewItem(itemName: String, itemPrice: String, itemCount: String) {
   val newItem = getNewItemEntry(itemName, itemPrice, itemCount)
   insertItem(newItem)
}

addNewItem()viewModelScope.launch를 사용하지 않았지만 DAO 메서드를 호출할 때 위 insertItem()에서 필요합니다. 그 이유는 정지 함수의 호출이 코루틴이나 또 다른 정지 함수에서만 허용되기 때문입니다. itemDao.insert(item) 함수는 정지 함수입니다.

항목을 데이터베이스에 추가하는 데 필요한 모든 함수를 추가했습니다. 다음 작업에서는 위 함수를 사용하도록 Add Item 프래그먼트를 업데이트합니다.

  1. AddItemFragment.kt에서 AddItemFragment 클래스 시작 부분에 InventoryViewModel 유형의 viewModel이라는 private val을 만듭니다. by activityViewModels() Kotlin 속성 위임을 사용하여 프래그먼트 전체에서 ViewModel을 공유합니다. 다음 단계에서 오류를 수정합니다.
private val viewModel: InventoryViewModel by activityViewModels {
}
  1. 람다 내에서 InventoryViewModelFactory() 생성자를 호출하고 ItemDao 인스턴스를 전달합니다. 이전 작업 중 하나에서 만든 database 인스턴스를 사용하여 itemDao 생성자를 호출합니다.
private val viewModel: InventoryViewModel by activityViewModels {
   InventoryViewModelFactory(
       (activity?.application as InventoryApplication).database
           .itemDao()
   )
}
  1. viewModel 정의 아래에 Item 유형의 item이라는 lateinit var을 만듭니다.
 lateinit var item: Item
  1. Add Item 화면에는 사용자에게서 항목 세부정보를 가져오는 텍스트 필드 세 개가 포함되어 있습니다. 이 단계에서는 TextFields의 텍스트가 비어 있지 않은지 확인하는 함수를 추가합니다. 이 함수를 사용하여 사용자 입력을 확인한 후 데이터베이스의 항목을 추가하거나 업데이트합니다. 이 확인 작업은 프래그먼트가 아닌 ViewModel에서 실행해야 합니다. InventoryViewModel 클래스에서 isEntryValid()라는 다음 public 함수를 추가합니다.
fun isEntryValid(itemName: String, itemPrice: String, itemCount: String): Boolean {
   if (itemName.isBlank() || itemPrice.isBlank() || itemCount.isBlank()) {
       return false
   }
   return true
}
  1. AddItemFragment.kt에서 onCreateView() 함수 아래에 Boolean을 반환하는 isEntryValid()라는 private 함수를 만듭니다. 다음 단계에서 누락된 반환 값 오류를 수정합니다.
private fun isEntryValid(): Boolean {
}
  1. AddItemFragment 클래스에서 isEntryValid() 함수를 구현합니다. viewModel 인스턴스에서 isEntryValid() 함수를 호출하여 텍스트 뷰의 텍스트를 전달합니다. viewModel.isEntryValid() 함수의 값을 반환합니다.
private fun isEntryValid(): Boolean {
   return viewModel.isEntryValid(
       binding.itemName.text.toString(),
       binding.itemPrice.text.toString(),
       binding.itemCount.text.toString()
   )
}
  1. isEntryValid() 함수 아래 AddItemFragment 클래스에서 addNewItem()이라는 또 다른 private 함수를 매개변수 없이 추가하고 아무것도 반환하지 않습니다. 함수 내에서 if 조건 내에 isEntryValid()를 호출합니다.
private fun addNewItem() {
   if (isEntryValid()) {
   }
}
  1. if 블록 내 viewModel 인스턴스에서 addNewItem() 메서드를 호출합니다. 사용자가 입력한 항목 세부정보를 전달하고 binding 인스턴스를 사용하여 읽습니다.
if (isEntryValid()) {
   viewModel.addNewItem(
   binding.itemName.text.toString(),
   binding.itemPrice.text.toString(),
   binding.itemCount.text.toString(),
   )
}
  1. if 블록 아래에 ItemListFragment로 다시 이동하는 val action을 만듭니다. findNavController().navigate()를 호출하여 action을 전달합니다.
val action = AddItemFragmentDirections.actionAddItemFragmentToItemListFragment()
findNavController().navigate(action)

androidx.navigation.fragment.findNavController.를 가져옵니다.

  1. 완료된 메서드는 다음과 같이 표시됩니다.
private fun addNewItem() {
       if (isEntryValid()) {
           viewModel.addNewItem(
               binding.itemName.text.toString(),
               binding.itemPrice.text.toString(),
               binding.itemCount.text.toString(),
           )
           val action = AddItemFragmentDirections.actionAddItemFragmentToItemListFragment()
           findNavController().navigate(action)
       }
   }
}
  1. 모든 것을 연결하려면 클릭 핸들러를 Save 버튼에 추가합니다. AddItemFragment 클래스의 onDestroyView() 함수 위에서 onViewCreated() 함수를 재정의합니다.
  2. onViewCreated() 함수에서 클릭 핸들러를 저장 버튼에 추가하고 addNewItem()을 호출합니다.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   super.onViewCreated(view, savedInstanceState)
   binding.saveAction.setOnClickListener {
       addNewItem()
   }
}
  1. 앱을 빌드하고 실행합니다. + Fab를 탭합니다. Add Item 화면에서 항목 세부정보를 추가하고 Save를 탭합니다. 이 작업으로 데이터가 저장되지만 아직 앱에는 아무것도 표시되지 않습니다. 다음 작업에서 Database Inspector를 사용하여 저장한 데이터를 확인합니다.

193c7fa9c41e0819.png

Database Inspector를 사용하여 데이터베이스 보기

  1. API 수준 26 이상을 실행하는 에뮬레이터 또는 연결된 기기에서 앱을 실행합니다(아직 실행하지 않은 경우). Database Inspector는 API 수준 26을 실행하는 에뮬레이터/기기에서 가장 잘 작동합니다.
  2. Android 스튜디오의 메뉴 바에서 View > Tool Windows > Database Inspector를 선택합니다.
  3. Database Inspector 창의 드롭다운 메뉴에서 com.example.inventory를 선택합니다.
  4. Inventory 앱의 item_databaseDatabases 창에 표시됩니다. item_database의 노드를 확장하고 검사할 Item을 선택합니다. Databases 창이 비어 있으면 에뮬레이터에서 Add Item 화면을 사용하여 항목을 데이터베이스에 추가합니다.
  5. Database Inspector에서 Live updates 체크박스를 선택하여 에뮬레이터나 기기에서 실행 중인 앱과 상호작용할 때 표시되는 데이터를 자동으로 업데이트합니다.

4803c08f94e34118.png

축하합니다. Room을 사용하여 데이터를 유지할 수 있는 앱을 만들었습니다. 다음 Codelab에서는 RecyclerView를 앱에 추가하여 데이터베이스에 항목을 표시하고 항목 삭제 및 업데이트와 같은 새로운 기능을 앱에 추가합니다. 다음 Codelab에서 뵙겠습니다!

이 Codelab의 솔루션 코드는 아래와 같이 GitHub 저장소와 분기에 있습니다.

이 Codelab의 코드를 가져와서 Android 스튜디오에서 열려면 다음을 실행합니다.

코드 가져오기

  1. 제공된 URL을 클릭합니다. 브라우저에서 프로젝트의 GitHub 페이지가 열립니다.
  2. 프로젝트의 GitHub 페이지에서 Code 버튼을 클릭하여 대화상자를 엽니다.

5b0a76c50478a73f.png

  1. 대화상자에서 Download ZIP 버튼을 클릭하여 컴퓨터에 프로젝트를 저장합니다. 다운로드가 완료될 때까지 기다립니다.
  2. 컴퓨터에서 파일을 찾습니다(예: Downloads 폴더).
  3. ZIP 파일을 더블클릭하여 압축을 해제합니다. 프로젝트 파일이 포함된 새 폴더가 만들어집니다.

Android 스튜디오에서 프로젝트 열기

  1. Android 스튜디오를 시작합니다.
  2. Welcome to Android Studio 창에서 Open an existing Android Studio project를 클릭합니다.

36cc44fcf0f89a1d.png

참고: Android 스튜디오가 이미 열려 있는 경우 File > New > Import Project 메뉴 옵션을 대신 선택합니다.

21f3eec988dcfbe9.png

  1. Import Project 대화상자에서 압축 해제된 프로젝트 폴더가 있는 위치로 이동합니다(예: Downloads 폴더).
  2. 프로젝트 폴더를 더블클릭합니다.
  3. Android 스튜디오가 프로젝트를 열 때까지 기다립니다.
  4. Run 버튼 11c34fc5e516fb1c.png을 클릭하여 앱을 빌드하고 실행합니다. 예상대로 작동하는지 확인합니다.
  5. Project 도구 창에서 프로젝트 파일을 살펴보고 앱이 구현된 방식을 확인합니다.
  • 테이블을 @Entity 주석이 달린 데이터 클래스로 정의합니다. @ColumnInfo 주석이 달린 속성을 테이블의 열로 정의합니다.
  • 데이터 액세스 객체(DAO)를 @Dao 주석이 달린 인터페이스로 정의합니다. DAO는 Kotlin 함수를 데이터베이스 쿼리에 매핑합니다.
  • 주석을 사용하여 @Insert, @Delete, @Update 함수를 정의합니다.
  • SQLite 쿼리 문자열과 함께 @Query 주석을 다른 쿼리의 매개변수로 사용합니다.
  • Database Inspector를 사용하여 Android SQLite 데이터베이스에 저장된 데이터를 확인합니다.

Android 개발자 문서

블로그 게시물

동영상

기타 문서 및 도움말