1. 시작하기 전에
대부분의 프로덕션 품질 앱에는 사용자가 앱을 닫은 후에도 저장해야 하는 데이터가 있습니다. 예를 들어 앱은 노래 재생목록이나 할 일 목록의 항목, 수입 및 지출 기록, 별자리 카탈로그, 개인 정보 기록을 저장할 수 있습니다. 이러한 대부분의 사례에서 이 영구 데이터를 저장하는 데 데이터베이스를 사용합니다.
Room은 Android Jetpack의 일부인 지속성 라이브러리입니다. Room은 SQLite 데이터베이스 위에 있는 추상화 레이어입니다. SQLite는 특수 언어(SQL)를 사용하여 데이터베이스 작업을 실행합니다. SQLite를 직접 사용하는 대신 Room은 데이터베이스 설정과 구성, 상호작용 작업을 간소화합니다. Room은 SQLite 문의 컴파일 시간 확인도 제공합니다.
아래 이미지는 Room이 이 과정에서 권장하는 전체 아키텍처에 어떻게 부합하는지 보여줍니다.
기본 요건
- Android 앱의 기본 사용자 인터페이스(UI) 빌드 방법을 알아야 합니다.
- 활동과 프래그먼트, 뷰 사용 방법을 알아야 합니다.
- 프래그먼트 간 이동 방법을 알고 Safe Args를 사용하여 프래그먼트 간 데이터를 전달해야 합니다.
- Android 아키텍처 구성요소
ViewModel
,LiveData
,Flow
를 잘 알아야 하고ViewModelProvider.Factory
를 사용하여 ViewModel을 인스턴스화하는 방법을 알아야 합니다. - 동시 실행 기본사항을 잘 알아야 합니다.
- 장기 실행 작업에 코루틴을 사용하는 방법을 알아야 합니다.
- SQL 데이터베이스와 SQLite 언어의 기본사항을 알아야 합니다.
학습할 내용
- Room 라이브러리를 사용하여 SQLite 데이터베이스를 만들고 상호작용하는 방법
- 항목, DAO, 데이터베이스 클래스를 만드는 방법
- 데이터 액세스 객체(DAO)를 사용하여 Kotlin 함수를 SQL 쿼리에 매핑하는 방법
빌드할 항목
- SQLite 데이터베이스에 인벤토리 항목을 저장하는 Inventory 앱을 빌드합니다.
필요한 항목
- Inventory 앱의 시작 코드
- Android 스튜디오가 설치된 컴퓨터
2. 앱 개요
이 Codelab에서는 Inventory 앱이라는 시작 앱을 사용하고, Room 라이브러리를 사용하여 데이터베이스 레이어를 앱에 추가합니다. 최종 버전의 앱은 RecyclerView
를 사용하여 인벤토리 데이터베이스의 목록 항목을 표시합니다. 사용자는 새 항목을 추가하거나 기존 항목을 업데이트하거나 인벤토리 데이터베이스에서 항목을 삭제할 수 있습니다. 다음 Codelab에서 앱의 기능을 완성합니다.
다음은 최종 버전 앱의 스크린샷입니다.
3. 시작 앱 개요
이 Codelab의 시작 코드 다운로드
이 Codelab은 시작 코드를 제공합니다. 이 Codelab에서 학습한 기능을 사용하여 시작 코드를 확장할 수 있습니다. 시작 코드에는 이전 Codelab을 통해 익숙한 코드와 이후 Codelab에서 학습할 익숙하지 않은 코드가 포함되어 있을 수 있습니다.
GitHub의 시작 코드를 사용하는 경우 폴더 이름은 android-basics-kotlin-inventory-app-starter
입니다. Android 스튜디오에서 프로젝트를 열 때 이 폴더를 선택하세요.
이 Codelab의 코드를 가져와서 Android 스튜디오에서 열려면 다음을 실행합니다.
코드 가져오기
- 제공된 URL을 클릭합니다. 브라우저에서 프로젝트의 GitHub 페이지가 열립니다.
- 프로젝트의 GitHub 페이지에서 Code 버튼을 클릭하여 대화상자를 엽니다.
- 대화상자에서 Download ZIP 버튼을 클릭하여 컴퓨터에 프로젝트를 저장합니다. 다운로드가 완료될 때까지 기다립니다.
- 컴퓨터에서 파일을 찾습니다(예: Downloads 폴더).
- ZIP 파일을 더블클릭하여 압축을 해제합니다. 프로젝트 파일이 포함된 새 폴더가 만들어집니다.
Android 스튜디오에서 프로젝트 열기
- Android 스튜디오를 시작합니다.
- Welcome to Android Studio 창에서 Open an existing Android Studio project를 클릭합니다.
참고: Android 스튜디오가 이미 열려 있는 경우 File > New > Import Project 메뉴 옵션을 대신 선택합니다.
- Import Project 대화상자에서 압축 해제된 프로젝트 폴더가 있는 위치로 이동합니다(예: Downloads 폴더).
- 프로젝트 폴더를 더블클릭합니다.
- Android 스튜디오가 프로젝트를 열 때까지 기다립니다.
- Run 버튼 을 클릭하여 앱을 빌드하고 실행합니다. 예상대로 작동하는지 확인합니다.
- Project 도구 창에서 프로젝트 파일을 살펴보고 앱이 설정된 방식을 확인합니다.
시작 코드 개요
- Android 스튜디오에서 시작 코드가 있는 프로젝트를 엽니다.
- Android 기기나 에뮬레이터에서 앱을 실행합니다. 에뮬레이터나 연결된 기기가 API 수준 26 이상을 실행하는지 확인합니다. Database Inspector는 API 수준 26을 실행하는 에뮬레이터/기기에서 가장 잘 작동합니다.
- 앱에 표시되는 인벤토리 데이터가 없습니다. 데이터베이스에 새 항목을 추가하는 FAB를 확인합니다.
- FAB를 클릭합니다. 앱이 새 화면으로 이동하고 여기서 새 항목의 세부정보를 입력할 수 있습니다.
시작 코드의 문제
- Add Item 화면에서 항목의 세부정보를 입력합니다. Save를 탭합니다. 항목 추가 프래그먼트가 닫혀 있지 않습니다. 시스템 뒤로 키를 사용하여 뒤로 이동합니다. 새 항목이 저장되지 않아 인벤토리 화면에 나열되지 않습니다. 앱이 완성되지 않아 Save 버튼 기능이 구현되지 않았습니다.
이 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()
함수는 프래그먼트를 제거하기 전에 키보드를 숨깁니다.
4. Room의 기본 구성요소
Kotlin은 데이터 클래스를 도입하여 쉽게 데이터를 처리하는 방법을 제공합니다. 이 데이터는 함수 호출을 사용하여 액세스하고 수정도 할 수 있습니다. 그러나 데이터베이스 환경에서는 데이터에 액세스하여 수정하려면 테이블과 쿼리가 있어야 합니다. 다음과 같은 Room 구성요소를 통해 이러한 워크플로가 원활해집니다.
Room에는 다음과 같은 세 가지 주요 구성요소가 있습니다.
- 데이터 항목은 앱 데이터베이스의 테이블을 나타냅니다. 테이블의 행에 저장된 데이터를 업데이트하고 삽입할 새 행을 만드는 데 사용됩니다.
- 데이터 액세스 객체(DAO)는 앱이 데이터베이스의 데이터를 검색 및 업데이트, 삽입, 삭제하는 데 사용하는 메서드를 제공합니다.
- 데이터베이스 클래스는 데이터베이스를 보유하며, 기본 앱 데이터베이스 연결을 위한 기본 액세스 포인트입니다. 데이터베이스 클래스는 앱에 데이터베이스와 연결된 DAO 인스턴스를 제공합니다.
Codelab 뒷부분에서 이러한 구성요소를 구현하며 자세히 알아봅니다. 다음 다이어그램은 Room 구성요소가 함께 작동하여 데이터베이스와 상호작용하는 방법을 보여줍니다.
Room 라이브러리 추가
이 작업에서는 필요한 Room 구성요소 라이브러리를 Gradle 파일에 추가합니다.
- 모듈 수준 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"
5. 항목 Entity 만들기
항목 클래스는 테이블을 정의하고 이 클래스의 각 인스턴스는 데이터베이스 테이블의 행을 나타냅니다. 항목 클래스에는 데이터베이스의 정보를 표시하고 상호작용하는 방법을 Room에 알려주는 매핑이 있습니다. 앱에서 항목은 항목 이름, 항목 가격, 사용할 수 있는 재고 등 인벤토리 항목에 관한 정보를 보유합니다.
@Entity
주석은 클래스를 데이터베이스 Entity 클래스로 표시합니다. 각 Entity 클래스에서 데이터베이스 테이블이 항목을 보유하려고 만들어집니다. Entity의 각 필드는 달리 표시되지 않는 한 데이터베이스에서 열로 표시됩니다(자세한 내용은 Entity 문서 참고). 데이터베이스에 저장된 모든 항목 인스턴스에는 기본 키가 있어야 합니다. 기본 키는 데이터베이스 테이블의 모든 레코드/항목을 고유하게 식별하는 데 사용됩니다. 일단 할당되면 기본 키는 수정할 수 없고 데이터베이스에 존재하는 한 항목 객체를 나타냅니다.
이 작업에서는 Entity 클래스를 만듭니다. 각 항목의 다음 인벤토리 정보를 저장할 필드를 정의합니다.
- 기본 키를 저장할
Int
- 항목 이름을 저장할
String
- 항목 가격을 저장할
double
- 재고 수량을 저장할
Int
- Android 스튜디오에서 시작 코드를 엽니다.
com.example.inventory
기본 패키지에서data
라는 패키지를 만듭니다.
data
패키지에서Item
이라는 새 Kotlin 클래스를 만듭니다. 이 클래스는 앱의 데이터베이스 항목을 나타냅니다. 다음 단계에서는 상응하는 필드를 추가하여 인벤토리 정보를 저장합니다.- 다음 코드를 사용하여
Item
클래스 정의를 업데이트합니다.Int
유형의id
와String,
유형의itemName
,Double
유형의itemPrice
,Int
유형의quantityInStock
을 기본 생성자의 매개변수로 선언합니다. 기본값0
을id
에 할당합니다. 기본 키인 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
로 표시되어야 합니다. - 데이터 클래스는
abstract
나open
,sealed
,inner
일 수 없습니다.
데이터 클래스에 관한 자세한 내용은 문서를 참고하세요.
- 클래스 정의 앞에
data
키워드를 배치하여Item
클래스를 데이터 클래스로 변환합니다.
data class Item(
val id: Int = 0,
val itemName: String,
val itemPrice: Double,
val quantityInStock: Int
)
Item
클래스 선언 위에서 데이터 클래스에@Entity
주석을 답니다.tableName
인수를 사용하여item
을 SQLite 테이블 이름으로 지정합니다.
@Entity(tableName = "item")
data class Item(
...
)
id
를 기본 키로 식별하려면id
속성에@PrimaryKey
주석을 답니다. 매개변수autoGenerate
를true
로 설정하여Room
에서 각 항목의 ID를 생성하도록 합니다. 이렇게 하면 각 항목의 ID가 고유해집니다.
@Entity(tableName = "item")
data class Item(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
...
)
- 나머지 속성에
@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
)
6. 항목 DAO 만들기
데이터 액세스 객체(DAO)
데이터 액세스 객체(DAO)는 추상 인터페이스를 제공하여 지속성 레이어를 애플리케이션의 나머지 부분과 분리하는 데 사용되는 패턴입니다. 이러한 격리는 이전 Codelab에서 확인한 단일 책임 원칙을 따릅니다.
DAO의 기능은 애플리케이션 나머지 부분의 기본 지속성 레이어에서 데이터베이스 작업 실행과 관련된 모든 복잡성을 숨기는 것입니다. 이를 통해 데이터를 사용하는 코드와 상관없이 데이터 액세스 레이어를 변경할 수 있습니다.
이 작업에서는 Room의 데이터 액세스 객체(DAO)를 정의합니다. 데이터 액세스 객체는 데이터베이스에 액세스하는 인터페이스를 정의하는 Room의 기본 구성요소입니다.
여기서 만드는 DAO는 데이터베이스 쿼리/검색, 삽입, 삭제, 업데이트를 위한 편의 메서드를 제공하는 맞춤 인터페이스입니다. Room은 컴파일 시간에 이 클래스의 구현을 생성합니다.
일반적인 데이터베이스 작업의 경우 Room
라이브러리는 @Insert
, @Delete
, @Update
와 같은 편의 주석을 제공합니다. 그 외 모든 경우에는 @Query
주석이 있습니다. SQLite에서 지원하는 모든 쿼리를 작성할 수 있습니다.
또 다른 이점은 Android 스튜디오에서 쿼리를 작성할 때 컴파일러가 SQL 쿼리에 구문 오류가 있는지 확인한다는 것입니다.
인벤토리 앱의 경우 다음 작업을 할 수 있어야 합니다.
- 새 항목을 삽입하거나 추가합니다.
- 기존 항목을 업데이트하여 이름과 가격, 수량을 업데이트합니다.
- 기본 키인
id
에 기반하여 특정 항목을 가져옵니다. - 모든 항목을 가져와서 표시할 수 있습니다.
- 데이터베이스의 항목을 삭제합니다.
이제 앱에서 DAO 항목을 구현합니다.
data
패키지에서 Kotlin 클래스ItemDao.kt
를 만듭니다.- 클래스 정의를
interface
로 변경하고@Dao
주석을 답니다.
@Dao
interface ItemDao {
}
- 인터페이스 본문에
@Insert
주석을 추가합니다.@Insert
아래에서Entity
클래스item
의 인스턴스를 인수로 사용하는insert()
함수를 추가합니다. 데이터베이스 작업은 실행에 시간이 오래 걸릴 수 있으므로 별도의 스레드에서 실행해야 합니다. 함수를 정지 함수로 만들어 코루틴에서 이 함수를 호출할 수 있도록 합니다.
@Insert
suspend fun insert(item: Item)
OnConflict
인수를 추가하고OnConflictStrategy.
IGNORE
값을 할당합니다.OnConflict
인수는 충돌이 발생하는 경우 Room에 실행할 작업을 알려줍니다.OnConflictStrategy.
IGNORE
전략은 기본 키가 이미 데이터베이스에 있으면 새 항목을 무시합니다. 사용 가능한 충돌 전략에 관한 자세한 내용은 문서를 참고하세요.
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(item: Item)
이제 Room
이 item
을 데이터베이스에 삽입하는 데 필요한 모든 코드를 생성합니다. Kotlin 코드에서 insert()
를 호출하면 Room
은 SQL 쿼리를 실행하여 데이터베이스에 항목을 삽입합니다. 참고: 함수 이름은 원하는 대로 지정할 수 있습니다. insert()
가 아니어도 됩니다.
- 한
item
에update()
함수와 함께@Update
주석을 추가합니다. 업데이트된 항목에는 전달된 항목과 같은 키가 있습니다. 항목의 다른 속성 일부나 전부를 업데이트할 수 있습니다.insert()
메서드와 마찬가지로 다음update()
메서드를suspend
로 만듭니다.
@Update
suspend fun update(item: Item)
delete()
함수와 함께@Delete
주석을 추가하여 항목을 삭제합니다. 정지 메서드로 만듭니다.@Delete
주석은 한 항목이나 항목 목록을 삭제합니다. 참고: 삭제할 항목을 전달해야 합니다. 항목이 없으면delete()
함수를 호출하기 전에 가져와야 할 수 있습니다.
@Delete
suspend fun delete(item: Item)
나머지 기능에는 편의 주석이 없으므로 @Query
주석을 사용하여 SQLite 쿼리를 제공해야 합니다.
- 주어진
id
에 기반하여 항목 테이블에서 특정 항목을 검색하는 SQLite 쿼리를 작성합니다. 그런 다음 Room 주석을 추가하고 이후 단계에서 수정된 버전의 다음 쿼리를 사용합니다. 다음 단계에서는 Room을 사용하여 이를 DAO 메서드로 변경합니다. item
에서 모든 열을 선택합니다.WHERE
id
는 특정 값과 일치합니다.
예:
SELECT * from item WHERE id = 1
- 위 SQL 쿼리를 변경하여 Room 주석 및 인수와 함께 사용합니다.
@Query
주석을 추가하고 쿼리를@Query
주석에 문자열 매개변수로 제공합니다.String
매개변수를 SQLite 쿼리인@Query
에 추가하여 항목 테이블에서 항목을 검색합니다. item
에서 모든 열을 선택합니다.WHERE
id
는 :id
인수와 일치합니다.:id
를 확인합니다. 쿼리에서 콜론 표기법을 사용하여 함수의 인수를 참조합니다.
@Query("SELECT * from item WHERE id = :id")
@Query
주석 아래에Int
인수를 사용하고Flow<Item>
을 반환하는getItem()
함수를 추가합니다.
@Query("SELECT * from item WHERE id = :id")
fun getItem(id: Int): Flow<Item>
Flow
나 LiveData
를 반환 유형으로 사용하면 데이터베이스의 데이터가 변경될 때마다 알림을 받을 수 있습니다. 지속성 레이어에서 Flow
를 사용하는 것이 좋습니다. Room
은 이 Flow
를 자동으로 업데이트하므로 명시적으로 한 번만 데이터를 가져오면 됩니다. 이는 다음 Codelab에서 구현할 인벤토리 목록을 업데이트하는 데 유용합니다. Flow
반환 유형으로 인해 Room은 백그라운드 스레드에서도 쿼리를 실행합니다. 이를 명시적으로 suspend
함수로 만들고 코루틴 범위 내에서 호출할 필요는 없습니다.
kotlinx.coroutines.flow.Flow
에서 Flow
를 가져와야 할 수도 있습니다.
getItems()
함수와 함께@Query
를 추가합니다.- SQLite 쿼리가
item
테이블의 모든 열을 오름차순으로 반환하도록 합니다. getItems()
가Item
항목의 목록을Flow
로 반환하도록 합니다.Room
은 이Flow
를 자동으로 업데이트하므로 명시적으로 한 번만 데이터를 가져오면 됩니다.
@Query("SELECT * from item ORDER BY name ASC")
fun getItems(): Flow<List<Item>>
- 눈에 띄는 변경사항은 표시되지 않지만 앱을 실행하여 오류가 없는지 확인합니다.
7. 데이터베이스 인스턴스 만들기
이 작업에서는 이전 작업에서 만든 Entity
와 DAO를 사용하는 RoomDatabase
를 만듭니다. 데이터베이스 클래스는 항목 및 데이터 액세스 객체의 목록을 정의합니다. 기본 연결의 기본 액세스 포인트이기도 합니다.
Database
클래스는 개발자가 정의한 DAO의 인스턴스를 앱에 제공합니다. 결과적으로 앱은 DAO를 사용하여 데이터베이스의 데이터를 연결된 데이터 항목 객체의 인스턴스로 검색할 수 있습니다. 앱은 정의된 데이터 항목을 사용하여 상응하는 테이블의 행을 업데이트하거나 삽입할 새 행을 만들 수도 있습니다.
@Database
주석이 달린 추상 RoomDatabase
클래스를 만들어야 합니다. 이 클래스에는 RoomDatabase
의 인스턴스를 만들거나(없는 경우) RoomDatabase
의 기존 인스턴스를 반환하는 메서드가 하나 있습니다.
다음은 RoomDatabase
인스턴스를 가져오는 일반적인 프로세스입니다.
RoomDatabase
를 확장하는public abstract
클래스를 만듭니다. 정의한 새 추상 클래스는 데이터베이스 홀더 역할을 합니다. 정의한 클래스는 추상 클래스입니다.Room
이 구현을 만들기 때문입니다.- 클래스에
@Database
주석을 답니다. 인수에서 데이터베이스의 항목을 나열하고 버전 번호를 설정합니다. ItemDao
인스턴스를 반환하는 추상 메서드나 속성을 정의하면Room
이 구현을 생성합니다.- 전체 앱에
RoomDatabase
인스턴스 하나만 있으면 되므로RoomDatabase
를 싱글톤으로 만듭니다. Room
의Room.databaseBuilder
를 사용하여 (item_database
) 데이터베이스를 만듭니다(없는 경우에만). 있다면 기존 데이터베이스를 반환합니다.
데이터베이스 만들기
data
패키지에서 Kotlin 클래스ItemRoomDatabase.kt
를 만듭니다.ItemRoomDatabase.kt
파일에서ItemRoomDatabase
클래스를RoomDatabase
를 확장하는abstract
클래스로 만듭니다. 클래스에@Database
주석을 답니다. 다음 단계에서 누락된 매개변수 오류를 수정합니다.
@Database
abstract class ItemRoomDatabase : RoomDatabase() {}
@Database
주석에는Room
이 데이터베이스를 빌드할 수 있도록 인수가 여러 개 필요합니다.
Item
을entities
목록이 있는 유일한 클래스로 지정합니다.version
을1
로 설정합니다. 데이터베이스 테이블의 스키마를 변경할 때마다 버전 번호를 높여야 합니다.- 스키마 버전 기록 백업을 유지하지 않도록
exportSchema
를false
로 설정합니다.
@Database(entities = [Item::class], version = 1, exportSchema = false)
- 데이터베이스는 DAO를 알아야 합니다. 클래스 본문 내에서
ItemDao
를 반환하는 추상 함수를 선언합니다. DAO는 여러 개가 있을 수 있습니다.
abstract fun itemDao(): ItemDao
- 추상 함수 아래에서
companion
객체를 정의합니다. 컴패니언 객체를 통해 클래스 이름을 한정자로 사용하여 데이터베이스를 만들거나 가져오는 메서드에 액세스할 수 있습니다.
companion object {}
companion
객체 내에서 데이터베이스에 관한 null을 허용하는 비공개 변수INSTANCE
를 선언하고null
로 초기화합니다.INSTANCE
변수는 데이터베이스가 만들어지면 데이터베이스 참조를 유지합니다. 이를 통해 주어진 시점에 열린 데이터베이스의 단일 인스턴스를 유지할 수 있습니다. 데이터베이스는 만들고 유지하는 데 비용이 많이 듭니다.
@Volatile
에 INSTANCE
주석을 답니다. 휘발성 변수의 값은 캐시되지 않고 모든 쓰기와 읽기는 기본 메모리에서 실행됩니다. 이렇게 하면 INSTANCE
값이 항상 최신 상태로 유지되고 모든 실행 스레드에서 같은지 확인할 수 있습니다. 즉, 한 스레드에서 INSTANCE
를 변경하면 다른 모든 스레드에 즉시 표시됩니다.
@Volatile
private var INSTANCE: ItemRoomDatabase? = null
INSTANCE
아래에서(여전히companion
객체 내) 데이터베이스 빌더에 필요한Context
매개변수를 사용하여getDatabase()
메서드를 정의합니다.ItemRoomDatabase
유형을 반환합니다.getDatabase()
에서 아직 아무것도 반환하지 않아 오류가 표시됩니다.
fun getDatabase(context: Context): ItemRoomDatabase {}
- 여러 스레드가 잠재적으로 경합 상태로 실행되고 동시에 데이터베이스 인스턴스를 요청하여 하나가 아닌 두 개의 데이터베이스가 생성될 수 있습니다. 코드를 래핑하여
synchronized
블록 내에 데이터베이스를 가져오면 한 번에 한 실행 스레드만 이 코드 블록에 들어갈 수 있으므로 데이터베이스가 한 번만 초기화됩니다.
getDatabase()
내에서 INSTANCE
변수를 반환하거나 INSTANCE
가 null이면 synchronized{}
블록 내에서 초기화합니다. elvis 연산자(?:
)를 사용하면 됩니다. 함수 블록 내에서 잠그려는 컴패니언 객체를 this
에 전달합니다. 다음 단계에서 오류를 수정합니다.
return INSTANCE ?: synchronized(this) { }
- synchronized 블록 내에서
val
인스턴스 변수를 만들고 데이터베이스 빌더를 사용하여 데이터베이스를 가져옵니다. 여전히 발생하는 오류는 다음 단계에서 수정합니다.
val instance = Room.databaseBuilder()
synchronized
블록 끝에instance
를 반환합니다.
return instance
synchronized
블록 내에서instance
변수를 초기화하고 데이터베이스 빌더를 사용하여 데이터베이스를 가져옵니다. 애플리케이션 컨텍스트, 데이터베이스 클래스, 데이터베이스 이름item_database
를Room.databaseBuilder()
에 전달합니다.
val instance = Room.databaseBuilder(
context.applicationContext,
ItemRoomDatabase::class.java,
"item_database"
)
Android 스튜디오에서 유형 불일치 오류가 발생합니다. 이 오류를 삭제하려면 다음 단계에서 이전 전략과 build()
를 추가해야 합니다.
- 필요한 이전 전략을 빌더에 추가합니다.
.fallbackToDestructiveMigration()
을 사용합니다.
일반적으로 스키마 변경 시점에 관한 이전 전략과 함께 이전 객체를 제공해야 합니다. 이전 객체는 데이터가 손실되지 않도록 이전 스키마의 모든 행을 가져와 새 스키마의 행으로 변환하는 방법을 정의하는 객체입니다. 이전은 이 Codelab의 범위를 벗어납니다. 간단한 해결 방법은 데이터베이스를 제거했다가 다시 빌드하는 것입니다. 즉, 데이터가 손실됩니다.
.fallbackToDestructiveMigration()
- 데이터베이스 인스턴스를 만들려면
.build()
를 호출합니다. 이렇게 하면 Android 스튜디오 오류가 삭제됩니다.
.build()
synchronized
블록 내에서INSTANCE = instance
를 할당합니다.
INSTANCE = instance
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
}
}
}
}
- 코드를 빌드하여 오류가 없는지 확인합니다.
Application 클래스 구현
이 작업에서는 Application 클래스에서 데이터베이스 인스턴스를 인스턴스화합니다.
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
이 있으면 됩니다.
8. ViewModel 추가
지금까지 데이터베이스를 만들었고 UI 클래스는 시작 코드의 일부였습니다. 앱의 임시 데이터를 저장하고 데이터베이스에도 액세스하려면 ViewModel이 있어야 합니다. 인벤토리 ViewModel은 DAO를 통해 데이터베이스와 상호작용하여 UI에 데이터를 제공합니다. 모든 데이터베이스 작업은 기본 UI 스레드에서 벗어나 실행되어야 하고 코루틴과 viewModelScope
를 사용하면 됩니다.
인벤토리 ViewModel 만들기
com.example.inventory
패키지에서 Kotlin 클래스 파일InventoryViewModel.kt
를 만듭니다.ViewModel
클래스에서InventoryViewModel
클래스를 확장합니다.ItemDao
객체를 매개변수로 기본 생성자에 전달합니다.
class InventoryViewModel(private val itemDao: ItemDao) : ViewModel() {}
- 클래스 외부에서
InventoryViewModel.kt
파일 끝에InventoryViewModelFactory
클래스를 추가하여InventoryViewModel
인스턴스를 인스턴스화합니다.ItemDao
인스턴스인InventoryViewModel
과 동일한 생성자 매개변수를 전달합니다.ViewModelProvider.Factory
클래스에서 클래스를 확장합니다. 다음 단계에서 구현되지 않은 메서드와 관련된 오류를 수정합니다.
class InventoryViewModelFactory(private val itemDao: ItemDao) : ViewModelProvider.Factory {
}
- 빨간색 전구를 클릭하고 멤버 구현을 선택하거나 다음과 같이
ViewModelProvider.Factory
클래스 내에서create()
메서드를 재정의할 수 있습니다. 그러면 클래스 유형을 인수로 사용하여ViewModel
객체를 반환합니다.
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
TODO("Not yet implemented")
}
create()
메서드를 구현합니다.modelClass
가InventoryViewModel
클래스와 같은지 확인하고 그 인스턴스를 반환합니다. 같지 않으면 예외가 발생합니다.
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
)
항목을 데이터베이스에 추가하려면 특정 항목의 이름과 가격, 사용할 수 있는 재고가 필요합니다. Codelab의 뒷부분에서 Add Item 화면을 사용하여 사용자에게서 이러한 세부정보를 가져옵니다. 현재 작업에서는 세 가지 문자열을 ViewModel의 입력으로 사용하고 이를 Item
항목 인스턴스로 변환한 후 ItemDao
인스턴스를 사용하여 데이터베이스에 저장합니다. 이제 구현해보겠습니다.
InventoryViewModel
클래스에서Item
객체를 가져오고 비차단 방식으로 데이터를 데이터베이스에 추가하는insertItem()
이라는private
함수를 추가합니다.
private fun insertItem(item: Item) {
}
- 기본 스레드 밖에서 데이터베이스와 상호작용하려면 코루틴을 시작하고 그 안에서 DAO 메서드를 호출합니다.
insertItem()
메서드 내에서viewModelScope.launch
를 사용하여ViewModelScope
에서 코루틴을 시작합니다. 시작 함수 내에서item
을 전달하는itemDao
에서 정지 함수insert()
를 호출합니다.ViewModelScope
는ViewModel
이 소멸될 때 하위 코루틴을 자동으로 취소하는ViewModel
클래스의 확장 속성입니다.
private fun insertItem(item: Item) {
viewModelScope.launch {
itemDao.insert(item)
}
}
다음을 가져옵니다. kotlinx.coroutines.launch,
androidx.lifecycle.
viewModelScope
com.example.inventory.data.Item
(자동으로 가져오지 않는 경우)
InventoryViewModel
클래스에서 문자열 세 개를 가져오고Item
인스턴스를 반환하는 또 다른 비공개 함수를 추가합니다.
private fun getNewItemEntry(itemName: String, itemPrice: String, itemCount: String): Item {
return Item(
itemName = itemName,
itemPrice = itemPrice.toDouble(),
quantityInStock = itemCount.toInt()
)
}
- 여전히
InventoryViewModel
클래스에서 항목 세부정보 문자열을 세 개 가져오는addNewItem()
이라는 공개 함수를 추가합니다. 항목 세부정보 문자열을getNewItemEntry()
함수에 전달하고 반환된 값을newItem
이라는 val에 할당합니다.newItem
을 전달하는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 프래그먼트를 업데이트합니다.
9. AddItemFragment 업데이트
AddItemFragment.kt
에서AddItemFragment
클래스 시작 부분에InventoryViewModel
유형의viewModel
이라는private val
을 만듭니다.by activityViewModels()
Kotlin 속성 위임을 사용하여 프래그먼트 전체에서ViewModel
을 공유합니다. 다음 단계에서 오류를 수정합니다.
private val viewModel: InventoryViewModel by activityViewModels {
}
- 람다 내에서
InventoryViewModelFactory()
생성자를 호출하고ItemDao
인스턴스를 전달합니다. 이전 작업 중 하나에서 만든database
인스턴스를 사용하여itemDao
생성자를 호출합니다.
private val viewModel: InventoryViewModel by activityViewModels {
InventoryViewModelFactory(
(activity?.application as InventoryApplication).database
.itemDao()
)
}
viewModel
정의 아래에Item
유형의item
이라는lateinit var
을 만듭니다.
lateinit var item: Item
- 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
}
AddItemFragment.kt
에서onCreateView()
함수 아래에Boolean
을 반환하는isEntryValid()
라는private
함수를 만듭니다. 다음 단계에서 누락된 반환 값 오류를 수정합니다.
private fun isEntryValid(): Boolean {
}
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()
)
}
isEntryValid()
함수 아래AddItemFragment
클래스에서addNewItem()
이라는 또 다른private
함수를 매개변수 없이 추가하고 아무것도 반환하지 않습니다. 함수 내에서if
조건 내에isEntryValid()
를 호출합니다.
private fun addNewItem() {
if (isEntryValid()) {
}
}
if
블록 내viewModel
인스턴스에서addNewItem()
메서드를 호출합니다. 사용자가 입력한 항목 세부정보를 전달하고binding
인스턴스를 사용하여 읽습니다.
if (isEntryValid()) {
viewModel.addNewItem(
binding.itemName.text.toString(),
binding.itemPrice.text.toString(),
binding.itemCount.text.toString(),
)
}
if
블록 아래에ItemListFragment
로 다시 이동하는val
action
을 만듭니다.findNavController
().navigate()
를 호출하여action
을 전달합니다.
val action = AddItemFragmentDirections.actionAddItemFragmentToItemListFragment()
findNavController().navigate(action)
androidx.navigation.fragment.findNavController.
를 가져옵니다.
- 완료된 메서드는 다음과 같이 표시됩니다.
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)
}
}
- 모든 것을 연결하려면 클릭 핸들러를 Save 버튼에 추가합니다.
AddItemFragment
클래스의onDestroyView()
함수 위에서onViewCreated()
함수를 재정의합니다. onViewCreated()
함수에서 클릭 핸들러를 저장 버튼에 추가하고addNewItem()
을 호출합니다.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.saveAction.setOnClickListener {
addNewItem()
}
}
- 앱을 빌드하고 실행합니다. + Fab를 탭합니다. Add Item 화면에서 항목 세부정보를 추가하고 Save를 탭합니다. 이 작업으로 데이터가 저장되지만 아직 앱에는 아무것도 표시되지 않습니다. 다음 작업에서 Database Inspector를 사용하여 저장한 데이터를 확인합니다.
Database Inspector를 사용하여 데이터베이스 보기
- API 수준 26 이상을 실행하는 에뮬레이터 또는 연결된 기기에서 앱을 실행합니다(아직 실행하지 않은 경우). Database Inspector는 API 수준 26을 실행하는 에뮬레이터/기기에서 가장 잘 작동합니다.
- Android 스튜디오의 메뉴 바에서 View > Tool Windows > Database Inspector를 선택합니다.
- Database Inspector 창의 드롭다운 메뉴에서
com.example.inventory
를 선택합니다. - Inventory 앱의 item_database가 Databases 창에 표시됩니다. item_database의 노드를 확장하고 검사할 Item을 선택합니다. Databases 창이 비어 있으면 에뮬레이터에서 Add Item 화면을 사용하여 항목을 데이터베이스에 추가합니다.
- Database Inspector에서 Live updates 체크박스를 선택하여 에뮬레이터나 기기에서 실행 중인 앱과 상호작용할 때 표시되는 데이터를 자동으로 업데이트합니다.
축하합니다. Room을 사용하여 데이터를 유지할 수 있는 앱을 만들었습니다. 다음 Codelab에서는 RecyclerView
를 앱에 추가하여 데이터베이스에 항목을 표시하고 항목 삭제 및 업데이트와 같은 새로운 기능을 앱에 추가합니다. 다음 Codelab에서 뵙겠습니다!
10. 솔루션 코드
이 Codelab의 솔루션 코드는 아래와 같이 GitHub 저장소와 분기에 있습니다.
이 Codelab의 코드를 가져와서 Android 스튜디오에서 열려면 다음을 실행합니다.
코드 가져오기
- 제공된 URL을 클릭합니다. 브라우저에서 프로젝트의 GitHub 페이지가 열립니다.
- 프로젝트의 GitHub 페이지에서 Code 버튼을 클릭하여 대화상자를 엽니다.
- 대화상자에서 Download ZIP 버튼을 클릭하여 컴퓨터에 프로젝트를 저장합니다. 다운로드가 완료될 때까지 기다립니다.
- 컴퓨터에서 파일을 찾습니다(예: Downloads 폴더).
- ZIP 파일을 더블클릭하여 압축을 해제합니다. 프로젝트 파일이 포함된 새 폴더가 만들어집니다.
Android 스튜디오에서 프로젝트 열기
- Android 스튜디오를 시작합니다.
- Welcome to Android Studio 창에서 Open an existing Android Studio project를 클릭합니다.
참고: Android 스튜디오가 이미 열려 있는 경우 File > New > Import Project 메뉴 옵션을 대신 선택합니다.
- Import Project 대화상자에서 압축 해제된 프로젝트 폴더가 있는 위치로 이동합니다(예: Downloads 폴더).
- 프로젝트 폴더를 더블클릭합니다.
- Android 스튜디오가 프로젝트를 열 때까지 기다립니다.
- Run 버튼 을 클릭하여 앱을 빌드하고 실행합니다. 예상대로 작동하는지 확인합니다.
- Project 도구 창에서 프로젝트 파일을 둘러보고 앱이 설정된 방식을 확인합니다.
11. 요약
- 테이블을
@Entity
주석이 달린 데이터 클래스로 정의합니다.@ColumnInfo
주석이 달린 속성을 테이블의 열로 정의합니다. - 데이터 액세스 객체(DAO)를
@Dao
주석이 달린 인터페이스로 정의합니다. DAO는 Kotlin 함수를 데이터베이스 쿼리에 매핑합니다. - 주석을 사용하여
@Insert
,@Delete
,@Update
함수를 정의합니다. - SQLite 쿼리 문자열과 함께
@Query
주석을 다른 쿼리의 매개변수로 사용합니다. - Database Inspector를 사용하여 Android SQLite 데이터베이스에 저장된 데이터를 확인합니다.
12. 자세히 알아보기
Android 개발자 문서
블로그 게시물
동영상
기타 문서 및 도움말