[Android] Room 이해 및 활용

WonseokOh·2022년 5월 6일
0

Android

목록 보기
7/16
post-thumbnail

Room이란?

  SNS를 사용하는 중에 네트워크 연결이 끊어졌을 때 새로운 사진들이 로딩 중이고 이전에 봤던 사진들은 계속 보이는 것을 경험해 본적이 다들 있을 것입니다. 이를 데이터 캐싱이라고 하며, 네트워크 액세스 할 수 없는 경우에도 로컬 데이터베이스의 데이터를 가지고 사용자들이 앱을 사용할 수 있도록 할 수 있습니다. Android에서는 SQLite라는 가벼운 관계형 데이터베이스가 네이티브 라이브러리에 포함되어 있습니다. 하지만 아래와 같은 이유로 인해 SQLite 보다 Jetpack 라이브러리에 포함된 Room 사용을 권장하고 있습니다.

  • SQL 쿼리에 대해서 올바르게 작성이 되었는지 컴파일 타임에 확인할 수 없습니다. 이로 인해 잘못된 쿼리 사용으로 영향을 받는 데이터가 생긴다면 오류를 직접 업데이트 해야합니다. 이 과정이 시간이 오래 걸리고 휴먼 에러를 발생시키기도 합니다.
  • SQL쿼리와 데이터 객체와의 변환이 자유롭지 못합니다. 쿼리를 통해 필터들을 각각 읽고 하나의 데이터 객체의 생성자로서 대입하기 때문에 상용구 코드들이 많이 사용될 수밖에 없습니다.

SQLite에 비해 데이터베이스 마이그레이션을 보다 쉽게 할 수 있습니다.

  다음과 같은 이유로 인해 안드로이드에서 로컬 데이터베이스를 사용하게 된다면 SQLite 데이터베이스에 있는 정보에 액세스하기 위한 추상화 계층인 Room 라이브러리를 사용해야 합니다. 즉 Room은 SQLite에 대한 추상화 계층(복잡한 SQLite로부터 핵심적인 기능들을 모아 제공하는 계층)을 제공하여 쉬운 데이터베이스 액세스와 동시에 SQLite를 완벽히 활용합니다. Room 라이브러리 학습 전에 SQLite를 사용해보시고 Room을 학습하신다면 라이브러리에 대한 이해가 한층 수월하고 라이브러리의 등장 배경을 느낄 수 있습니다.


Room 기본 구성요소

데이터베이스

데이터베이스는 앱에 저장되어 있는 로컬 데이터에 대한 액세스 포인트를 제공해주는 역할

DAO(Data Access Object)

DAO는 데이터베이스에 데이터를 추가, 삭제, 업데이트 작업을 할 수 있는 메소드를 제공해주는 역할, 그 외에도 다양한 쿼리 사용 가능

Entity

데이터베이스 내에 존재하는 테이블을 가리킵니다.

  데이터베이스 클래스는 데이터베이스와 연관된 DAO 인스턴스를 앱에 제공합니다. 이후 앱은 DAO를 통해서 테이블을 가리키는 Entity 인스턴스를 가져올 수 있습니다. 그리고 앱에서 Entity 클래스를 이용하여 테이블 내에 데이터를 추가하거나 수정합니다.


실습

  이제 Room을 직접 사용해보면서 구성요소들을 익힐 필요가 있습니다. 저는 Android CodeLab의 예시를 활용하여 실습을 하였습니다.

Entity 만들기

  위의 같이 word_table 이라는 이름을 가진 테이블을 생성합니다. 테이블 내의 컬럼은 문자열을 나타내는 word 하나로 테이블 내 기본키에 해당합니다. Room에서는 테이블을 가리키는 Entity 클래스를 정의하여 테이블을 나타낼 수 있고 클래스 내 프로퍼티로 테이블의 컬럼을 생성합니다.

@Entity(tableName = "word_table")
data class Word(
    @PrimaryKey @ColumnInfo(name = "word") val word: String
)

  Word 클래스를 Room 데이터베이스에서 활용하기 위해서는 Annotation을 사용하여 클래스와 데이터베이스간 연결을 만들어야 합니다. 특정 Annotation을 사용하여 클래스의 각 부분이 데이터베이스와 어떻게 연결되는지 알 수 있습니다.

@Entity

클래스 선언에 @Entitiy Annotation을 추가하여 해당 클래스가 SQLite의 테이블을 나타냅니다. 기본적으로 클래스 명이 테이블의 이름이 되지만 테이블의 이름을 변경하고 싶으면 괄호 내의 tableName = "테이블명" 을 입력하여 변경할 수 있습니다. 위의 예시에서는 테이블의 이름을 "word_table"로 지정하였습니다.

@PrimaryKey

테이블에는 기본적으로 데이터를 유일하게 구별할 수 있는 기본키가 존재합니다. Entity 내의 여러 속성 중 기본키를 지정하기 위해서는 다음 Annotation을 속성 앞에 선언하여 지정할 수 있습니다. 만약 하나의 속성이 아닌 여러 가지 속성으로 이루어진 복합 기본키를 지정하기 위해서는 @Entity 내에 primaryKeys = arrayOf("속성1","속성2") 와 같이 설정하시면 됩니다.

@ColumnInfo

테이블 내의 컬럼은 기본적으로 클래스 속성 이름으로 선언되지만 컬럼 명을 변경하고 싶으면 다음 Annotation에서 name = "컬럼 명" 을 통해 변경할 수 있습니다. Room에서 Entity의 속성으로 사용하기 위해서는 무조건 public으로 선언되어야 합니다.

@Ignore

다음 예시에는 활용되지 않은 Annotation이지만 가끔 활용될 수 있어 설명하게 되었습니다. @Entity 클래스는 모든 속성들로 컬럼을 생성하지만 테이블 내에 유지하고 싶지 않은 데이터가 있을 경우 속성 앞에 @Ignore Annotation을 붙이면 무시하게 됩니다.


DAO 만들기

  위에서 언급했듯이 DAO는 앱의 데이터베이스에 데이터를 추가, 삭제, 수정해주는 역할로서 SQL 쿼리를 메소드와 연결하여 호출할 수 있도록 합니다. Room에서는 직접 SQL문을 작성하지 않고 @Insert와 같은 Annotation을 활용하여 보다 쉽게 쿼리를 작성할 수 있습니다.

@Dao
interface WordDao {
    @Query("SELECT * FROM word_table ORDER BY word ASC")
    fun getAlphabetizedWords() : List<Word>

    @Query("SELECT * FROM word_table ORDER BY word ASC")
    fun getAlphabetizedWordsWithCoroutine() : Flow<List<Word>>

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun insert(word: Word)

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun insertWithCoroutine(word: Word)

    @Query("DELETE FROM word_table")
    fun deleteAll()

    @Query("DELETE FROM word_table")
    suspend fun deleteAllWithCoroutine()
}

  DAO는 기본적으로 추상 클래스 또는 인터페이스로 선언되어야 하며 SQLite와 마찬가지로 모든 쿼리는 별도의 스레드에서 실행되어야 합니다. Room에서는 Kotlin의 코루틴도 지원하여 suspend 함수를 사용하여 비동기 프로그래밍을 쉽게 구현할 수 있습니다. DAO에서도 다양한 Annotaion을 사용하여 보다 간편하게 쿼리 작성과 데이터베이스와의 연결을 만들 수 있습니다.

@Dao

Room의 Dao 클래스로 식별할 수 있도록 합니다.

@Insert

@Insert Annotation은 쿼리문을 제공하지 않아도 되는 특수 DAO Annotation 입니다. 데이터를 추가하는 Annotation으로 여기에서는 쓰이지 않지만 삭제(@Delete), 수정(@Update) 하는 Annotation도 존재합니다. @Insert Annotation 옆에 onConflict = "충돌 정책" 은 데이터 충돌 시 처리하는 정책을 추가하게 됩니다. IGNORE은 데이터가 충돌한다면 새로운 데이터는 무시하게 됩니다. 자세한 Conflict 정책은 공식문서에서 확인할 수 있습니다.

@Query

@Query는 SQL 쿼리문을 매개변수로 받아 복잡한 쿼리나 기타 작업을 할 수 있도록 합니다. 첫 번째 메소드는 word_table의 데이터를 오름차순으로 탐색하게 해주는 쿼리와 연결하였습니다. 쿼리는 컴파일 타임에 확인하여 잘못된 쿼리 작성이 있을 경우에는 컴파일 에러를 발생시킵니다. Room에서는 Entity의 속성과 쿼리의 컬럼명이 일치하지 않더라도 리턴을 하게 되는데 일부의 필드의 이름만 일치하지 않을 경우는 경고를 발생시키고 없는 필드가 있다면 오류를 발생시킵니다.


Room 데이터베이스 추가

  Room 데이터베이스는 추상 클래스이고 RoomDatabase를 상속하여 선언합니다. 일반적으로 앱 전체에는 Room 데이터베이스 인스턴스는 하나만 존재하면 되기 때문에 ApplicationContext를 사용하여 싱글톤으로 작성합니다.

@Database(entities = arrayOf(Word::class), version = 1, exportSchema = false)
public abstract class WordRoomDatabase : RoomDatabase() {
    abstract fun wordDao() : WordDao
    
    companion object{
        @Volatile
        private var INSTANCE: WordRoomDatabase? = null

        fun getDatabase(
            context: Context
        ): WordRoomDatabase{
            if(INSTANCE == null){
                synchronized(this){
                    val instance = Room.databaseBuilder(
                        context.applicationContext,
                        WordRoomDatabase::class.java,
                        "word_database"
                    ).build()
                    INSTANCE = instance
                }
            }
            return INSTANCE!!
        }
    }
}

  WordRoomDatabase 클래스는 RoomDatabase를 확장한 추상클래스로 @Database Annotation을 작성하여 Room 데이터베이스가 되도록 처리하였습니다. 해당 클래스 내부에는 DAO에 접근할 수 있는 추상 메소드가 존재하여 DAO를 반환할 수 있습니다. 데이터베이스에 여러 인스턴스가 생성되는 것을 방지하기 위해 WordRoomDatabase는 싱글톤으로 정의하였습니다. WordRoomDatabase를 처음 액세스 시 RoomDatabase Builder를 사용하여 ApplicationContext에서 RoomDatabase 객체를 만들고 이름을 "word_database"로 지정하였습니다.

@Database

  @Database는 Room 데이터베이스가 되도록 연결해주고 데이터베이스 내에 속한 테이블과 데이터베이스 버전 명을 추가로 입력합니다. 테이블은 entites에 Entity 클래스들을 지정하고 version은 최초 생성이니 1로 지정하였습니다. 데이터베이스의 스키마 변경 필요 시 데이터베이스 마이그레이션이 필요한데 이 때 버전도 기존의 버전보다 높여서 지정해야 합니다.

@Volatile

Room과 관련있는 Annotation은 아니지만 해당 Annotation이 지정된 속성은 Cache에서가 아닌 메인메모리에서 직접 읽고 쓰는 작업을 명시하도록 나타냅니다. 싱글톤 인스턴스에 지정하여 일관성을 보장할 수 있습니다.


데이터베이스 인스턴스화

  앱에서 Room 데이터베이스 인스턴스는 하나만 필요하게 되는데 가장 쉽게 인스턴스를 생성하는 방법은 Application 클래스의 멤버로 데이터베이스 인스턴스를 생성하는 것입니다.

class WordsApplication : Application() {
    val database by lazy { WordRoomDatabase.getDatabase(this) }
}
<application
        android:name=".WordsApplication"
        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.Roomexample">
        ...

Application을 만든 후 Manifest 에 name 속성에 다음과 같이 지정을 해야 적용이 됩니다.


정리

  Room이 SQLite에 비해 쿼리 작성이 간편하고 컴파일 타임에 오류를 확인할 수 있다는 측면에서 많은 장점이 있습니다. CodeLab에서는 MVVM 아키텍처 기반의 Repository Pattern을 사용하여 데이터 소스 자체를 추상화하고 있습니다. 그리고 모든 쿼리는 별도의 스레드에서 실행해야 하기 때문에 위 예시는 별도의 스레드를 생성해야 합니다. CodeLab에서는 코루틴을 사용하여 쉽게 쿼리를 사용하지만 학습 범위 이상인것 같아 다음에 코루틴을 별도로 정리해보려고 합니다. 해당 글은 여기서 마무리 되는 것이 아닌 새롭게 학습한 내용(ex. 마이그레이션, 테이블 관계 정의 등 )이 있을 때마다 추가하겠습니다.


참고

profile
"Effort never betrays"

0개의 댓글