본문 바로가기
오늘은 뭘 배울까?/Android

안드로이드 Room의 사용법과 예제

by Kim Juhwan 2021. 4. 3.

1. Room
   1-1. Room이란?
   1-2. Room 구조
   1-3. TMI
2. 사용법
   2-1. gradle
   2-2. Entity
   2-3. DAO
   2-4. Room Database
   2-5. 데이터 베이스 사용
3. 예제
   3-1. room + singleton + coroutine
   3-2. room

 

 

 


 

1. Room

1-1. Room이란?

Room은 스마트폰 내장 DB에 데이터를 저장하기 위해 사용하는 라이브러리이다.

평소에 우리는 메모를 저장하고, 일정을 저장하고, 즐겨보는 웹툰을 즐겨찾기 하고... 사용자의 데이터를 내장 DB에 저장할 일이 많다. 과거에는 SQLite라는 데이터베이스 엔진을 이용해 데이터를 저장했으나 다음과 같은 단점이 있었다

 

SQLite의 문제점

한마디로 사용하기 어렵다는 뜻이다. Room은 이러한 문제들을 자동으로 처리할 수 있도록 도와주는 놈이다.

Room은 완전히 새로운 개념은 아니고, SQLite를 활용해서 객체 매핑을 해주는 역할을 한다.

아무튼 이러한 이유들로 구글에서는 SQLite 대신 Room을 사용할 것을 권장하고 있다.

 

1-2. Room 구조

Room 구조

(처음에 Rest of The App을 앱의 휴식이라고 해석한 건 나뿐인 건가 🙈)

위 사진에서 Room Database, Data Access Objects, Entities 이렇게 3개가 Room의 구성 요소이고

Rest of The App은 앱의 나머지 부분을 뜻한다.

각 요소에 대한 설명은 [목차 2]에서 다룰 예정이다.

 

1-3. TMI

정말 정말 간단한 정보를 저장할 경우를 생각해보자.

예를 들어 자동 로그인 여부를 저장하고 싶은데 고작 이 true/false 값을 저장하려고 Room을 사용하는 건 닭 잡는데 소 잡는 칼 쓰는 격이다. 별것도 아닌 거에 큰 노력을 들여야 한다는 것이다.

이럴 때는 Room이 아니라 sharedpreferences라는 것을 사용하면 된다.

 

sharedpreferences에 대해서는 추후에 포스팅 예정!

 

반대로 대량의 데이터를 처리하게 될 경우는 Room보다 Realm을 사용하면 좋다. 속도도 빠르고 안정적이고 비동기 지원이 된다는 장점이 있으나 앱 용량이 커진다는 단점이 있어 상황에 맞게 사용하면 된다.

 

 

2. 사용법

2-1. gradle

dependency 추가

gradle을 추가하는 가장 쉬운 방법은 액티비티에서 Room이라고 적고 Alt+Enter 해서 추가하는 방법이다.

 

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

그러면 gradle에 이렇게 2개의 문장이 들어간 걸 확인할 수 있다.

하지만 만약 코틀린을 사용한다면 여기서 몇 가지 설정을 더 해주어야 한다.

(Android 4.1.2 / Room 2.2.6 / 2021.04.03 기준)

 

2021-11-17 추가 내용
오랜만에 Room을 사용해보려고 했는데
implementation 'androidx.room:room-runtime:2.2.6'이 자동으로 추가가 되지 않는다.
글을 작성할 때 내가 헷갈렸던건지 바뀐건지 모르겠지만 수동으로 추가해주어야 한다.
만약 추가하지 않으면
cannot find implementation for database. database_impl does not exist 이런 에러가 뜬다.

 

    implementation 'androidx.room:room-runtime:2.2.6'
    kapt 'androidx.room:room-compiler:2.2.6'

annotationProcessor는 코틀린에서 kapt 컴파일러 플러그인과 함께 사용하도록 되어있으므로

annotationProcessor -> kapt로 변경하고

 

plugins {
    id 'kotlin-kapt'
}

gradle의 맨 앞부분에 플러그인을 추가해주면 된다.

 

2-2. Entity

uid(Primary Key) name age phone
1 박보영 32 010-1111-2222
2 이지은 29 010-1111-3333
3 김세정 26 010-1111-4444

한국말로 하면 '개체'인 Entity는 관련이 있는 속성들이 모여 하나의 정보 단위를 이룬 것이다.

예를 들어 위와 같이 사람의 이름, 나이, 번호라는 속성이 모여서 하나의 정보 단위를 이루면 이것을 Entity라고 한다.

 

Entity(개체)와 Object(객체)는 비슷해 보이지만 다른 의미를 가지고 있다.
객체는 개체를 포함한 더 큰 개념이다.
대상에 대한 정보뿐만 아니라 동작, 기능, 절차 등을 포함하는 것이 객체이다.
이에 관련해서는 관련 서적을 읽고 따로 정리해서 포스팅할 예정이다.

 

@Entity
data class User (
    var name: String,
    var age: String,
    var phone: String
){
    @PrimaryKey(autoGenerate = true) var id: Int = 0
}

아무튼 그래서 Entity를 생성해야 한다. (데이터베이스 테이블을 만든다고 생각하자)

data class에 @Entity 어노테이션을 붙여주고 저장하고 싶은 속성의 변수 이름과 타입을 정해준다.

primaryKey는 키 값이기 때문에 유일한(Unique) 값이어야 한다. 직접 지정해도 되지만 autoGenerate를 true로 주면 자동으로 값을 생성한다.

 

어노테이션이란 '@'가 붙은 것들을 말하는데, 데이터를 설명하는 데이터이다. 

 

테이블의 이름은 따로 정하지 않으면 클래스 이름을 사용하게 되는데

만약 테이블 이름을 정해주고 싶다면 @Entity(tableName="userProfile") 이렇게 하면 된다.

 

2-3. DAO

니가 왜 거기서 나와

다오(DAO)

정통파 드라이버. 정의로운 성격으로 인해 레이싱 중 남을 공격하거나 비정상적인 방법으로 승리를 쟁취하기 꺼려한다.

어려서부터 아버지의 일을 돕기 위해 카트를 종종....

은 장난이고 (아이고 부장님 깔깔깔 🤣)

 

Data Access Object의 줄임말이다.

데이터에 접근할 수 있는 메서드를 정의해놓은 인터페이스이다.

 

DAO에 대한 더 자세한 설명은 여기에서 확인할 수 있다.

 

@Dao
interface UserDao {
    @Insert
    fun insert(user: User)

    @Update
    fun update(user: User)

    @Delete
    fun delete(user: User)
}

Dao는 이렇게 생성하면 된다. 우선 class가 아니라 interface임에 유의하자.

맨 위에 @Dao 어노테이션을 붙이고 그 안에 메서드를 정의하게 되는데

@Insert를 붙이면 테이블에 데이터 삽입

@Update를 붙이면 테이블의 데이터 수정

@Delete를 붙이면 테이블의 데이터 삭제이다.

 

그렇다면 만약 삽입/수정/삭제 외에 다른 기능을 하는 메서드를 만들고 싶다면 어떻게 해야 할까?

테이블에 있는 값 전부 불러오기라든지, 특정 이름을 가진 사람 삭제라든지 하고 싶을 수도 있지 않은가?

 

@Dao
interface UserDao {
    @Query("SELECT * FROM User") // 테이블의 모든 값을 가져와라
    fun getAll(): List<User>

    @Query("DELETE FROM User WHERE name = :name") // 'name'에 해당하는 유저를 삭제해라
    fun deleteUserByName(name: String)
}

그런 경우는 @Query 어노테이션을 붙이고 그 안에 어떤 동작을 할 건지 sql 문법으로 작성을 해주어야 한다.

 

예를 들어 해석해보자면

@Query("SELECT * FROM User")

-> User라는 테이블로부터(=FROM) 모든 값을(=*) 가져와!(=SELECT)라는 뜻이다.

@Query("DELETE FROM User WHERE name= :name")

-> User라는 테이블로부터 name인 것을 찾아서 지워라는 뜻이다.

 

@Ignore와 onConflict = OnConflictStrategy.REPLACE도 유용하게 쓰일 수 있을 것 같은데
아직 테스트해보지 않았다. 해보고 수정해야겠다.
sql 문법을 잘 활용하면 더 복잡한 메서드도 만들 수 있다.

 

2-4. Room Database

@Database(entities = [User::class], version = 1)
abstract class UserDatabase: RoomDatabase() {
    abstract fun userDao(): UserDao
}

이번에는 데이터베이스를 생성하고 관리하는 데이터베이스 객체 만들기 위해서 위와 같은 추상 클래스를 만들어 줘야 한다. 우선 RoomDatabase 클래스를 상속받고, @Database 어노테이션으로 데이터베이스임을 표시한다.

 

어노테이션 괄호 안을 보면 entities가 있는데 여기에 [목차 2-2]에서 만든 entity를 넣어주면 된다.

version은 앱을 업데이트하다가 entity의 구조를 변경해야 하는 일이 생겼을 때 이전 구조와 현재 구조를 구분해주는 역할을 한다. 만약 구조가 바뀌었는데 버전이 같다면 에러가 뜨며 디버깅이 되지 않는다.

처음 데이터베이스를 생성하는 상황이라면 그냥 1을 넣어주면 된다.

 

@Database(entities = arrayOf(User::class, Student::class), version = 1)
abstract class UserDatabase: RoomDatabase() {
    abstract fun userDao(): UserDao
}

만약 하나의 데이터 베이스가 여러 개의 entity를 가져야 한다면 arrayOf() 안에 콤마로 구분해서 entity를 넣어주면 된다.

 

 

주의사항

공식문서에서는 데이터베이스 객체를 인스턴스 할 때 싱글톤으로 구현하기를 권장하고 있다.

일단 여러 인스턴스에 액세스를 꼭 해야 하는 일이 거의 없고, 객체 생성에 비용이 많이 들기 때문이다.

 

이 부분에 대한 이해가 잘 안 된다면 다음 목차로 넘어가도 당장 문제가 생기거나 하진 않는다.
하지만 신경 써야 할 부분이고, 알아 두어야 할 부분이라는 걸 기억해두자

 

@Database(entities = [User::class], version = 1)
abstract class UserDatabase: RoomDatabase() {
    abstract fun userDao(): UserDao

    companion object {
        private var instance: UserDatabase? = null

        @Synchronized
        fun getInstance(context: Context): UserDatabase? {
            if (instance == null) {
                synchronized(UserDatabase::class){
                    instance = Room.databaseBuilder(
                        context.applicationContext,
                        UserDatabase::class.java,
                        "user-database"
                    ).build()
                }
            }
            return instance
        }
    }
}

그래서 위와 같이 companion object로 객체를 선언해서 사용하면 된다.

싱글톤으로 구현하지 않을 거라면 저 코드 부분을 호출할 부분에서 사용하면 된다.

 

객체를 생성할 때 databaseBuilder라는 static 메서드를 사용하는데

context와, database 클래스 그리고 데이터 베이스를 저장할 때 사용할 데이터베이스의 이름을 정해서 넘겨주면 된다.

다른 데이터베이스랑 이름이 겹치면 꼬여버리니 주의하자

 

실제로 이름을 겹치게 사용한 적이 있었다. 그로 인해 생겼던 문제에 대해 게시물로 작성한 적이 있다.
synchronized에 대해서는 다음에 적어야겠다. 내용이 길어져서 너무 지침 ㅠㅠ

 

2-5. 데이터 베이스 사용

        var newUser = User("김똥깨", "20", "010-1111-5555")

        // 싱글톤 패턴을 사용하지 않은 경우
        val db = Room.databaseBuilder(
                applicationContext,
                AppDatabase::class.java,
                "user-database"
        ).build()
        db.UserDao().insert(newUser)

        // 싱글톤 패턴을 사용한 경우
        val db = UserDatabase.getInstance(applicationContext)
        db!!.userDao().insert(newUser)

[목차 2-4]에서 싱글톤 패턴 사용 유무에 따라 위와 같이 사용해주면 된다.

이렇게 하면 [목차 2-3]에서 insert를 새로운 데이터를 삽입하는 메서드로 정의했었기 때문에 newUser가 데이터베이스에 추가된다.

 

는 훼이크

여기서 끝이 났으면 좋겠지만 사실 끝이 아니다.

저대로 실행하면 "Cannot access database on the main thread since it may potentially lock the UI for a long period of time" 에러가 뜬다.

쉽게 말하자면 "야 이거 오래 걸리는 작업이잖아, 나 바쁘니까 다른 애한테 시켜"정도로 해석할 수 있다.

 

더 설명하기엔 너무 길어지니 비동기 개념과 코루틴에 대해 적었던 게시물을 확인하자 

 

        var newUser = User("김똥깨", "20", "010-1111-5555")

        // 싱글톤 패턴을 사용하지 않은 경우
        val db = Room.databaseBuilder(
                applicationContext,
                AppDatabase::class.java,
                "user-database"
        ).allowMainThreadQueries() // 그냥 강제로 실행
                .build()
        db.UserDao().insert(newUser)

        // 싱글톤 패턴을 사용한 경우
        val db = UserDatabase.getInstance(applicationContext)
        CoroutineScope(Dispatchers.IO).launch { // 다른애 한테 일 시키기
            db!!.userDao().insert(newUser)
        }

이제 선택권이 2가지가 있다.

 

어딜 감히 컴퓨터 주제에 인간의 명령을 거부해? 😤

-> allowMainThreadQueries()를 사용해 강제로 실행시킨다

-> 이 경우 나중에 문제가 생길 수 있다. Room을 한 번 사용해보는 학습단계에서는 써도 무방하다.

 

어쩔 수 없지 뭐, 다른 애한테 부탁해볼까? 🙄

-> 비동기 실행을 하면 된다.

-> 비동기 실행에는 여러 가지 방법이 있으나 예제에서는 코루틴을 사용했다.

 

3. 예제

3-1. room + singleton + coroutine

 

juhwankim-dev/SelfStudy

코틀린으로 공부한 것들을 올리는 공간입니다. Contribute to juhwankim-dev/SelfStudy development by creating an account on GitHub.

github.com

내가 Room을 처음 배웠을 때 분명히 설명 잘되어있는 글을 읽어봐도 이해가 안 가는 부분이 많았다.

그래서 급하게 만들어본 예제를 깃허브에 올려두었다.

이름, 나이, 번호를 입력하고 DB에 저장하는 간단한 기능만 구현해두었다.

 

3-2. room

 

juhwankim-dev/SelfStudy

코틀린으로 공부한 것들을 올리는 공간입니다. Contribute to juhwankim-dev/SelfStudy development by creating an account on GitHub.

github.com

싱글톤이나 코루틴에 대해 아직 모르시는 분들은 Room 배우기도 벅찰 수도 있기 때문에 두 개념을 제외시키고 메인 스레드를 이용해서 아주 간단한 예제도 만들어 보았다.

도움이 되길...

 

 


💡 느낀 점

  • Room에 대해 다 공부했다고 생각했는데 포스팅을 하다 보니 새롭게 알게 된 내용도 많았다.
  • 여태까지 중복검사를 DB 읽어오고 나서 했었는데 sql 문법으로 해결 가능한 거였다니.. 멍청이..
  • Migration는 아직 쓸 일이 없어 공부하지 못했는데 얼른 공부해봐야지...
  • 처음 배웠을 때의 기억을 되살리면서 쉽게 설명한다고 노력했는데 어떨지 모르겠다.

📘 참고한 자료


반응형

댓글