본문 바로가기
Android/개념 및 정보

[Android] Realm Database 첫 시작(with kotlin)

by Taehyung Kim, dev 2020. 12. 5.
728x90

현재 업무 프로젝트에서 Realm Database 를 사용하고 있는데..

아직 Realm DB 를 사용해보지 못한 터라 업무 능력 향상을 위해 Realm Database 를 공부하기로 했어요

 

처음 접하는 개념이기에 아래와 같은 순서대로 알아보고 시작해보았습니다.

 

Realm 이란

Realm 특징

Android Realm 사용 방법

 

Realm 이란

Realm이란 Realm 데이터베이스 컨테이너의 인스턴스입니다. Realm은 로컬, 동기화, 혹은 인 메모리 방식으로 사용할 수 있습니다. 이 중 어느 종류의 Realm이라도 애플리케이션에서 같은 방식으로 동작할 수 있습니다. 인 메모리 Realm은 저장 메커니즘이 없는 임시 저장소를 뜻합니다. 동기 Realm은 Realm 오브젝트 서버를 사용해서 다른 기기 사이에 컨텐츠를 동기화합니다. 애플리케이션이 동기 Realm을 로컬 파일처럼 사용하는 동안 쓰기 접근이 가능한 다른 디바이스에서 해당 Realm의 데이터를 업데이트할 수 있습니다. 따라서 Realm은 방에 참여한 어느 사용자라도 업데이트할 수 있는 채팅 애플리케이션의 채팅방을 나타낼 수 있습니다. 혹은 소유한 모든 기기에서 접근할 수 있는 쇼핑 앱의 장바구니가 될 수도 있습니다.

공식 홈페이지에서 위와 같이 설명해주고 있네요.

 

Realm 특징

- SQL query 를 몰라도 작성이 가능하다.(ORM : Object Relational Mapping)

- DBMS 에 대한 종속성을 줄일 수 있다.

- 성능이 뛰어나다.

- 서버, 모바일 간 DB 공유가 가능하다.

- 라이브러리 크기때문에 앱 용량이 2~3 MB 증가한다.

 

제가 본 앱 개발 관점에서 주요한 특징을 한 줄로 정리하면 아래와 같습니다.

앱 용량의 크기가 늘어나지만 성능은 뛰어나다.

앱의 크기와 성능 둘 다 매출에 영향을 준다고 생각합니다.

 

Android Realm 사용 방법

1. Install Realm as a Gradle plugin

아래와 같이 build.gradle 파일에 종속성을 추가합니다.

 

build.gradle(project) 파일에 추가해주세요.

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath "io.realm:realm-gradle-plugin:x.y.z"
    }
}

 

그리고 build.gradle(Module) 파일에 아래와 같이 추가해주세요.

plugins {
    id 'com.android.application'
    id 'kotlin-android'
    
    // Realm plugin
    id 'kotlin-kapt'
    id 'kotlin-android-extensions'
    id 'realm-android'
}

android {
    .
    .
    .
}

dependencies {
      .
      .
      .
}

 

추가한 뒤에 sync 해줍니다.

 

realm 에서 제공하는 기본 샘플들이 있습니다. 아래 샘플에서 Intro example 을 살펴보겠습니다.

https://github.com/realm/realm-java/tree/master/examples 

 

realm/realm-java

Realm is a mobile database: a replacement for SQLite & ORMs - realm/realm-java

github.com

 

위 샘플은 Java 로 작성되어있는데요, 저는 실전에서 코틀린을 주로 사용하기 때문에

보다 나은 학습을 위해 Intro example 을 Kotlin 으로 다시 작성하였습니다.

 

Java 로 적용하시려면 위 샘플을 클론하여 빌드하시면 되구요,

코틀린을 적용하시면 아래 코드를 참고하여 만드시면 됩니다.

 

결과물은 아래와 같은데요,

Realm DB 에 저장한 결과를 액티비티에 로그처럼 뿌려주는 샘플입니다.

 

프로젝트 구조는 아래와 같습니다.

model 패키지에 Database 의 모델이 들어가고 MainActivity 에서는 Database CRUD 가 실행됩니다.

MyApplication

MyApplication 내부를 아래와 같이 작성합니다.

MyApplication은 Realm Database 를 시작하기 위한 역할로

Realm.init(this) 를 최초에 실행합니다. 이 부분을 실행하지 않으면 Realm Database 를 사용할 수 없습니다.

class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        // Initialize Realm. Should only be done once when the application starts.
        Realm.init(this);
    }
}

위와 같이 작성한 뒤 Manifest 에 MyApplication 을 등록해줍니다.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.thkim.realmtest">

    <application
        android:name=".MyApplication" <-- 이 부분을 추가합니다.
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.RealmTest">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

Model

Cat

@LinkingObjects 어노테이션을 통해 뒤에서 만들 Person 모델 클래스를 명시적으로 연결해줍니다.

open class Cat : RealmObject() {
    var name: String? = null

    // You can define inverse relationships
    @LinkingObjects("cats")
    val owners: RealmResults<Person>? = null
}

Dog

RealmModel 을 implement 하면 클래스에 @RealmClass 어노테이션을 설정하여야합니다.

또한 위와 마찬가지로 @LinkingObjects 어노테이션을 붙여 연결해줍니다.

@RealmClass
open class Dog : RealmModel {
    var name: String? = null

    // You can define inverse relationships
    @LinkingObjects("dog")
    val owners: RealmResults<Person>? = null
}

Person

RealmObject 를 implement 해주고 아래 데이터를 명시해줍니다.

또한 예제에서는 아래에 getter & setter 가 선언되어 있었지만 코틀린에서는 따로 명시해줄 필요가 없습니다.

open class Person : RealmObject() {
    // All fields are by default persisted.
    var age: Int = 0

    // Adding an index makes queries execute faster on that field.
    @Index
    lateinit var name: String

    // Primary keys are optional, but it allows identifying a specific object
    // when Realm writes are instructed to update if the object already exists in the Realm
    @PrimaryKey
    var id: Long = 0L

    // Other objects in a one-to-one relation must also implement RealmModel, or extend RealmObject
    var dog: Dog? = null

    // One-to-many relations is simply a RealmList of the objects which also implements RealmModel
    var cats: RealmList<Cat>? = null

    // It is also possible to have list of primitive types (long, String, Date, byte[], etc.)
    var phoneNumber: RealmList<String>? = null

    // You can instruct Realm to ignore a field and not persist it.
    @Ignore
    var tempReference: Int? = 0
    // Let your IDE generate getters and setters for you!
    // Or if you like you can even have public fields and no accessors! See Dog.java and Cat.java

	// 이하 getter & setter
}

 

MainActivity

class MainActivity : AppCompatActivity() {

    private lateinit var realm: Realm

    // Results obtained from a Realm are live, and can be observed on looper threads (like the UI thread).
    // Note that if you want to observe the RealmResults for a long time, then it should be a field reference.
    // Otherwise, the RealmResults can no longer be notified if the GC has cleared the reference to it.
    private lateinit var persons: RealmResults<Person>

    // OrderedRealmCollectionChangeListener receives fine-grained changes - insertions, deletions, and changes.
    // If the change set isn't needed, then RealmChangeListener can also be used.
    private val realmChangeListener =
        OrderedRealmCollectionChangeListener { people: RealmResults<Person?>?, changeSet: OrderedCollectionChangeSet ->
            val insertions =
                if (changeSet.insertions.isEmpty()) "" else "\n - Insertions: " + Arrays.toString(
                    changeSet.insertions
                )
            val deletions =
                if (changeSet.deletions.isEmpty()) "" else "\n - Deletions: " + Arrays.toString(
                    changeSet.deletions
                )
            val changes =
                if (changeSet.changes.isEmpty()) "" else "\n - Changes: " + Arrays.toString(
                    changeSet.changes
                )
            showStatus("Person Database State : ${changeSet.state}")
            if (insertions != "" || deletions != "" || changes != "") {
                showStatus("Person was loaded, or written to. $insertions$deletions$changes")
            }
        }

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

        container.removeAllViews()

        // Clear the Realm if the example was previously run.
        Realm.deleteRealm(Realm.getDefaultConfiguration()!!)

        // Create the Realm instance
        realm = Realm.getDefaultInstance()

        // Asynchronous queries are evaluated on a background thread,
        // and passed to the registered change listener when it's done.
        // The change listener is also called on any future writes that change the result set.
        persons = realm.where(Person::class.java).findAllAsync()

        // The change listener will be notified when the data is loaded,
        // or the Realm is written to from any threads (and the result set is modified).
        persons.addChangeListener(realmChangeListener)

        // These operations are small enough that
        // we can generally safely run them on the UI thread.
        basicCRUD(realm)
        basicQuery(realm)
        basicLinkQuery(realm)

        // More complex operations can be executed on another thread.
        ComplexBackgroundOperations(WeakReference(this)).execute()
    }

    override fun onDestroy() {
        super.onDestroy()
        persons.removeAllChangeListeners() // Remove the change listener when no longer needed.
        realm.close() // Remember to close Realm when done.
    }

    private fun showStatus(text: String) {
        Log.i(TAG, text)
        val textView = TextView(this)
        textView.text = text
        container.addView(textView)
    }

    private fun basicCRUD(realm: Realm) {
        showStatus("Perform basic Create/Read/Update/Delete (CRUD) operations...")

        // All writes must be wrapped in a transaction to facilitate safe multi threading
        realm.executeTransaction {
            // Add a person
            // RealmObjects with primary keys created with 'createObject()' must specify the primary key value as an argument.
            val person = it.createObject(Person::class.java, 1).apply {
                name = "Young Person"
                age = 14

                // Even young people have at least one phone in this day and age.
                // Please note that this is a RealmList that contains primitive values.
                phoneNumber?.add("+1 123 4567")
            }
        }

        // Find the first person (no query conditions) and read a field
        val person = realm.where(Person::class.java).findFirst()
        showStatus("${person?.name} : ${person?.age}")

        // Update person in a transaction
        realm.executeTransaction {
            // Managed objects can be modified inside transactions.
            person?.name = "Senior Person"
            person?.age = 99
            showStatus("${person?.name} got older : ${person?.age}")
        }

        // Delete all persons
        showStatus("Deleting all persons")
        realm.executeTransaction { r -> r.delete(Person::class.java) }
    }

    private fun basicQuery(realm: Realm) {
        showStatus("\nPerforming basic Query operation...")

        // Let's add a person so that the query returns something.
        realm.executeTransaction { r ->
            val oldPerson = Person().apply {
                id = 99
                age = 99
                name = "George"
            }
            realm.insertOrUpdate(oldPerson)
        }

        showStatus("Number of persons: " + realm.where(Person::class.java).count())

        val filed = "age"
        val age = 99

        val results: RealmResults<Person> =
            realm.where(Person::class.java).equalTo(filed, age).findAll()

        showStatus("Size of result set: " + results.size)
    }

    private fun basicLinkQuery(realm: Realm) {
        showStatus("\nPerforming basic Link Query operation...")

        // Let's add a person with a cat so that the query returns something.
        realm.executeTransaction {
            val catLady = realm.createObject(Person::class.java, 24).apply {
                age = 52
                name = "Mary"

                val tiger = realm.createObject(Cat::class.java)
                tiger.name = "Tiger"
                this.cats?.add(tiger)
            }

            showStatus("Number of persons : ${realm.where(Person::class.java).count()}")

            val results = realm.where(Person::class.java).equalTo("cats.name", "Tiger").findAll()

            showStatus("Size of result set : ${results.size}")
        }
    }

    // This AsyncTask shows how to use Realm in background thread operations.
    //
    // AsyncTasks should be static inner classes to avoid memory leaks.
    // In this example, WeakReference is used for the sake of simplicity.
    class ComplexBackgroundOperations(
        private val weakReference: WeakReference<MainActivity>
    ) : AsyncTask<Void, Void, String>() {

        override fun onPreExecute() {
            super.onPreExecute()
            val activity = weakReference.get() ?: return
            activity.showStatus("\n\nBeginning complex operations on background thread.")
        }

        override fun doInBackground(vararg p0: Void?): String {
            val activity = weakReference.get() ?: return ""

            // Open the default realm. Uses `try-with-resources` to automatically close Realm when done.
            // All threads must use their own reference to the realm.
            // Realm instances, RealmResults, and managed RealmObjects can not be transferred across threads.
            Realm.getDefaultInstance().use { realm ->
                var info: String?
                info = activity.complexReadWrite(realm)
                info += activity.complexQuery(realm)
                return info
            }
        }

        override fun onPostExecute(result: String?) {
            super.onPostExecute(result)
            val activity = weakReference.get() ?: return
            activity.showStatus(result ?: "result is null.")
        }
    }

    private fun complexReadWrite(realm: Realm): String {
        var status = "\nPerforming complex Read/Write operation..."

        // Add ten persons in one transaction
        realm.executeTransaction { r ->
            val fido = r.createObject(Dog::class.java)
            fido.name = "fido"
            for (i in 0..9) {
                val person = r.createObject(Person::class.java, i)
                person.name = ("no.$i")
                person.age = i
                person.dog = fido

                // The field tempReference is annotated with @Ignore.
                // This means setTempReference sets the Person tempReference
                // field directly. The tempReference is NOT saved as part of
                // the RealmObject:
                person.tempReference = 42
                for (j in 0 until i) {
                    val cat = r.createObject(Cat::class.java)
                    cat.name = "Cat_$j"
                    person.cats?.add(cat)
                }
            }
        }

        // Implicit read transactions allow you to access your objects
        status += "\nNumber of persons: ${realm.where(Person::class.java).count()}"

        status += "\n| Name \t\t\t\t| Age\t | Dog name\t\t| Cats size | id"
        // Iterate over all objects, with an iterator
        for (person in realm.where(Person::class.java).findAll()) {
            val dogName: String? = person.dog?.name
            status += "\n| ${person.name}\t\t\t| ${person.age}\t| $dogName\t| ${person.cats?.size}| ${person.id}"
        }

        // Sorting
        val sortedPersons = realm.where(Person::class.java).sort("age", Sort.DESCENDING).findAll()
        status += "\nSorting ${sortedPersons.last()?.name} == ${
            realm.where(Person::class.java).findFirst()?.name
        }"

        return status
    }

    private fun complexQuery(realm: Realm): String {
        var status = "\n\nPerforming complex Query operation..."
        status += "\nNumber of persons: ${realm.where(Person::class.java).count()}"

        // Find all persons where age between 7 and 9 and name begins with "Person".
        val results = realm.where(Person::class.java)
            .between("age", 7, 9) // Notice implicit "and" operation
            .beginsWith("name", "no").findAll()
        status += "\nSize of result set: ${results.size}"

        return status
    }

    companion object {
        private const val TAG = "MainActivity"
    }
}

코드 양이 꽤 길어보이네요.

주목해야할 부분은 onCreate() 내부에

// These operations are small enough that
// we can generally safely run them on the UI thread.
basicCRUD(realm)
basicQuery(realm)
basicLinkQuery(realm)

// More complex operations can be executed on another thread.
ComplexBackgroundOperations(WeakReference(this)).execute()

이 부분입니다.

 

해당 함수에서 Realm Database 의 처리가 발생합니다.

또한 Database 가 변경되면 Listener 가 실행됩니다.

    private val realmChangeListener =
        OrderedRealmCollectionChangeListener { people: RealmResults<Person?>?, changeSet: OrderedCollectionChangeSet ->
            val insertions =
                if (changeSet.insertions.isEmpty()) "" else "\n - Insertions: " + Arrays.toString(
                    changeSet.insertions
                )
            val deletions =
                if (changeSet.deletions.isEmpty()) "" else "\n - Deletions: " + Arrays.toString(
                    changeSet.deletions
                )
            val changes =
                if (changeSet.changes.isEmpty()) "" else "\n - Changes: " + Arrays.toString(
                    changeSet.changes
                )
            showStatus("Person Database State : ${changeSet.state}")
            if (insertions != "" || deletions != "" || changes != "") {
                showStatus("Person was loaded, or written to. $insertions$deletions$changes")
            }
        }

해당 리스너에서 어떤 변화가 발생했는지 액티비티로 로그처럼 볼 수 있습니다.

showStatus 함수가 액티비티에 로그를 뿌려주는 역할을 합니다.

 

ComplexBackgroundOperations 함수는 지금은 API 30 에서 Deprecated 된 백그라운드 처리를 위한 AsyncTask 함수입니다.

(여담이지만 백그라운드 처리를 위해서 구글은 코루틴을 사용을 권장한다고 합니다.)

 

해당 AsyncTask 에서는 데이터베이스에 접근을 한번에 여러번 처리하는 복잡한 처리를 진행하게 됩니다. 그럴 때는 메인 스레드가 아닌 AsyncTask 처럼 다른 스레드에서 수행하는 것이 적절하다고 합니다.

 

지금까지 Realm Database 에 대해서 간단하게 알아봤습니다.

기본 example 이 Java 로 만들어져있어서 제가 Kotlin 으로 변경하는 작업을 진행했습니다.

생각보다 시간이 오래걸리더군요 ..

레퍼런스가 잘 되어있다고 하여 구글링하며 찾아봤는데 생각만큼 많지 않더라구요.(제가 구글링을 못하는 것도 한 몫을 하네요 ㅠ)

 

Realm Database 도 많이 사용하지만 구글에서 Room Database 를 더욱 추천하며 많이 사용한다고 합니다.

다음에는 Room Database 에 대해서 알아보고 비교해보는 스터디를 진행해보겠습니다.

 

이 글이 정확하지 않지만 누군가에게는 도움이 되면 좋을 것 같네요.

 

문제가 되는 부분이 있으면 피드백 환영해요~!

728x90

댓글