1.  Room: ORM 라이브러리

ORM Object-Relational Mapping은 객체 Class와 관계형 데이터베이스의 데이터 Table을 매핑하고 변환하는 기술로 복잡한 쿼리를 잘 몰라도 코드만으로 데이터베이스의 모든 것을 컨트롤할 수 있도록 도와줌

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


1) Room 추가하기


  ✏️  Room 프로젝트를 하나 새로 생성. SQLite 프로젝트에서 몇 개의 화면과 액티비티는 복사해서 사용

build.gradle 파일을 열고 android 블록에 viewBinding 설정 추가
viewBinding {
    enabled = true
}

 

dependencies 블록 앞부분에 다음과 같이 Room을 추가

 

  Room은 빠른 처리 속도를 위해서 어노테이션 프로세서 annotation processor를 사용하는데, 코틀린에서는 이것을 대신해서 kapt를 사용. kapt를 사용하기 위해서는 파일 상단에 kapt 플러그인을 추가 (큰 따옴표를 사용)

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id "org.jetbrains.kotlin.kapt" version "1.6.21"
}
dependencies {

    def room_version = "2.4.0"

    implementation("androidx.room:room-runtime:$room_version")
    annotationProcessor("androidx.room:room-compiler:$room_version")
    kapt("androidx.room:room-compiler:$room_version")
    implementation("androidx.room:room-ktx:$room_version")

 

💡  kapt란?
     "자바 6부터 도입된 Pluggable Annotation Processing API (JSR 269)를 Kotlin에서도 사용 가능하게 하는 것입니다."[안드로이드 공식문서]


  ✓  어노테이션 프로세싱이란 우리가 간단하게 '@명령어'처럼 사용하는 주석형태의 문자열을 실제 코드로 생성해주는 것
  ✓  @로 시작하는 명령어를 어노테이션이라고 하는데, 어노테이션이 컴파일 시에 코드로 생성되기 때문에 실행 시에 발생할 수 있는 성능 문제가 많이 개선됨
  ✓  Room을 사용하면 클래스명이나 변수명 위에 @어노테이션을 사용해서 코드로 변환할 수 있음

 


2) RoomMemo 클래스 정의하기

 

A. 먼저 SQLite 프로젝트에서 사용한 파일 중에서 java 패키지 아래에 있는 MainActivity, RecyclerAdapter를 복사해서 이 프로젝트에 붙여넣기 함

  • 동일한 파일인 MainActivity가 이미 있기 때문에 붙여넣기 여부를 묻는 팝업창에 [Overwrite]를 클릭
  • 붙여넣기를 한 ActivityMain을 열어보면 패키지 명과 네 번째 import인 ActivityMainBinding의 경로가 다름 두 군데만 수정
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.recyclerview.widget.LinearLayoutManager
import kr.somin.room_0521.databinding.ActivityMainBinding

B.  [res] - [layout] 밑에 있는 activity_main.xml과 item_recycler.xml도 복사해서 붙여넣기


C.  패키지 이름에서 [New] - [Kotlin File/Class]를 선택해서 RoomMemo로 클래스를 생성

@Entity(tableName = "room_memo")
class RoomMemo {

    constructor(content: String, datetime: Long) {
        this.content = content
        this.datetime = datetime
    }

    @PrimaryKey(autoGenerate = true)
    @ColumnInfo
    var num: Long? = null

    @ColumnInfo
    var content: String = ""

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

}

 

@Entity(tableName = "room_memo")
class RoomMemo  { ... }

 

  ✓  @Entity 어노테이션을 class RoomMemo 위에 작성. Room 라이브러리는 @Entity 어노테이션이 적용된 클래스를 찾아 테이블로 변환
  ✓  데이터베이스에서 테이블명을 클래스명과 다르게 하고 싶을 때는 @Entity(tableName = "테이블명")과 같이 작성하면 됨

 

@ColumnInfo
var num: Long? = null

@ColumnInfo
var content: String = ""

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

 

  ✓  멤버 변수 num, content, date 3개를 선언하고 변수명 위에 @ColumnInfo 어노테이션을 작성해서 테이블의 컬럼으로 사용된다는 것을 명시. 컬럼명도 테이블명처럼 변수명과 다르게 하고 싶을 때는 @ColumnInfo(name = "컬럼명")과 같이 작성하면 됨.

 

@PrimaryKey(autoGenerate = true)
@ColumnInfo
var num: Long? = null

 

  ✓  num 변수에 @PrimaryKey 어노테이션을 사용해서 키(Key)라는 점을 명시하고 자동증가 옵션을 추가

 

constructor(content: String, datetime: Long) {
    this.content = content
    this.datetime = datetime
}

 

  ✓  content외 datetime을 받는 생성자를 작성

 


3)  RoomMemoDAO 인터페이스 정의하기


Room은 데이터베이스에 읽고 쓰는 메서드를 인터페이스 형태로 설계하고 사용. 코드 없이 이름만 명시하는 형태로 인터페이스를 만들면 Room이 나머지 코드를 자동 생성함

💡  DAO란?
      Data Access Object의 약어로 데이터베이스에 접근해서 DML 쿼리 (SELECT, INSERT, UPDATE, DELETE)를 실행하는 메서드의 모음

 

[app] - [java] 밑의 패키지 아래에 RoomMemoDao 인터페이스를 생성

 

인터페이스 상단에 @Dao 어노테이션을 작성하고 Dao라는 것을 명시
@Dao
interface RoomMemoDao { ... }

 

조회, 삽입, 수정, 삭제에 해당하는 3개의 메서드를 만들고 각각의 어노테이션 을 붙여줌
@Dao
interface RoomMemoDao {
    // 다른 ORM 툴과는 다르게 조회를 하는 select 쿼리는 직접 작성하도록 설계.
    // 대부분의 ORM은 select도 메서드로 제공

    @Query("SELECT * FROM room_memo")
    fun getAll() : List<RoomMemo>

    @Insert(onConflict = REPLACE)
    fun insert(memo: RoomMemo)
}

 

  ✓  두 번째 @Insert 어노테이션의 경우 옵션으로 onConflict = REPLACE를 적용하면 동일한 키를 가진 값이 입력되었을 때 UPDATE 쿼리로 실행이 됨

 

어노테이션의 종류
어노테이션 위치 옵션 설명
@Database 클래스 entities, version 데이터베이스
@Entity 클래스 (tableName = "테이블명") 테이블
@ColumnInfo 멤버변수 (name = "컬럼명") 컬럼
@PrimaryKey 멤버변수 (autoGenerate = true) 컬럼 옵션
@Dao 인터페이스   실행 메서드 인터페이스
@Query 멤버 메서드 ("쿼리") 쿼리를 직접 작성하고 실행
@Insert 멤버 메서드 (onConflict = REPLACE) 중복 시 수정
@Delete 멤버 메서드   삭제

 


4)  RoomHelper 클래스 정의하기


SQLiteOpenHelper를 상속받아서 구현했던 것처럼 Room도 유사한 구조로 사용할 수 있음. Room은 RoomDatabase를 제공하는데 RoomDatabase를 상속받아 클래스를 생성하면 됨
  📍  주의할 점은 추상 클래스로 생성해야 함. 기존 클래스와 동일하게 생성하고 class 앞에 abstract 키워드를 붙이면 추상 클래스가 됨.

 

[app] - [java] 밑의 패키지 아래에 RoomHelper 클래스를 생성하고 앞에 abstract 키워드를 붙여서 추상 클래스를 만듦
이 클래스는 RoomDatabase를 상속받음.
abstract class RoomHelper: RoomdDatabase() {}

 

클래스명 위에 @Database 어노테이션을 작성
@Database(entities = arrayOf(RoomMemo::class), version = 1, exportSchema = false)

 

  📌  @Database 어노테이션 속성

옵션 설명
entities Room 라이브러리가 사용할 엔티티(테이블) 클래스 목록
version 데이터베이스의 버전
exportSchema true면 스키마 정보를 파일로 출력

 

RoomHelper 클래스 안에 앞에서 정의한 RoomMemoDao 인터페이스의 구현체를 사용할 수 있는 메서드명을 정의
@Database(entities = arrayOf(RoomMemo::class), version = 1, exportSchema = false)
abstract class RoomHelper: RoomDatabase() {
    abstract fun roomMemoDao(): RoomMemoDao
}

 

  ✓  빈 껍데기 코드만 작성해 두는 것만으로 Room 라이브러리를 통해서 미리 만들어져 있는 코드를 사용할 수 있게 됨

 

 


5)  어댑터에서 사용하는 Memo 클래스를 RoomMemo 클래스로 변경

 

  ·  Memo 문자열을 모두 RoomMemo로 수정

  ·  helper 변수가 선언된 부분을 RoomHelper를 사용할 수 있도록 수정 

        ➡️  var helper: SQLiteHelper? = null ▶️ var helper: RoomHelper? = null

  ·  buttonDelete 클릭리스너에 있는 deleteMemo() 메서드를 RoomHelper의 메서드로 수정. RoomHelper를 사용할 때는 여러 개의 Dao가 있을 수 있기 때문에 '헬퍼.Dao().메서드()'형태로 어떤 Dao를 쓸 것인지 명시해야 함.

       ➡️  helper?.deleteMemo(mMemo!!) ▶️ helper?.roomMemoDao()?.delete(mRoomMemo!!)


6)  MainActivity에서 RoomHelper 사용

 

MainActivity.kt 파일을 열고 앞에서 작성한 SQLiteHelper를 RoomHelper로 교체

 

MainActivity 맨 윗줄에 정의된 helper 변수를 RoomHelper를 사용할 수 있도록 코드를 수정
private var helper: RoomHelper? = null

 

onCreate()의 setContentView 바로 아래줄에 helper를 생성하는 부분을 추가
setContentView(binding.root)
helper = Room.databaseBuilder(this, RoomHelper::class.java, "room_memo")
    .allowMainThreadQueries()
    .build()


  ✓  databaseBuilder() 메서드의 세 번째 파라미터가 실제 생성되는 DB 파일의 이름.

  ✓  Room은 기본적으로 서브 스레드에서 동작하도록 설계되어 있기 때문에 allowMainThreadQueries() 옵션이 적용되지않으면 앱이 동작을 멈춤.

 

어댑터의 데이터 목록에 셋팅하는 코드를 RoomHelper를 사용하는 것으로 수정
adapter.listData.addAll(helper?.roomMemoDao()?.getAll()?: listOf())

 

  ✓  helper에 null이 허용되므로 helper 안의 코드를 사용하기 위해서는 helper?.의 형태로 사용. 이어지는 roomMemoDao()?.도 같은 맥락이고, adapter의 listData에 null이 허용되지 않기 때문에 마지막에 ?:(Elvis Operator)를 사용해서 앞의 2개가 null일 경우 사용하기 위한 디폴트 값을 설정

 

저장 버튼을 클릭 시 사용하는 코드도 RoomHelper로 바꿔줌
binding.buttonSave.setOnClickListener {
    if (binding.editMemo.text.toString().isNotEmpty()) {
        val memo = RoomMemo(binding.editMemo.text.toString(), System.currentTimeMillis())
        helper?.roomMemoDao()?.insert(memo)
        adapter.listData.clear()

        adapter.listData.addAll(helper?.roomMemoDao()?.getAll()?: listOf())
        adapter.notifyDataSetChanged()
        binding.editMemo.setText("")
     }
}

 

 

 

[ 내용 참고 : IT 학원 강의 ]

+ Recent posts