본문 바로가기
안드로이드 공부 노트

Android Jetpack - 1편 (Room) 예제를 이용한 사용법

by 지게요 2022. 1. 20.
728x90
반응형

# Room이란?

Room은 스마트폰 내장 DB에 데이터를 저장하기 위해 사용하는 라이브러리이다.

다양한 Annotation을 통해 컴파일 시 코드들을 자동으로 만들어주며 LiveData, RxJava와 같은 Observation 형태를 지원하고 MVP, MVVM과 같은 아키텍처 패턴에 쉽게 활용할 수 있도록 되어있다.

과거에는 SQLite라는 데이터베이스 엔진을 이용해 데이터를 저장했으나 안드로이드 공식 문서에서는 다음과 같은 이유로 Room사용을 지향 하고 있다

공식 문서에서 나온 SQLite의 문제점

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

 

# Room 구조

공식 문서에서 나온 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
반응형