^(코딩캣)^ = @"코딩"하는 고양이;

[단편 Android 개발 정리] 룸(Room) 사용하기 (2021년 버전)

API/Android
2021. 7. 8. 22:32

단편 Android 개발 정리

이전 게시글: 사이드 메뉴(DrawerLayout) 구현하기 (2021년 버전)

 

룸(Room) 사용하기 (2021년 버전)

안드로이드 앱에서 SQLite는 로컬 데이터베이스로 쓰이고 있다. SQLite를 통해 데이터베이스에 접근하기 위해 이전까지는 DatabaseHelper 클래스를 파생 및 구현시키는 방법을 써 왔으나, 룸(Room)이라는 것이 새로 제공되어 이를 정리해 두고자 한다. 결론부터 말하면 Helper 클래스를 구현하면서 메소드별로, 테이블별로 일일이 커서를 읽고 형변환하고 예외처리하는 등의 귀찮은 작업은 이제 컴파일러가 알아서 도맡을테니 메타 데이터로 껍데기만 알려주면 되는 편리한 기능이다.

 

컴파일러 의존성 설정하기

2021년 기준 최신 Android Studio에서 Room을 사용하고자 한다면 다음과 같이 gradle에 의존성을 추가하면 된다. 이 역시 나중에 Android Studio가 버전 상승하면 할 필요가 없어지려나?

dependencies {
// ... 기본적으로 생성되는 코드들은 생략하고 끝에 이런 내용들을 추가한다.

// 2021년 현재 2.3.0이 최신 버전이나 2.2.5를 써 보겠다.
def $room_version = "2.2.5"

// 클래스나 매개변수 앞에 붙는 annotation을 처리하기 위함
implementation 'androidx.room:room-runtime:$room_version'
annotationProcessor 'androidx.room:room-compiler:$room_version'

// 아래 내용이 없으면 나중에 실행할 때
// 클래스명_Impl does not exists라는 오류가 발생함
apply plugin: 'kotlin-kapt'
kapt 'androidx.room:room-compiler:$room_version'

물론 다음과 같이 버전을 하드코딩해도 된다. 나중에 룸의 버전이 올라갔을 때 불편할 수는 있겠다.

dependencies {
// ... 기본적으로 생성되는 코드들은 생략하고 끝에 이런 내용들을 추가한다.

apply plugin: 'kotlin-kapt'
kapt 'androidx.room:room-compiler:2.2.5'

implementation 'androidx.room:room-runtime:2.2.5'
annotationProcessor 'androidx.room:room-compiler:2.2.5'

kapt라는 것이 등장하는데, 이것은 kotlin 컴파일러에 부착되어서 돌아가는 플러그인 같은 것이다.

 

엔티티를 나타내는 클래스 선언하기

엔티티(entity), 행(row), 레코드(record)... 라고 불리는 것을 클래스 형태로 선언할 차례다. 예를 들어 SQL로 다음과 같이 생성된 테이블에 접근하고자 한다.

CREATE TABLE "Entity" (
	"id"	INTEGER NOT NULL UNIQUE,
	"title"	TEXT NOT NULL,
	"content"	TEXT,
	"data"	BLOB,
	PRIMARY KEY("id")
);
CREATE INDEX "IndexEntity" ON "Entity" (
	"id"	ASC
);

id는 정수형 값이고 NULL을 허용하지 않으며 테이블 내에서 유일해야 하고 인덱스까지 되어 있다고 가정한다. titleNULL을 허용하지 않는 문자열이다. content도 문자열이지만 NULL을 허용한다. data는 이진(binary) 데이터이다.

이 테이블에 보관되는 각 행을 표현하는 kotlin 클래스는 다음과 같이 적을 수 있다.

@Entity(tableName = "Entity", primaryKeys = ["id"], indicies = [Index(name = "IndexEntity", value = "id")]
class Entity(@ColumnInfo(name = "id", typeAffinity = ColumnInfo.INTEGER) 
             var id: Int, 
             @ColumnInfo(name = "title", typeAffinity = ColumnInfo.TEXT)
             var title: String,
             @ColumnInfo(name = "content", typeAffinity = ColumnInfo.TEXT)
             var content: String?,
             @ColumnInfo(name = "data", typeAffinity = ColumnInfo.BLOB)
             var data: ByteArray?) {
	// ...
}

titlecontent의 차이에 주목한다. 하나는 NULL을 허용하지 않고 다른 하나는 허용한다. 이 차이는 annotation에서 드러나지 않는다. 둘 다 타입이 ColumnInfo.TEXT로 되어 있기 때문이다. 대신 매개변수의 형식을 본다. 하나는 String이고 다른 하나는 nullable인 String?이다.

SQLite에서 BLOB으로 선언한 필드의 경우 kotlin과 연계 시 그 자료형은 ByteArray로 될 수 있다.

테이블을 생성할 때 사용한 SQL 구문의 내용과 코틀린 클래스의 annotation으로 지정한 내용에 차이가 있을 경우 런타임 시 테이블 선언이 서로 다르다는 오류를 뱉을 수 있다.

 

엔티티에 대해 할 수 있는 조회, 삽입 및 삭제 작업 선언하기

이전까지는 Helper 클래스를 작성하면서 이를 직접 구현하였다. SQL 구문을 구성해서 쿼리 수행하고, 결과를 읽어야 한다면 커서(Cursor)를 열고 필드 순번으로써 해당 필드를 접근하고 예외 발생 시 오류처리 등등... 이것이 룸(Room)을 사용하면 단순히 인터페이스 선언만으로 축약된다. 인터페이스를 구현하는 작업은 앞서 플러그인의 버프를 받는 컴파일러가 수행한다. 즉, 인터페이스에 대한 클래스 구현은 컴파일러가 알아서 해 준다. 이와 같이 테이블의 행들을 가지고 작업할 수 있는 객체를 DAO(Database Access Object)라 한다.

@Dao
interface IEntityAccess {
	@Query("SELECT * FROM Entity")
    fun select(): List<Entity>
    @Query("SELECT * FROM Entity WHERE title LIKE :condition")
    fun select(condition: String?): List<Entity>
}

이런 식으로 쿼리를 작성한다. 메소드의 반환형은 대체로 리스트면 무난할 것 같다. 매개변수는 쿼리 안에서 콜론을 붙이고 매개변수 이름을 그대로 적으면 된다.

UPDATE, INSERT, UPDATE 등의 코드는 annotation만 작성하면 된다. 쿼리는 알아서 작성해준다. 이 때 기본 키를 가지고 특정 행을 식별하는데, 기본키에 해당하는 필드 자체를 수정할 수는 없다. 기본키 필드에 담긴 값 자체를 수정하려면 위와 같이 쿼리를 직접 작성해야 한다.

@Dao
interface IEntry2Access {
	@Insert
    fun insert(entry: Entry2)
    @Update
    fun update(entry: Entry2) // 수정할 행의 기본 키와 새로 수정할 내용이 담긴 객체
    @Delete
    fun delete(entry: Entry2)
}

 

데이터베이스 클래스 작성하기

엔티티를 정의했고, 그 엔티티에 대해 수행하는 작업을 정의했다. 그러한 엔티티들을 포함하는 단위인 데이터베이스도 클래스로 작성할 차례이다.

@Database(entities = [Entity::class, Entity1::class, Entity2::class], version = 1, exportSchema = false)
abstract class Database : RoomDatabase() {
    abstract fun accessEntity(): IEntityAccess
    abstract fun accessEntity1(): IEntityAccess
    companion object {
    	private val instance: Database? = null
    	@Synchronized
        fun getInstance(context: Context): Database {
        	if (instance == null) {
            	synchronized(Database::class) {
                    instance = Room.databaseBuilder(
                        context,
                        Database::class.java,
                        파일경로).build()
                }
            }
            
            return instance!!
        }
    }
}

asset에 이미 구촉된 데이터베이스가 있다면 이것을 앱 로컬 경로로 복사하는 식으로 생성한다.

instance = Room.databaseBuilder(
	context,
	Database::class.java,
	파일경로).createFromAsset(이름).build()

 

카테고리 “API/Android”
more...