[안드로이드] Room

J·2023년 3월 20일
1

안드로이드

목록 보기
21/29

1. Room

1-1. Room이란..?

Room은 스마트폰 내장 DB에 데이터를 저장하기 위해 사용하는 라이브러리이다.
과거에는 SQLite라는 데이터베이스 엔진을 사용해 데이터를 저장했으나, 현재는 사용하기 어려워 직접적으로 쓰지는 않는다.
Room은 SQLite의 문제점을 자동으로 처리할 수 있도록 도와주는 역할을 하는데,
완전히 새로운 개념이 아니라 SQLite를 활용해서 객체 매핑을 하는 역할을 한다.

이러한 이유들로 인해서 구글에서는 Room사용을 권장하고 있다.

1-2. Room의 구조


위 사진에서 Room Database, Data Access Objects, Entities 이렇게 3개가 Room의 구성요소이고,
Rest of The App은 앱의 나머지 부분을 뜻한다.

1-3. TMI

정말 정말 간단한 정보를 저장할 경우를 생각해보자.
예를 들어서 자동 로그인 여부를 저장하고 싶은데, 고작 이 true/false 값을 저장하려고 Room을 사용하는 것은
닭을 잡는데 소를 잡는 칼을 쓰는 격이다.
별 것도 아닌 것에 큰 노력을 들여야 한다는 것이다.
이럴 때는, Room이 아니라 SharedPreferences 라는 것을 쓰면 된다.

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

2. 사용법

2-1. gradle

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

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

그러면 이렇게 gradle에 2개의 문장이 추가된 것을 확인할 수 있다.
(하지만, 코틀린 사용시 여기에서 몇 가지 설정을 더 해줘야 한다고 함. 하지만 난 아직 코틀린으로 작업하지 않으므로 Pass)

2-2. Entity

Entity의 한국어 뜻은 '개체'로, 관련이 있는 속성끼리 모여서 하나의 '정보'를 이룬 것을 말한다.
예를 들어서, 사람의 이름, 나이, 전화번호 라는 속성이 모여서 하나의 정보 단위를 이루면 그것을 'Entity'라고 한다.

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

아무튼 그래서 Entity를 생성해야 한다.
(DB 테이블을 만든다고 생각하면 쉽다~~)

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

data class에 @Entity 어노테이션을 붙여주고 저장하고 싶은 속성의 변수 이름과 타입을 정해준다.
primaryKey는 키 값이기 때문에 유니크(Unique) 해야 한다.
직접 지정해도 되지만, autoGenerate를 true로 주면 자동으로 값을 생성한다.

2-3. DAO

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

@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문법으로 작성해주면 된다.

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 클래스 그리고 데이터베이스를 저장할 때 사용할 데이터베이스의 이름을 정해서 넘겨주면 된다.
(다른 데이터베이스와 이름이 겹치면 꼬이니 주의하자!!)

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가지가 있다.

  1. allowMainThreadQueries()를 사용해 강제로 실행시킨다.
    => 이 경우, 나중에 문제가 생길 수 있다. Room을 한번 사용해보는 학습단계에서는 써도 무방하다.

  2. 비동기 실행을 시킨다.
    => 비동기 실행에는 여러 가지 방법이 있다. 대표적으로 많이 사용하는 비동기에는 '코루틴'이 있다.

0개의 댓글