본문 바로가기

Kotlin Multiplatform Mobile

[KMM] Realm-Kotlin

Kotlin MultiPlatform Mobile에서 Realm Database를 사용하기 위해,
순수 Kotlin언어로 작성된 Realm-Kotlin을 소개합니다!

 

  1. sdk 설치
  2. Object Schema 정의
  3. Realm 열기
  4. Data 읽고 쓰기
  5. Reactive 형태로 Data 변화 감지

1. sdk 설치

  • Kotlin MultiPlatform에서 설치하는 방법으로
    해당 모듈의 build.gradle.kts 파일에서 realm, coroutines 관련 dependencies 및 plugins 추가
    • 내부적으로 Realm Kotlin은 coroutines를 사용하여 비동기 처리를 하기에 관련 라이브러리 필요함
    • 주의사항 : Realm Kotlin 버전 1.3.0 이상 사용 시 프로젝트의 Kotlin 버전 1.7.20 이상 사용해야 함
      (Coroutine 라이브러리는 1.6.0 이상 사용)
plugins {
    id("io.realm.kotlin") version "1.5.0"
}
...
kotlin {
    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
                implementation("io.realm.kotlin:library-base:1.5.0")
            }
        }
    }
}

2. Object Schema 정의

  • Realm Database에서 사용할 data model 정의 시 RealmObject 상속
  • 기본적인 데이터 타입뿐만 아니라 Primary Keys와 관계 형식의 데이터도 정의할 수 있음
  • 관계형 프로퍼티
    • To-One Relationship : 하나 이상의 다른 객체와 연결되지 않음을 의미하므로 타입 선언 시 반드시 nullable이어야 함
    • To-Many Relationship : 리스트 형태의 RealmObject들을 정의하는 방법으로 RealmList<T>타입 선언
class GitUserRealm : RealmObject {
    @PrimaryKey
    var id: Int = 0
    var login: String = ""
    var name: String = ""
    var company: String = ""
    var follower: GitFollowerRealm? = null
    var followers: RealmList<GitFollowerRealm> = realmListOf()
}

class GitFollowerRealm : EmbeddedRealmObject {
    var id: Int = 0
    var login: String = ""
    var url: String = ""
}

 

  • EmbeddedRealmObject 특징 
    (아직 공식 문서에 작성된 기능은 아니지만 realm-kotlin(Github Page)의 Pull Request 내역에서 찾음 - 2023/01/30 기준)
    • 단독으로 사용할 수 없고, 반드시 하나의 parent가 연결되어야 함
    • @PrimaryKey 필드 사용 불가
    • parent object 삭제 시 embedded 된 모든 objects 함께 삭제됨
    • (Realm-Java의 @RealmClass(embedded = true)과 같은 역할)

3. Realm 열기

private val realm: Realm by lazy {
    val configuration = RealmConfiguration.Builder(
      schema = setOf(GitUserRealm::class, GitFollowerRealm::class)
    ).apply {
        deleteRealmIfMigrationNeeded()
    }.build()
    
    Realm.open(configuration)
}
  • Realm 초기화 시 정의한 스키마와 configuration을 지정할 수 있음
  • RealmConfiguration 옵션
    • deleteRealmIfMigrationNeeded() : migration 없이 기존 realm 스키마 변경 시 기존 데이터 삭제 후 새로운 스키마 정의
    • encryptionKey(encryptionKey: ByteArray) : 64 byte 암호화 키 사용하여 realm file 암호화 및 복호화
      (암호화되지 않은 realms 보다 10% 느림)
    • inMemory() : realm file에 쓰지 않고 메모리에서만 데이터를 관리하는 옵션으로 realm 또는 앱 종료 시 데이터가 유지되지 않음

4. Data 읽고 쓰기

  • Create, Update, Delete 시 Frozen Architecture에 의해 write transaction block을 사용하여 live objects를 사용해야 함
    • realm에서 제공하는 write transaction 방식은 2가지로
      • writeBlocking : 작업이 완료될 때까지 호출한 thread를 blocking 함
        (UI thread 사용 시 ANR 발생 가능성 있음)
      • write : Coroutines 사용하여 비동기적으로 작업을 처리함
        realm에서 제공하는 default thread(Dispatcher) 사용
        (호출 시 별도로 thread 변경할 필요 없음
// sync
fun insertBlocking() {
    realm.writeBlocking {
        copyToRealm(gitUserData, UpdatePolicy.ALL)
    }
}
// async
suspend fun insert() {
    realm.write {
        copyToRealm(gitUserData, UpdatePolicy.ALL)
    }
}

// MutableRealm에 정의된 interface
public fun <T : RealmObject> copyToRealm(instance: T, updatePolicy: UpdatePolicy = UpdatePolicy.ERROR): T

// Realm에 정의된 interface
public fun <R> writeBlocking(block: MutableRealm.() -> R): R
public suspend fun <R> write(block: MutableRealm.() -> R): R
  • Create / Update
    • write transaction 내에서 작업해야 함
    • copyToRealm()은 새로운 object를 realm에 넣거나 존재하는 object를 업데이트
    • copyToRealm()의 updatePolicy는 ERROR로 설정되어 있어 primary key가 중복되면 Exception이 발생함
      -> updatePolicy를 UpdatePolicy.ALL 변경하면 매칭되는 key의 object 값들을 업데이트할 수 있음
      (Realm-Java의 insertOrUpdate()와 같은 기능)
// sync
fun get() =
    realm.query<GitUserRealm>()
        .find()
// async as flow
fun getAsFlow(id: Int) =
    realm.query<GitUserRealm>("id == '$id'")
        .asFlow()

// MutableRealm에 정의된 interface
override fun <T : BaseRealmObject> query(
    clazz: KClass<T>,
    query: String,
    vararg args: Any?
): RealmQuery<T>

// RealmResults, ResultsChange에 정의된 interface
public fun find(): RealmResults<T>
public fun asFlow(): Flow<ResultsChange<T>>
  • Read
    • query()는 조회하고자 하는 RealmObject와 query 내용과 매칭되는 RealmQuery를 반환함
    • 반환된 RealmQuery의 결과를 조회하는 방법은 2가지로
      • find() : 동기적으로 동작하며 query 시 호출한 thread를 사용함 
        (UI thread 사용 시 ANR 발생 가능성 있음)
      • asFlow() : query 결과를 Flow로 반환하여 CoroutineScope에서 비동기적으로 작업할 수 있음
    • 주의사항
      • Realm.query()를 통해 조회한 데이터의 결과는 frozen objects로 업데이트 및 삭제 시 write transaction block 안에서
        • MutableRealm.query()로 조회하여 live objects를 반환받아 사용하거나
        • MutableRealm.findLatest()를 통해 live objects로 변환하여 사용해야 함
// sync
fun deleteBlocking() {
    realm.writeBlocking {
        val gitUser = query<GitUserRealm>().find()
        delete(gitUser)
    }
}
// async
suspend fun delete() {
    realm.write {
        val gitUser = query<GitUserRealm>().find()
        delete(gitUser)
    }
}

// MutableRealm에 정의된 interface
public fun delete(deleteable: Deleteable)
  • Delete
    • write transaction 내에서 작업해야 함
    • MutableRealm.query()를 통해 지우고자 하는 live objects를 조회하여 반환된 RealmQuery 또는 RealmResults로 삭제

5. Reactive 형태로 Data 변화 감지

scope.launch {
    gitDao.getAsFlow().collectLatest { user ->
        user.toString()
    }
}
  • realm.query() 시 flow로 형태로 반환하고
  • coroutineScpoe 안에서 데이터 처리

관련 코드는 https://github.com/Yeechaan/kmm-git 에 업로드 해두었고,
shared 모듈의 commonMain을 참고하면 된다.

 

realm-kotlin

공식문서 : https://www.mongodb.com/docs/realm/sdk/kotlin/

Github Page : https://github.com/realm/realm-kotlin