# Room이란?
Room은 스마트폰 내장 DB에 데이터를 저장하기 위해 사용하는 라이브러리이다.
다양한 Annotation을 통해 컴파일 시 코드들을 자동으로 만들어주며 LiveData, RxJava와 같은 Observation 형태를 지원하고 MVP, MVVM과 같은 아키텍처 패턴에 쉽게 활용할 수 있도록 되어있다.
과거에는 SQLite라는 데이터베이스 엔진을 이용해 데이터를 저장했으나 안드로이드 공식 문서에서는 다음과 같은 이유로 Room사용을 지향 하고 있다

Room은 완전히 새로운 개념은 아니고, SQLite를 활용해서 객체 매핑을 해주는 역할을 한다.
# Room 구조

Room은 위에 사진처럼 Database,Entity,Dao(Data Access Object) 3가지 구성요소가 있다.
# 사용법
이제부터 Room을 활용한 간단한 사람 메모 앱 예제를 하면서 살펴보자

이름, 나이, 전화번호를 입력하고 추가하기를 누르면 아래 RecyclerView에 추가가 되고 특정 이름을 입력하고 삭제를 하면 RecyclerView에 삭제가 되는 간단한 앱이다.
# gradle추가
plugins{
id 'kotlin-kapt'
}
dependencies {
implementation 'androidx.room:room-runtime:2.4.1'
kapt 'androidx.room:room-compiler:2.4.1'
}
# Entity
DB 내의 Table, 즉 DB에 저장할 데이터 형식으로 class의 변수들이 컬럼(column)이 되어 table이 된다.
테이블 예제
uid(PK) | name | age | phone |
1 | Kim | 23 | 010-1234-4567 |
2 | Lee | 25 | 010-7891-4561 |
3 | Yeom | 23 | 010-4567-1234 |
data class에 @Entity 어노테이션을 붙여주고 저장하고 싶은 속성의 변수 이름과 타입을 정해준다.
primaryKey는 키 값이기 때문에 유일한(Unique) 값이어야 한다. 직접 지정해도 되지만 autoGenerate를 true로 주면 자동으로 값을 생성한다.
import androidx.room.Entity
import androidx.room.PrimaryKey
//만약 테이블 이름을 정해주고 싶으면 아래와 같이
//@Entity(tableName = "userProfile")
@Entity
data class User(
// @PrimaryKey 기본키 직접 지정 방식
// var id : Int = 0
var name : String,
var age : String,
var Phone : String
){
// 기본키 자동 생성 방식
@PrimaryKey(autoGenerate = true) var id : Int = 0
}
- @Entity : Table 이름을 선언한다. 기본적으로 entity class 이름을 database table 이름으로 인식한다.
- @PrimaryKey : 각 entity 는 1개의 primary key를 가져야 한다.
# DAO
Data Access Objects의 줄임말이다.
데이터에 접근할 수 있는 메서드를 정의해놓은 인터페이스이다. (SQL 쿼리 지정 가능)
import androidx.room.*
@Dao
interface UserDao {
@Insert
fun insert(user : User)
@Update
fun update(user : User)
@Delete
fun delete(user: User)
@Query("SELECT * FROM User") // 테이블의 모든 값을 가져와라
fun getAll() : List<User>
@Query("DELETE FROM User WHERE name = :name") // 'name'에 해당하는 유저를 삭제해라
fun deleteUserByName(name : String)
@Query("DELETE FROM User") // 전체 테이블 값 삭제
fun deleteAll()
}
우선 class가 아니라 interface다
맨 위에 @Dao 어노테이션을 붙이고 그 안에 메서드를 정의하게 된다
- @Insert : 테이블에 데이터 삽입
- @Update : 테이블에 데이터 수정
- @Delete : 테이블에 데이터 삭제
- @Query : 삽입/수정/삭제 외에 다른 기능을 하는 메서드를 만들고 싶다면 @Query 사용해 어떤 동작을 할 건지 SQL 문법으로 작성하면 된다.
# Database
데이터베이스의 전체적인 소유자 역할, DB 생성 및 버전 관리를 한다.
@Database로 주석이 지정된 클래스는 다음 조건을 충족해야 한다.
1. RoomDatabase를 확장하는 추상 클래스여야 합니다.
2. 주석 내에 데이터베이스와 연결된 항목의 목록을 포함해야 합니다.
3. 인수가 0개이며 @Dao로 주석이 지정된 클래스를 반환하는 추상 메서드를 포함해야 합니다.
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
// entities : 이 DB에 어떤 테이블들이 있는지 명시
// version : Scheme가 바뀔 때 이 version도 바뀌어야 함
@Database(entities = [User::class], version = 1)
abstract class UserDB : RoomDatabase() {
abstract fun userDao():UserDao
companion object{
private var instance : UserDB? = null
@Synchronized
fun getInstance(context: Context):UserDB?{
if (instance == null) {
synchronized(UserDB::class){
instance = Room.databaseBuilder(
context.applicationContext,
UserDB::class.java,
"user-database"
).build()
}
}
return instance
}
}
}
공식문서에서는 데이터베이스 객체를 인스턴스 할 때 싱글톤으로 구현하기를 권장하고 있다.
그래서 위와 같이 companion object로 객체를 선언해서 사용하면 된다.
- @Database : class가 Database임을 알려주는 어노테이션
- @entities : 이 DB에 어떤 entity들이 있는지 명시
- @version : 앱을 업데이트하다가 entity의 구조를 변경해야 하는 일이 생겼을 때 이전 구조와 현재 구조를 구분해주는 역할
※ 이부분에 대해서는 나중에 자세하게 정리를 따로 해야 할 듯..
@Synchronized : 자바에서는 스레드를 동기화하기 위해서 synchronized를 제공한다.
(예를 들면, 자칫 여러 인스턴스를 생성될 수 있는 경우에 하나만 생성하도록 하기 위해 이 노테이션을 통해 방지한다.)
# activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
android:text="Android Room 테스트"
android:textColor="@color/purple_700"
android:textSize="30sp"
android:textStyle="bold"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:orientation="vertical"
android:padding="5dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView">
<EditText
android:id="@+id/name_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="이름 입력"
android:singleLine="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" />
<EditText
android:id="@+id/age_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="나이 입력"
android:singleLine="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" />
<EditText
android:id="@+id/phone_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="전화번호 입력"
android:singleLine="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/mainRV"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@+id/linearLayout"
app:layout_constraintBottom_toTopOf="@id/linearLayout2"
tools:listitem="@layout/rv_item"/>
<LinearLayout
android:id="@+id/linearLayout2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="5dp"
android:layout_marginBottom="5dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<Button
android:id="@+id/add_Btn"
android:layout_width="100dp"
android:layout_height="50dp"
android:layout_marginEnd="50dp"
android:background="#ECB8B8"
android:text="추가하기"
android:textSize="20sp"
android:textStyle="bold" />
<Button
android:id="@+id/delAll_Btn"
android:layout_width="100dp"
android:layout_height="50dp"
android:layout_marginEnd="50dp"
android:background="#ECB8B8"
android:text="전체삭제하기"
android:textSize="17sp"
android:textStyle="bold" />
<Button
android:id="@+id/del_Btn"
android:layout_width="100dp"
android:layout_height="50dp"
android:background="#ECB8B8"
android:text="삭제하기"
android:textSize="20sp"
android:textStyle="bold" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

이런 식으로 디자인을 할 것이다.
# rv_item
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="5dp">
<TextView
android:id="@+id/name_output"
android:layout_width="75dp"
android:layout_height="75dp"
android:layout_marginStart="20dp"
android:gravity="center"
android:text="name"
android:textSize="25sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/age_output"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:layout_marginStart="30dp"
android:text="age"
android:textSize="15sp"
app:layout_constraintStart_toEndOf="@+id/name_output"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/phone_output"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="39dp"
android:layout_marginStart="30dp"
android:text="phone"
android:textSize="15sp"
app:layout_constraintStart_toEndOf="@+id/name_output"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
표시할 RecyclerViewAdapter
# MainRvAdapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Adapter
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.example.android_room.R
import com.example.android_room.db.User
class MainRvAdapter : RecyclerView.Adapter<MainRvAdapter.ViewHolder>(){
var mData = listOf<User>()
inner class ViewHolder(itemView : View) : RecyclerView.ViewHolder(itemView){
private val itemName = itemView.findViewById<TextView>(R.id.name_output)
private val itemAge = itemView.findViewById<TextView>(R.id.age_output)
private val itemPhone = itemView.findViewById<TextView>(R.id.phone_output)
fun setData(data : User){
itemName.text = data.name
itemAge.text = data.age
itemPhone.text = data.Phone
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainRvAdapter.ViewHolder {
return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.rv_item,parent,false))
}
override fun onBindViewHolder(holder: MainRvAdapter.ViewHolder, position: Int) {
val item = mData[position]
holder.setData(item)
}
override fun getItemCount(): Int {
return mData.size
}
}
# MainActivity(데이터 베이스 사용)
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.EditText
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.android_room.adapter.MainRvAdapter
import com.example.android_room.db.User
import com.example.android_room.db.UserDB
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
private lateinit var db : UserDB
private lateinit var nameInput : EditText
private lateinit var ageInput : EditText
private lateinit var phoneInput : EditText
private lateinit var addBtn: Button
private lateinit var deleteBtn : Button
private lateinit var deleteAllBtn : Button
private lateinit var dataList: List<User>
private lateinit var mainRV : RecyclerView
private lateinit var rvAdapter: MainRvAdapter
private val TAG = "MainActivity"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// DB 생성
db = UserDB.getInstance(this)!!
nameInput = findViewById(R.id.name_input)
ageInput = findViewById(R.id.age_input)
phoneInput = findViewById(R.id.phone_input)
addBtn = findViewById(R.id.add_Btn)
mainRV = findViewById(R.id.mainRV)
rvAdapter = MainRvAdapter()
deleteBtn = findViewById(R.id.del_Btn)
deleteAllBtn = findViewById(R.id.delAll_Btn)
mainRV.apply {
setHasFixedSize(true)
adapter = rvAdapter
layoutManager = LinearLayoutManager(context,LinearLayoutManager.VERTICAL,false)
}
// DB 접근은 Main thread에서 불가하므로 CoroutineScope 사용해 처리 한다
CoroutineScope(Dispatchers.IO).launch {
// DB 읽어오고 Adapter 데이터에 추가 해주기
dataList = db.userDao().getAll()
rvAdapter.mData = dataList
}
// 추가 버튼
addBtn.setOnClickListener {
val name = nameInput.text.toString()
val age = ageInput.text.toString()
val phone = phoneInput.text.toString()
when {
nameInput.text.isNullOrBlank() -> {
Toast.makeText(this, "이름을 입력해주세요", Toast.LENGTH_SHORT).show()
}
ageInput.text.isNullOrBlank() -> {
Toast.makeText(this, "나이를 입력해주세요", Toast.LENGTH_SHORT).show()
}
phoneInput.text.isNullOrBlank() -> {
Toast.makeText(this, "전화번호를 입력해주세요", Toast.LENGTH_SHORT).show()
}
else -> {
CoroutineScope(Dispatchers.IO).launch {
// 입력한 값 읽어와서 삽입 해주기
db.userDao().insert(User(name, age, phone))
// DB에 있는 값 다 읽어와서 다시 Adapter 데이터에 추가 해주기
dataList = db.userDao().getAll()
rvAdapter.mData = dataList
}
// 마지막 위치에서 삽입이 되었다고 Adapter에 알려줘서 갱신 시키기
rvAdapter.notifyItemInserted(dataList.size)
nameInput.text.clear()
ageInput.text.clear()
phoneInput.text.clear()
}
}
}
// 삭제 버튼
deleteBtn.setOnClickListener {
val name = nameInput.text.toString()
if(nameInput.text.isEmpty())
{
Toast.makeText(this, "삭제하실 이름을 입력하세요", Toast.LENGTH_SHORT).show()
}
else {
CoroutineScope(Dispatchers.IO).launch {
db.userDao().deleteUserByName(name)
dataList = db.userDao().getAll()
rvAdapter.mData = dataList
}
rvAdapter.notifyDataSetChanged()
nameInput.text.clear()
}
}
// 전체삭제 버튼
deleteAllBtn.setOnClickListener {
CoroutineScope(Dispatchers.IO).launch {
db.userDao().deleteAll()
dataList = db.userDao().getAll()
rvAdapter.mData = dataList
}
rvAdapter.notifyDataSetChanged()
}
}
}
여기서 주의할 점은 DB 접근은 main thread에서 불가하므로 CoroutineScope를 사용해 비동기 처리를 해주는 것이다.
# 예제 영상

# GitHub
https://github.com/JiSeokYeom/Android_Room_EX.git
다음편에는 binding을 이용해 코드를 좀 더 줄여 보겠다.
참고 사이트
https://todaycode.tistory.com/39
https://developer.android.com/training/data-storage/room
https://velog.io/@ryalya/Android-DB-Room%EC%9D%B4%EB%9E%80
'안드로이드 공부 노트' 카테고리의 다른 글
안드로이드 Fragment LifeCycle(프래그먼트 생명 주기)알아보기! (0) | 2022.01.23 |
---|---|
Android Jetpack - 2편(View Binding) 예제를 이용한 사용법 (0) | 2022.01.22 |
안드로이드 디자인패턴 - MVC, MVP, MVVM 패턴 (0) | 2022.01.14 |
안드로이드 4대 컴포넌트 (1) | 2021.12.18 |
안드로이드 액티비티 생명주기 (Life Cycle) (0) | 2021.12.15 |