[Android JetPack] 앱 속 DB, Room을 알아보자

이현우·2020년 11월 12일
0

JetPack Review

목록 보기
1/7
post-thumbnail

Room

원래 SQLite를 활용하여 어플리케이션 내부의 저장소(DB)를 만들어 저장을 했었다.

그러나 SQLite를 사용할 때에는

  • Query의 유효성 검사 기능을 제공하지 못했던 점
  • Scheme가 바뀔 떄 자동적으로 업데이트를 하지 못했던 점
  • ORM 지원이 안 되어 데이터를 객체로 변환시키기 위해 데이터 처리를 더 해야했던 점
  • Observer 패턴의 데이터(LiveData, RxJava)를 생성하고 동작시키기 위해 추가적인 Boiler Plate 코드를 작성해야하는 점

등을 추가적으로 투자해야 하므로, Android X 버전 이후로 어플리케이션 내부 DB로 Room을 추천하고 있다.

아래의 코드는 Kotlin으로 작성되었음을 미리 밝힙니다.

What is Room

SQLite의 추상화된 버전이라 생각하시면 조금 더 편할 것 같습니다.

이전에는 SQLite에 직접 데이터를 가공하여 DB에 맞는 형식으로 데이터를 CRUD하는 방식이었다면, Room에서는 객체로 데이터를 주고받을 수 있고 반응형 데이터로도 데이터를 주고받을 수 있게 설계가 되어있습니다!

그렇다면 Room은 어떻게 구성을 할 수 있을까요?

이번 예제에서는 다음과 같은 DB를 짜고 데이터를 입출력하는 기능을 만들어보려 합니다.

Entity: Database Table

@Entity(tableName = "daily_sleep_quality_table")
data class SleepNight(
        @PrimaryKey(autoGenerate = true)
        var nightId: Long = 0L,
        @ColumnInfo(name = "start_time_milli")
        val startTimeMilli: Long = System.currentTimeMillis(),
        @ColumnInfo(name = "end_time_milli")
        var endTimeMilli: Long = startTimeMilli,
        @ColumnInfo(name = "sleep_quality")
        var sleepQuality: Int = -1
)

데이터베이스의 Table(데이터 구조) 역할을 맡고 있는 Entity입니다. 이 테이블들이 모여서 나중에 살펴볼 Database를 구성할 것입니다.

Room에서의 Entity는 우리가 알고 있는 테이블 형식과 달리 Data Class의 형태로 구성되어있습니다. 이런 구조는 Room에서 데이터 입출력을 할 때 객체로 입출력을 한다는 것을 드러내는 가장 강력한 증거죠!

Entity는 어노테이션(@ 형태)로 클래스를 정의하는데 Entity를 구성하는 어노테이션을 살펴보도록 하죠

@Entity

이 클래스가 Entity임을 알려주는 어노테이션입니다. 다음과 같은 속성들을 사용할 수 있습니다.

  • tableName
    - Entity의 이름을 정해줍니다. default는 클래스 이름의 camelCase입니다.
  • primaryKeys = arrayOf()
    - Entity의 Primary Key가 N개일경우 어노테이션 속성으로 선언해줍니다.

@PrimaryKey

관계형 데이터베이스(RDB)를 다뤄보신 분들은 아실 그 용어죠? DB의 기본 키를 나타내는 어노테이션입니다. 클래스의 멤버변수에 사용합니다.

  • autoGenerate
    - 이 속성을 활용해서 DB Row의 id를 부여할 수 있습니다.

@ColumnInfo(name = "any_name")

관계형 DB의 Column의 이름을 변수명을 짓고 싶지 않을 때 사용하는 어노테이션입니다. 코틀린에서 변수 네이밍 컨벤션은 camelCase인데 DB의 네이밍 컨벤션은 대부분 snake_case죠?

그래서 이 부분을 변환하기 위해 이 어노테이션을 사용하는 것 같습니다.

DAO: Database Access Object

Room의 데이터를 접근하기 위해서는 Database를 접근할 수 있는 객체를 정의해서 그 객체를 통해 접근해야 합니다. 이 객체를 DAO라 합니다.

왜 데이터베이스에 직접 접근하지 않냐고 물어볼 수 있으나 이를 자세히 다룬다면 분명히 이 게시글의 길이는 끝없이 길어질 것이기에 유용한 설명을 해주고 있는 블로그 링크를 첨부하도록 하겠습니다.

그렇다면 DAO는 어떤 형태로 만들어질까요?

@Dao
interface SleepDatabaseDao {
    @Insert
    fun insert(sleepNight: SleepNight)

    @Update
    fun update(sleepNight: SleepNight)

    @Query("SELECT * FROM daily_sleep_quality_table WHERE nightId = :key")
    fun get(key: Long): SleepNight

    @Delete
    fun deleteAllNights(nights: List<SleepNight>): Int

    @Query("DELETE FROM daily_sleep_quality_table")
    fun clear()

    @Query("SELECT * FROM daily_sleep_quality_table ORDER BY nightId DESC")
    fun getAllNights(): LiveData<List<SleepNight>>

    @Query("SELECT * FROM daily_sleep_quality_table ORDER BY nightId DESC LIMIT 1")
    fun getToNight(): SleepNight?
}

DAO는 interface(혹은 추상 클래스)로 정의할 수 있습니다. 그리고 쿼리를 써야하는 기존 DB의 CRUD 방식과는 달리 조금 더 친절하게 데이터를 다룰 수 있도록 어노테이션을 기반으로 하여 데이터 입출력을 정의합니다.

@Insert

DB에 row 데이터를 삽입합니다. 위에서는 return 값을 정의 안 했지만, 만약 정의한다면 long 타입 삽입된 row의 id 값(여러 개를 삽입했으면 id들 long의 리스트 형)이 반환됩니다.

@Update

DB의 row 데이터를 수정합니다. Data Class 객체를 전달해준다면 id 값을 제외한 최소 한 가지 이상의 데이터가 수정된 채로 전달될 것이니 이를 기반으로 row 데이터를 수정합니다.

@Delete

DB의 row 데이터를 삭제합니다.

@Query("query")

기존 방식과 같이 query를 활용하여 데이터를 CRUD할 수 있게 만드는 어노테이션입니다. 위의 연산들보다 조금 더 복잡한 연산이 필요한 경우에 이런 쿼리 어노테이션을 활용하여 데이터를 가져올 수 있습니다.

Database

@Database(entities = [SleepNight::class], version = 1, exportSchema = false)
abstract class SleepDatabase : RoomDatabase() {
    abstract val sleepDatabaseDao: SleepDatabaseDao

    companion object {
        @Volatile
        private var INSTANCE: SleepDatabase? = null

        fun getInstance(context: Context): SleepDatabase {
            synchronized(this) {
                var instance = INSTANCE

                if(instance == null) {
                    instance = Room.databaseBuilder(
                            context.applicationContext,
                            SleepDatabase::class.java,
                            "sleep history database"
                    )
                            .fallbackToDestructiveMigration()
                            .build()
                    INSTANCE = instance
                }
                return instance
            }
        }
    }
}

RecyclerView의 어댑터 같은 느낌으로 Entity만큼 정의된 Dao 객체들을 반환할 수 있는 함수들을 가지고 있는 추상 클래스 형태로 정의합니다.

Room 데이터베이스에서 Dao를 가져와 이 객체를 통해 데이터를 CRUD한다

즉 Room Database는 수 많은 Dao들을 관리하기 위한 Dao Manager 클래스라고 생각하면 될 것 같습니다.

@Database

이 클래스가 Database임을 알려주는 어노테이션입니다.

  • entities
    - 이 DB에 어떤 테이블들이 있는 지 명시해줍니다.
  • version
    - Scheme가 바뀔 때 이 version도 바뀌어야 합니다.
  • exportSchema
    - Room의 Schema 구조를 폴더로 Export 할 수 있습니다. 데이터베이스의 버전 히스토리를 기록할 수 있다는 점에서 true로 설정하는 것이 좋습니다. 하지만 Test 상황에서는 굳이 true로 할 필요까진 없겠죠?

싱글턴 객체와 synchronized

  • 싱글턴 객체
    - Database 객체를 생성하는 것 자체가 리소스를 상당히 많이 소비하는 작업이기 때문에 데이터베이스는 전체 프로젝트에서 하나만 생성하는 싱글턴 객체로 선언을 해야합니다.
    • 그러나 멀티 스레딩 환경(동시에 여러 유저들이 DB로 접근할 수 있는 상황)에서는 DB에 동시에 조회하여 데이터들을 Update 하는 경우, 데이터베이스의 건전성에 문제가 발생할 수 있습니다.
    • 따라서, 스레드 하나만 들어올 수 있게 DB 객체는 synchronized로 스레드를 동기화 시켜버립니다.

조금 더 알아보고 싶다면

안드로이드 공식 문서

profile
이현우의 개발 브이로그

0개의 댓글