Mobile/Kotlin

[kotlin] ORM라이브러리 Room

클리엘 2020. 12. 30. 16:17
728x90

ORM(Object Relational Mapping)는 객체(Class) 자체를 하나의 테이블로 매핑하여 객체를 조작하는 것으로 테이블의 데이터를 처리할 수 있도록 해주는 기술입니다.

 

DB를 조작하는 쿼리를 잘 모르더라도 객체를 대상으로 한 추가, 수정, 삭제동작을 그대로 DB의 테이블에 적용할 수 있으며 Android에서는 이런 ORM을 사용할 수 있도록 Room이라고 하는 라이브러리를 제공하고 있습니다.

 

우선 Room을 사용해 보기 위해 이전에 만들어봤던 전화번호앱을 다시 사용해 보겠습니다. 코드는 필요하지 않고 디자인만 가져올 것입니다.

 

[Mobile/Kotlin] - [kotlin] SQLite - 연결 및 사용하기

프로젝트에 ROOM라이브러리를 추가하기 위해 Gradle Scripts -> build.gradle(Module...) 파일을 연뒤 상단에 다음과 같은 내용을 추가합니다.

apply plugin: 'kotlin-kapt'

ROOM라이브러리를 사용하려면 kapt를 플러그인을 사용해야 합니다. kapt는 @로 시작하는 어노테이션을 코드로 생성할 수 있도록 해주는 것인데 나중에 보시면 알겠지만 테이블이나 칼럼 등을 생성할 때 @을 사용한 어노테이션을 사용하므로 kapt플러그인이 필요한 것입니다.

 

그리고 아래 dependencies 영역에 다음과 같이 Room라이브러리를 추가합니다.

def room_version = "2.2.5"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"

Room에 관한 자세한 설명은 아래 링크를 참고해 주세요. 예제에서 Room의 버전을 2.2.5로 지정하였는데 Room의 최신 버전도 아래 링크를 통해 확인할 수 있습니다.

 

https://developer.android.com/jetpack/androidx/releases/rooom 

 

AndroidX 출시  |  Android 개발자  |  Android Developers

AndroidX 출시 Jetpack 라이브러리는 Android OS와 별도로 제공되므로 라이브러리 업데이트가 독립적으로 더 자주 실행될 수 있습니다. 라이브러리는 엄격한 시맨틱 버전 관리를 따릅니다. 버전 문자열

developer.android.com

이제 Room에서 사용할 데이터 클래스를 만듭니다. 마치 하나의 테이블을 생성하는 것과 같다고 보시면 되겠습니다.

 

app -> java -> [패키지명]에서 마우스 오른쪽 버튼을 눌러 New -> Kotlin File/Class를 선택합니다.

 

이어지는 화면에서 Class를 선택하고 이름에 RoomPhoneBook를 입력합니다.

 

파일이 생성되면 클래스명 위에 @Entity을 추가합니다. 객체를 Table로 변환하는 것입니다.

package com.example.myapplication

import androidx.room.Entity

@Entity
class RoomPhoneBook {
}

Table의 이름은 클래스 이름을 따라가는데 만약 다른 이름의 테이블을 생성하고자 한다면 @Entity(tableName = "PhoneBook")처럼 테이블명을 지정해 주면 됩니다.

 

테이블을 정의했으니 이제 내부에 데이터 컬럼을 만들어 보죠. 키로 지정할 컬럼은 @PrimaryKey로 하고 값을 자동으로 생성하기 위해 autoGenerate=true를 지정합니다. 실제 컬럼은 @ColumnInfo를 사용하며 컬럼명은 아래 변수명을 따라갑니다. 만약 다른 이름의 컬럼을 생성하려고 하면 @ColumnInfo(name = "name")처럼 이름을 직접 지정할 수 있습니다.

package com.example.myapplication

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

@Entity
class RoomPhoneBook {
    @ColumnInfo
    @PrimaryKey(autoGenerate = true)
    var seq: Long? = null

    @ColumnInfo
    var name: String = ""

    @ColumnInfo
    var phone: String = ""
}

마지막으로 생성자를 추가합니다.

@Entity
class RoomPhoneBook {
    @ColumnInfo
    @PrimaryKey(autoGenerate = true)
    var seq: Long? = null

    @ColumnInfo
    var name: String = ""

    @ColumnInfo
    var phone: String = ""
    
    constructor(name: String, phone: String) {
        this.name = name
        this.phone = phone
    }
}

위에서 처럼 객체(Class)를 만들고 난 이후, 객체를 대상으로 실제 데이터를 조회, 추가, 삭제는 interface를 통해 구현됩니다.

 

app -> java -> [패키지명]에서 마우스 오른쪽 버튼을 눌러 New -> Kotlin File/Class를 선택한 뒤 이어지는 화면에서 interface를 선택하고 이름을 IRoomPhoneBook로 입력해 interface파일을 생성합니다.

 

interface 상단에 @Dao를 명시합니다. DAO(Data Access Object)는 DB 쪽으로 데이터를 조회, 수정, 삽입, 삭제하는 메서드를 의미합니다.

package com.example.myapplication

import androidx.room.Dao

@Dao
interface IRoomPhoneBook {
}

먼저 조회 부분은 @Query를 통해 직접 쿼리를 작성합니다.

package com.example.myapplication

import androidx.room.Dao
import androidx.room.Query

@Dao
interface IRoomPhoneBook {
    @Query("Select * From RoomPhoneBook")
    fun GetItem(): List<RoomPhoneBook>
}

다음으로 데이터를 추가하는 Insert와 데이터를 삭제하는 Delete를 구현합니다. Insert에 onConflict = REPLACE는 같은 값의 데이터가 추가될 때 이를 Update로 바꾼다는 의미입니다.

package com.example.myapplication

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

@Dao
interface IRoomPhoneBook {
    @Query("Select * From RoomPhoneBook")
    fun GetItem(): List<RoomPhoneBook>
    
    @Insert(onConflict = REPLACE)
    fun Add(phonebook: RoomPhoneBook)
    
    @Delete
    fun Remove(phonebook: RoomPhoneBook)
}

SQLite를 사용할 때는 SQLiteOpenHelper를 상속해 Helper를 작성했는데 Room 또한 RoomDatabase를 상속해 Helper를 구현합니다.

 

app -> java -> [패키지명]에서 마우스 오른쪽 버튼을 눌러 New -> Kotlin File/Class를 선택한 뒤 Class를 선택하고 RoomHelper이름으로 새로운 파일을 생성합니다.

 

abstract를 붙여 추상 클래스로 만들고 RoomDatabase로부터 상속받도록 합니다.

package com.example.myapplication

import androidx.room.RoomDatabase

abstract class RoomHelper: RoomDatabase() {
}

그리고 class위에 @Database 어노테이션을 작성해 Entity와 버전 등을 지정하고 클래스 안에서 앞서 만들었던 인터페이스를 사용할 수 있는 메서드를 추가합니다.

package com.example.myapplication

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

@Database(entities = arrayOf(RoomPhoneBook::class), version = 1)
abstract class RoomHelper: RoomDatabase() {
}

이제 Recycler로 넘어와 Adapter 클래스를 생성합니다. Adapter클래스는 이름만 조금 바뀔 뿐 SQLite를 사용할 때와 마찬가지 방식으로 만들어집니다. 그래서 아래 글의 Adapter코드를 그대로 가져와 재사용하였으며 신규로 만든 RoomPhoneBook클래스와 인터페이스에 맞게 명칭과 관련된 약간의 코드만 수정하였습니다.

 

SQLite에서의 Adapter는 아래 글을 참고해 주시기 바랍니다.

 

[Mobile/Kotlin] - [kotlin] SQLite - 연결 및 사용하기

package com.example.myapplication

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.phone_book_item.view.*

class RecyclerAdapter: RecyclerView.Adapter<RecyclerAdapter.Holder>() {
    var pbList = mutableListOf<RoomPhoneBook>()
    var roomHelper: RoomHelper? = null

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.phone_book_item, parent, false)
        return Holder(view)
    }

    override fun onBindViewHolder(holder: Holder, position: Int) {
        val pb = pbList.get(position)
        holder.GetItem(pb)
    }

    override fun getItemCount(): Int {
        return pbList.size
    }

    inner class Holder(itemView: View): RecyclerView.ViewHolder(itemView) {
    	var pb: RoomPhoneBook? = null
        
        init {
            itemView.btnDelete.setOnClickListener {
                roomHelper?.roomPhoneBook()?.Remove(pb!!)
                pbList.remove(pb)
                notifyDataSetChanged()
            }
        }

        fun GetItem(phoneBook: RoomPhoneBook) {
            itemView.txtSeq.text = "${phoneBook.seq.toString()}"
            itemView.txtName.text = "${phoneBook.name}"
            itemView.txtPhone.text = "${phoneBook.phone}"

            this.pb = phoneBook
        }
    }
}

MainActivity에서는 helper변수를 선언하고 초기 설정을 추가합니다. 참고로 allowMainThreadQueries()는 Room이 메인 스레드에서 동작이 가능하도록 하기 위함입니다.

var roomHelper: RoomHelper? = null
roomHelper = Room.databaseBuilder(this, RoomHelper::class.java, "RoomPhoneBook").allowMainThreadQueries().build()

그다음 adapter에 helper와 초기 데이터를 설정합니다. GetItem() 메서드는 List형식을 반환하므로 형식을 일치시키기 위해 as MutableList<RoomPhoneBook>로 형 변환을 시도한 것이며 ?: mutableListOf() 구문은 데이터가 null인 경우 기본값을 지정하기 위한 것입니다.

val adapter = RecyclerAdapter()
adapter.roomHelper = roomHelper
adapter.pbList = roomHelper?.roomPhoneBook()?.GetItem() as MutableList<RoomPhoneBook> ?: mutableListOf()

또한 MainActivity에서 목록을 표시하기 위해 추가한 rccList에 adapter를 지정하고 추가 버튼 이벤트를 만들어 입력한 내용을 추가한 뒤 데이터를 표시하는 부분을 구현합니다.

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        var roomHelper: RoomHelper? = null
        roomHelper = Room.databaseBuilder(this, RoomHelper::class.java, "testDB").allowMainThreadQueries().build()

        val adapter = RecyclerAdapter()
        adapter.roomHelper = roomHelper
        adapter.pbList = roomHelper?.roomPhoneBook()?.GetItem() as MutableList<RoomPhoneBook> ?: mutableListOf()

        rccList.adapter = adapter
        rccList.layoutManager = LinearLayoutManager(this)

        btnSave.setOnClickListener {
            val pbItem = RoomPhoneBook(txtName.text.toString(), txtPhone.text.toString())
            roomHelper?.roomPhoneBook()?.Add(pbItem)

            adapter.pbList.clear()
            adapter.pbList.addAll(roomHelper?.roomPhoneBook()?.GetItem() as MutableList<RoomPhoneBook> ?: mutableListOf())
            adapter.notifyDataSetChanged()
        }
    }
}

728x90