본문 바로가기
Android App/Kotlin

데이터베이스-ORM(Object-Relational Mapping) 라이브러리 Room사용

by AppJinny 2022. 11. 25.

*데이터베이스-ORM라이브러리 Room사용

*ORM(Object-Relational Mapping)

-객체(Class)와 관계형 데이터베이스의 데이터(테이블)를 매핑하고 변환하는 기술

-코드의 클래스파일에 ORM적용 시 자동으로 쿼리로 변환해 테이블 생성 가능

-쿼리를 잘 몰라도 코드만드로 데이터베이스의 모든 것을 컨트롤할 수 있음

-안드로이드는 SQLite를 코드 관점에서 접근할 수 있도록 ORM라이브러리 Room을 제공

 

*Room 

-데이터베이스에 읽고 쓰는 메서드를 인터페이스 형태로 설계하고 사용

-코드없이 이름만 명시하는 형태로 인터페이스를 만들면 Room이 나머지 코드 자동 생성함

-Room을 사용 시 클래스명이나 변수명 위에 @어노테이션을 사용해 코드로 변환함

 

*kapt

-plugins {} 에 id 'kotlin-kapt' 추가하여 사용 설정함

-자바6부터 도입된 Pluggable Annotation Processing API를 코틀린에서의 사용

-Annotation Processing(어노테이션 프로세싱) : @명령어 처럼 사용하는 주석 형태의 문자열을 실제 코드로 생성해 주는 것

-Annotation(어노테이션) : @으로 시작하는 명령어, 컴파일 시 코드로 생성되기 때문에 처리속도를 빠르게 할 수 있음

-Room은 빠른 처리 속도를 위해 어노테이션 프로세서 사용

-코틀린에서는 어노테이션 프로세서 대신에 kapt를 사용하기 때문에  kapt 플러그인 추가

-Room버전 확인: https://developer.android.com/jetpack/androidx/releases/room#groovy

 

*DAO(Data Access Object):

-데이터베이스에 접근해서 DML쿼리를 실행하는 메서드 모음

-인터페이스를 만들어 @Dao로 사용하여 DML쿼리를 명시하고 메서드 생성해서 사용

-DML쿼리: SELECT, INSERT, UPDATE, DELETE

 

*@(어노테이션)의 종류

어노테이션 종류

*@Database 어노테이션 속성

-entities : Room라이브러리가 사용할 엔터티(테이블) 클래스 목록

-version : 데이터베이스의 버전

-exportSchema : true설정 시 스키마 정보 파일로 출력

-스키마 : 데이터베이스의 구조와 제약 조건에 관한 전반적인 명세를 기술한 메타데이터의 집합
--스키마는 데이터베이스를 구성하는 데이터 개체(Entity), 속성(Attribute), 관계(Relationship) 및 데이터 조작 시 데이터 값들이 갖는 제약 조건 등에 관해 전반적으로 정의함

 

*ORM기술, Room라이브러리 사용

-build.gradle

-- android{} 에 뷰바인딩 추가

--plugins{} 에  kapt 플러그인 추가

--dependencies{} 에 Room라이브러리 추가

--Room버전 확인: https://developer.android.com/jetpack/androidx/releases/room#groovy

//뷰바인딩
buildFeatures{
    viewBinding true
}
//1
//kapt사용 설정
//kapt: 자바6부터 도입된 Pluggable Annotation Processing API를 코틀린에서의 사용
//Annotation Processing(어노테이션 프로세싱): @명령어 처럼 사용하는 주석 형태의 문자열을
//실제 코드로 생성해 주는 것
//어노테이션: @으로 시작하는 명령어, 컴파일 시 코드로 생성되기 때문에 발생할 수 있는 성능문제 개선됨
//빠른 처리속도를 위한 것

//코틀린에서는 어노테이션 프로세서 대신 kapt 사용하기 때문에 kapt 플러그인 추가
id 'kotlin-kapt'
//2
//Room라이브러리 추가
//Room은 빠른 처리 속도를 위해 어노테이션 프로세서를 사용하는데
//코틀린에서는 이것 대신 kapt 사용
//kapt사용하기 위해서는 kapt플러그인이 먼저 추가되어 있어야 함
//Room을 사용하면 클래스명이나 변수명 위에 @어노테이션을 사용해서 코드로 변환할 수 있음
//Room버전 확인: https://developer.android.com/jetpack/androidx/releases/room#groovy
def room_version = "2.4.3"

implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"

// To use Kotlin annotation processing tool (kapt)
kapt "androidx.room:room-compiler:$room_version"

 

-MainActivity.kt

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.room.Room
import com.heeyjinny.room.databinding.ActivityMainBinding

//1
//ORM의 Room라이브러리 사용하기
//app - java 밑 패키지명 우클릭 - New - Kotlin Class/File 생성
//RoomMemo.kt 클래스 생성

class MainActivity : AppCompatActivity() {

    //뷰바인딩
    val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }

    //2
    //helper변수를 생성하여 RoomHelper를 사용할 수 있도록 함
    var helper: RoomHelper? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //뷰바인딩
        setContentView(binding.root)

        //2-1
        //RoomHelpe를 사용할 수 있는 변수 helper생성
        //Room라이브러리의 databaseBuilder 속성 사용
        //databaseBuilder의 세 번째 파라미터는 실제 생성되는 DB파일의 이름
        //Room은 기본적으로 서브 스레드에서 동작하도록 설계되어 있어서
        //메인 스레드에서 동작하도록 만들어줘야함
        //이전에 메인스레드에서 동작할 수 있도록 만드는 코드 찾아와 적용하기...ㅠㅠ
        //지금은 allowMainThreadQueries()옵션을 사용하지만 권장되지 않음
        helper = Room.databaseBuilder(this, RoomHelper::class.java, "room_memo")
            .allowMainThreadQueries().build()


        //3
        //어댑터 생성
        val adapter = RecyclerAdapter()

        //8
        //데이터 삭제를 위해 생성해둔 helper를 어댑터에 전달
        //RecyclerAdapter.kt에 helper프로퍼티 생성...
        adapter.helper = helper

        //4
        //어댑터에있는 목록(listData)에서 조회하여(select) 가져온 데이터 세팅
        //helper에 null이 허용되므로 ?. 의 형태로 사용
        //roomMemoDao() 와 Dao의 메서드 getAll()도 null허용 ?. 사용하고
        //adapter의 listData에는 null이 허용되지 않기 때문에 앞의2개가 null일 경우 사용하는
        //엘비스 연산자 ?: 를 사용하여 디폴트 값 설정함
        adapter.listData.addAll(helper?.roomMemoDao()?.getAll()?: listOf())

        //5
        //리사이블러뷰 위젯에 어댑터를 연결하고 레이아웃 매니저 설정
        binding.recyclerMemo.adapter = adapter
        binding.recyclerMemo.layoutManager = LinearLayoutManager(this)

        //6
        //리사이클러뷰 아이템 목록에 있는 저장버튼에 클릭이벤트 설정
        binding.buttonSave.setOnClickListener {

            //6-1
            //조건식을 사용하여 메모입력 위젯인 EditText에 값이 있으면 해당 내용으로 Memo클래스 생성
            if (binding.editText.text.toString().isNotEmpty()){

                //6-2
                //입력한 텍스트 값이 있다면 RoomMemo클래스를 생성해 파라미터로 값을 전달하고 변수에 저장
                val memo = RoomMemo(binding.editText.text.toString(), System.currentTimeMillis())
                //6-3
                //helper클래스의 roomMemoDao()메서드에 변수memo의 값을 삽입해(insert) 데이터베이스에 저장
                helper?.roomMemoDao()?.insert(memo)
                //6-4
                //저장이 끝났으면 어댑터의 데이터 모두 초기화
                adapter.listData.clear()
                //6-5
                //데이터베이스에서 새로운 목록을 읽어와 어댑터에 다시 세팅하고 갱신(새로고침 개념...)
                //새로 생성되는 메모에는 번호가 자동 입력되므로 번호를 갱신하기 위해 새로운 데이터를 세팅함
                adapter.listData.addAll(helper?.roomMemoDao()?.getAll() ?: listOf())
                //6-6
                //어댑터의 세팅이 끝났다는 것(데이터가 변경되었다는 것) 알려주고 갱신
                adapter.notifyDataSetChanged()
                //6-7
                //editText위젯에 써있는 텍스트 내용 지워서(빈 칸으로) 초기화
                binding.editText.setText("")

            }

        }//클릭리스너...

        //7
        //메모 목록에 삭제버튼 추가하여 메모 삭제 기능 구현
        //item_recycler.xml 수정...

    }//onCreate
}//ManinActivity

 

-RoomMemo.kt

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey

//1
//@Entity 어노테이션 적용: 정의된 class 위에 사용
//@Entity: Room라이브러리는 @Entity이 적용된 클래스를 찾아 테이블로 변환
//@Entity(tableName = "테이블명"): 테이블명을 클래스명과 다르게 하고 싶을 때 옵션
//RoomMemo클래스의 내용을 테이블명이 room_memo인 테이블로 생성하여 변환
@Entity(tableName = "room_memo")
class RoomMemo {

    //2
    //테이블의 컬럼인 num, content, date를 멤버변수로 선언하고 위에
    //@ColumnInfo 어노테이션 작성
    //@ColumnInfo: 테이블의 컬럼명으로 사용
    //@ColumnInfo(name = "컬럼명")테이블의 컬럼명을 변수명과 다르게 하고 싶을 때 옵션

    //3
    //num변수에 키(Key)라는 점을 명시하고 자동증가 옵션 추가
    //@PrimaryKey(autoGenerate = true)
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo
    var num: Long? = null

    @ColumnInfo
    var content: String = ""

    @ColumnInfo(name = "date")
    var datetime: Long = 0

    //4
    //content와 datetime을 받는 생성자 작성
    //num은 값이 자동증가되므로 생성자로 내용을 받지 않아도 됨
    constructor(content: String, datetime: Long) {
        //4-1
        //이 클래스에 있는(this) 변수 content와 datetime에 생성자로 받은 값넣기
        this.content = content
        this.datetime = datetime
    }

    //5
    //만약 변수를 테이블의 컬럼으로 사용하고 싶지 않을 때
    //@Ignore: 해당 변수가 테이블과 관계없는 변수라는 정보를 알림
    @Ignore
    var temp: String = "임시로 사용되는 데이터"

    //6
    //DAO(Data Access Object): 데이터베이스에 접근해서 DML쿼리를 실행하는 메서드 모음
    //DML쿼리: SELECT, INSERT, UPDATE, DELETE
    //RoomMemoDAO 인터페이스 정의하기
    //app - java밑 패키지 우클릭 - New - Kotlin Class/File
    //Interface선택 - RoomMemoDao 생성

}//class RoomMomo

 

-RoomMemoDao.kt

import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy.REPLACE
import androidx.room.Query

//1
//DAO(Data Access Object): 데이터베이스에 접근해서 DML쿼리를 실행하는 메서드 모음
//DML쿼리: SELECT, INSERT, UPDATE, DELETE
//RoomMemoDAO 인터페이스에 @Dao(쿼리메서드모음) 명시
@Dao
interface RoomMemoDao {
    //2
    //조회, 삽입수정, 삭제 3개 메서드 생성 후 각 어노테이션 붙임

    //2-1
    //조회메서드 생성
    //다른 ORM툴과 다르게 조회하는 select쿼리는 직접 작성해야함
    //그래서 @Query어노테이션을 사용해 직접 작성
    //조회한다(select) 모든내용을(*) room_memo에 있는(from)
    //room_memo안에 있는 테이블의 모든 내용을 RoomMemo클래스의 배열타입을 가지는 메서드 getAll()생성
    @Query("select * from room_memo")
    fun getAll(): List<RoomMemo>

    //2-2
    //삽입, 수정 메서드 생성
    //onConflict = REPLACE 옵션 적용: 동일한 키를 가진 값이 입력되었을 때 덮어쓰기(Update,수정됨)
    //REPLACE 옵션 적용 시 import할 때 androidx.room 패키지로 선택함
    @Insert(onConflict = REPLACE)
    fun insert(memo: RoomMemo)

    //2-3
    //삭제 메서드 생성
    @Delete
    fun delete(memo: RoomMemo)

    //3
    //RoomHelper 클래스 정의
    //SQLiteOpenHelper()를 상속받아 구현했던 것 처럼 Room도 유사한 구조로 사용
    //RoomDatabase()를 상속받아 클래스 생성하여 사용함
    //상속받아서 클래스 생성 시 주의할점은 추상클래스(abstract)로 생성해야 함
    //app - java밑 클래스명 우클릭 - New - Kotlin Class/File -
    //Class클릭 - RoomHelper클래스 생성

}

 

-RoomHelper.kt

import androidx.room.Database
import androidx.room.RoomDatabase

//1
//RoomHelper 클래스 정의하기
//SQLiteOpenHelpe())를 상속받아 구현했던 것 처럼 Room도 유사한 구조로 사용
//RoomDatabase()를 상속받아 클래스 생성하여 사용함
//상속받아서 클래스 생성 시 주의할점은 추상클래스(abstract)로 생성해야 함

//RoomDatabase를 상속받는 추상클래스 RoomHelper 클래스 생성하고
//@Database 어노테이션 작성
//@Database의 속성
//entities: Room라이브러리가 사용할 엔터티(테이블) 클래스 목록
//version: 데이터 베이스의 버전
//exportSchema: true사용 시 스키마 정보를 파일로 출력함
@Database(entities = arrayOf(RoomMemo::class), version = 1, exportSchema = false)
abstract class RoomHelper: RoomDatabase() {

    //2
    //RoomMemoDao 인터페이스 구현하는 메서드 생성
    //인터페이스 구현하는 메서드만 작성해도 Room라이브러리를 통해 미리 만들어져있는
    //코드를 사용할 수 있음
    abstract fun roomMemoDao(): RoomMemoDao

    //3
    //화면에 보여질 뷰 작성을 위해
    //activity_main.xml, item_recycler.xml 작성 후
    //어댑터 클래스 생성...RecyclerAdapter
    
}

 

-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"
    android:padding="16dp"
    tools:context=".MainActivity">

<!--  리사이클러뷰 생성하여 화면 구성하기  -->
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerMemo"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_marginBottom="8dp"
        app:layout_constraintBottom_toTopOf="@+id/editText"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

<!--  메모 입력 창, 버튼 생성  -->
    <EditText
        android:id="@+id/editText"
        android:layout_width="0dp"
        android:layout_height="65sp"
        android:hint="메모를 입력하세요"
        android:inputType="textMultiLine"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/buttonSave"
        app:layout_constraintStart_toStartOf="parent" />

    <Button
        android:id="@+id/buttonSave"
        android:layout_width="wrap_content"
        android:layout_height="65dp"
        android:text="저장"
        android:backgroundTint="@color/teal_200"
        android:textSize="20sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/editText" />

<!--  리사이클러뷰 아이템 용도로 사용할 레이아웃 파일 생성  -->
<!--  app - res - layout 우클릭 - Layout Resource File  -->
<!--  item_recycler.xml 생성  -->

</androidx.constraintlayout.widget.ConstraintLayout>

 

-item_recycler.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="wrap_content"
    android:padding="8dp">

<!-- 데이터베이스에 저장할 번호, 내용, 시간 표시할 텍스트뷰 배치  -->
    <TextView
        android:id="@+id/textNum"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="32dp"
        android:text="01"
        android:textSize="16sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />


<!--  android:maxLines="2" : 텍스트뷰의 줄 제한 2줄  -->
<!--  android:ellipsize="end" : 줄 제한이 넘어가면 말줄임표 속성(...) -->
<!--  FIN. macLine, ellipsize 삭제하여 전체내용 보여주기  -->
    <TextView
        android:id="@+id/textContent"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:gravity="center_vertical"
        android:padding="16sp"
        android:text="메모 내용 표시"
        android:layout_marginBottom="32dp"
        app:layout_constraintEnd_toStartOf="@+id/btnDelete"
        app:layout_constraintStart_toEndOf="@+id/textNum"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/textDateTime"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="2022/01/01 08:40"
        android:layout_marginTop="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textContent"/>

<!--  배치 후 소스코드 연결하기  -->
<!--  리사이클러뷰와 액티비티 연결을 위한 어댑터 클래스 만들기  -->
<!--  app - java 밑 패키지 우클릭 - 클래스생성  -->
<!--  RecyclerAdapter.kt 클래스 생성  -->


<!--  삭제 버튼 생성  -->
    <Button
        android:id="@+id/btnDelete"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_marginEnd="16dp"
        android:text="삭제"
        android:gravity="center"
        android:layout_marginBottom="32dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

<!--  삭제버튼을 눌러서 RoomMemo의 데이터와 어댑터에있는 Memo컬렉션의 데이터 삭제  -->
<!--  MainActivity.kt 에서 코드 추가  -->

</androidx.constraintlayout.widget.ConstraintLayout>

 

-RecyclerAdapter.kt

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.heeyjinny.room.databinding.ItemRecyclerBinding
import java.text.SimpleDateFormat

//리사이클러뷰 어댑터를 상속받는 어댑터클래스 생성
//리사이클러뷰 목록만들기 과정과 동일함...
//Holder클래스 inner클래스로 어댑터 클래스 안에서 생성하기만 바뀜...
//2
class RecyclerAdapter: RecyclerView.Adapter<RecyclerAdapter.Holder>() {

    //8
    //데이터 삭제 구현을 위해 생성하는 helper프로퍼티 생성하고
    //Holder클래스에 클릭리스너 달기...
    var helper: RoomHelper? = null

    //3
    var listData = mutableListOf<RoomMemo>()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
        //4
        val binding = ItemRecyclerBinding.inflate(LayoutInflater.from(parent.context),parent,false)

        return Holder(binding)
    }

    override fun onBindViewHolder(holder: Holder, position: Int) {
        //6
        val memo = listData.get(position)
        holder.setRoomMemo(memo)

    }

    override fun getItemCount(): Int {
        //3
        return listData.size
    }


    //1
    //10
    //삭제버튼을 클릭하면 어댑터의 helper와 listData에 접근해야함
    //어댑터 클래스의 멤버변수인 helper와 listData에 접근을 위해
    //Holder클래스 전체를 어댑터 클래스 안에 옮기고 class앞에 inner키워드를 붙여줌
    inner class Holder(val binding: ItemRecyclerBinding): RecyclerView.ViewHolder(binding.root){

        //11
        //홀더는 한 화면에 그려지는 개수만큼 만든 후 재사용함 그래서
        //클릭하는 시점에 어떤 데이터가 있는지 알아야 하므로 변수 선언 후 setRoomMemo()를 통해 넘어온 RoomMemo임시 저장
        var mRoomMemo: RoomMemo? = null

        //9
        //삭제버튼을 누르면 반응하는 메서드 만들기
        init {
            binding.btnDelete.setOnClickListener {
                //12
                //roomMemo데이터를 먼저 삭제하고 listData데이터도 삭제
                //mRoomMemo는 null허용설정이 되었기 때문에 !!강제함
                //RoomHelper를 사용할 때 여러개의 Dao가 있을 수 있기 때문에
                //헬퍼?.Dao()?.메서드() 형태로 가운데에 어떤 Dao를 쓸 건지 명시해야함
                helper?.roomMemoDao()?.delete(mRoomMemo!!)
                listData.remove(mRoomMemo)

                //12-1
                //어댑터 갱신
                notifyDataSetChanged()

            }

        }//init

        //13
        //메모내욜 전체보기를 위한 xml 약간 수정...
        //item_recycler.xml

        //5
        fun setRoomMemo(memo: RoomMemo){
            binding.textNum.text = "${memo.num}"
            binding.textContent.text = memo.content
            val sdf = SimpleDateFormat("yyyy/MM/dd hh:mm:ss")
            binding.textDateTime.text = sdf.format(memo.datetime).toString()

            //11-1
            //setRoomMemo()를 통해 넘어온 RoomMemo임시 저장
            this.mRoomMemo = memo

        }

    }//Holder

}//RecyclerAdapter

//7
//MainActivity.kt 에서 모든 코드를 조합해 동작가능하게 작성

 

-결과

 

 

 


이 포스팅에 작성한 내용은 고돈호, ⌜이것이 안드로이드다⌟, 한빛미디어(주), 2022 에서 발췌하였습니다.