Android

[Android] 룸(Room) 지속성 라이브러리

728x90

 

 

룸(Room)이란?

 안드로이드 앱에서 SQLite 데이터베이스를 쉽고 편리하게 사용할 수 있도록 하는 기능이다. SQLite 위에 만든 구글의 ORM(Object-relational mapping)이다. 룸을 사용하면 앱의 단일 정보 소스로 제공되는 캐시를 통해 인터넷 연결 여부와 관계없이 앱에 있는 주요 정보의 일관된 사본을 볼 수 있다.

 

룸의 구성요소 (Database, Entity, Dao)

1. Database

 데이터베이스 홀더를 포함하며 앱의 지속적인 관계형 데이터에 대한 기본 연결을 위한 기본 Access Point 역할을 한다. @Database로 처리된 클래스는 다음과 같은 조건을 충족해야 한다.

  • RoomDatabase를 확장하는 추상 클래스여야 한다.
  • 어노테이션 내에 데이터베이스와 연결된 엔티티의 목록을 포함해야 한다.
  • 인수가 0개인 추상 메서드를 포함하고 @Dao로 처리된 클래스를 반환해야 한다.

 

 

@Database(entities = arrayOf(User::class), version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}
val db = Room.databaseBuilder(
    applicationContext,
    AppDatabase::class.java, "database-name"
).build()

 

 앱이 단일 프로세스에서 실행되는 경우 RoomDatabase 인스턴스는 리소스를 상당히 많이 소비하기 때문에 싱글톤 디자인 패턴에 따라 인스턴스화 해야 한다.

싱글톤 디자인 패턴이란?
인스턴스가 프로그램 내에서 오직 하나만 생성되는 것을 보장하고, 프로그램 어디서든 인스턴스에 접근할 수 있도록 하는 패턴

 

 

2. Entity

 데이터베이스 내의 테이블을 나타낸다. Room을 사용할 때 관련 필드 집합을 엔티티들로 정의한다. 각 엔티티에 대한 항목을 보관하기 위해 연결된 데이터베이스 객체 내에 테이블이 생성된다.

@Entity
data class User (
    @PrimaryKey 
    val uid: Int,
    
    @ColumnInfo(name = "first_name")
    val firstName: String?,
    
    @ColumnInfo(name = "last_name")
    val lastName: String?
)   

 

Primary key 사용

 각 Entity는 하나 이상의 필드를 기본 키로 정의해야 한다. 자동 ID를 할당하려면 @PrimaryKey의 autoGenerate 속성을 설정하면 된다. 복합 기본키로 @Entity 어노테이션의 primaryKeys 속성을 사용한다.

@Entity(primaryKeys = arrayOf("firstName", "lastName"))
data class User (
    val firstName: String?,
    
    val lastName: String?
)

 

이름 지정

 tableName 속성을 사용해서 테이블의 이름을 다르게 지정할 수 있다. 컬럼의 이름은 @ColumnInfo를 통해 지정할 수 있다.

@Entity(tableName = "users")
data class User (
    @PrimaryKey val id: Int,
    
    @ColumnInfo(name = "first_name") 
    val firstName: String?,
    
    @ColumnInfo(name = "last_name") 
    val lastName: String?
)

 

필드 무시

 기본적으로 Room은 Entity에 정의된 각 필드의 컬럼을 생성한다. Entity에 유지하지 않으려는 필드가 있으면 @Ignore를 사용한다. 상위 필드를 상속하면 일반적으로 @Entity 속성의 ignoreColumns 속성을 사용한다.

@Entity
data class User(
    @PrimaryKey val id: Int,
    val firstName: String?,
    
    val lastName: String?,
    
    @Ignore 
    val picture: Bitmap?
)

 

특정 컬럼 Index 생성

 Index를 추가해 쿼리속도를 높일 수 있다. @Entity 내의 indices 속성을 통해 색인, 복합 색인을 나열한다. unique 속성을 true로 설정해 제약조건을 표기한다.

@Entity(indices = arrayOf(Index(value = ["first_name", "last_name"], unique = true)))
data class User(
    @PrimaryKey
    val id: Int,
    
    @ColumnInfo(name = "first_name")
    val firstName: String?,
    
    @ColumnInfo(name = "last_name") 
    val lastName: String?,
    
    @Ignore 
    var picture: Bitmap?
)

 

 

3. Data Access Objects(DAO)

 DAO는 데이터베이스에 엑세스하는 데 사용되는 메서드를 가지고 있다. 인터페이스로서 쿼리를 사용하는 메서드를 정의한다.

 각 DAO에는 앱의 데이터베이스에 대한 추상적 엑세스를 제공하는 방법이 포함되어 있으므로(인터페이스내에 쿼리와 함께 함수만 정의) 이 DAO 객체들은 룸의 주요 구성 요소를 형성한다.

 쿼리 빌더나 직접적인 쿼리 대신 DAO 클래스를 사용하여 데이터베이스에 접근하여 데이터베이스 구조의 다양한 구성 요소를 분리할 수 있다. 또한, DAO를 사용하면 애플리케이션을 테스트할 때 데이터베이스 접근을 쉽게 할 수 있다.

 DAO는 인터페이스 또한 추상 클래스일 수 있다. 추상 클래스인 경우 선택적으로 RoomDatabase를 유일한 매개 변수로 사용하는 생성자를 가질 수 있다.

 

Insert

 @Insert 어노테이션을 지정하면 Room은 단일 트랜잭션의 데이터베이스에 모든 매개변수를 삽입하는 구현을 생성한다.

@Dao
interface MyDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertUsers(vararg users: User)

    @Insert
    fun insertBothUsers(user1: User, user2: User)

    @Insert
    fun insertUsersAndFriends(user: User, friends: List<User>)
}

 

 테이블에 엔티티를 삽입할 때 같은 값인 경우 충돌이 발생하는데 onConflict 속성을 사용해 이 충돌을 어떻게 해결할지를 정의할 수 있다. REPLACE로 지정하면 충돌 발생 시 새로 들어온 데이터로 교체한다.

 

Update

 데이터베이스에서 매개 변수로 지정된 엔티티 집합을 수정한다.

@Dao
interface MyDao {
    @Update
    fun updateUsers(vararg users: User)
}

 

Delete

 매개변수로 지정된 엔티티 집합을 데이터베이스에서 제거한다. 기본키를 사용하여 삭제할 엔티티를 찾는다.

@Dao
interface MyDao {
    @Delete
    fun deleteUsers(vararg users: User)
}

 

Query

 데이터베이스에서 읽기/쓰기 작업을 수행할 수 있다. 각 @Query 메서드는 Compile time에 확인되므로 쿼리에 문제가 있으면 Runtime Error 대신 Compile Error가 발생한다. 

 룸은 반환된 객체의 필드 이름이 쿼리 응답의 해당 컬럼 이름과 일치하지 않는 두 가지 방법 중 하나로 경고를 표시한다.

  • 일부 필드 이름만 일치하는 경우 경고 표시
  • 필드 이름이 일치하지 않으면 오류 발생
@Dao
interface MyDao {
    @Query("SELECT * FROM user")
    fun loadAllUsers(): Array<User>
}

 

Query에 매개변수 전달

 Query에 매개변수를 전달할 때는 아래의 예시처럼 :minAge 이런 식으로 작성해줘야 한다. 컴파일 시 :minAge bind 매개변수와 minAge 메서드 매개변수를 일치시킨다. 

@Dao
interface MyDao {
    @Query("SELECT * FROM user WHERE age > :minAge")
    fun loadAllUsersOlderThan(minAge: Int): Array<User>
    
    @Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
    fun loadAllUsersBetweenAges(minAge: Int, maxAge: Int): Array<User>

    @Query("SELECT * FROM user WHERE first_name LIKE :search " +
           "OR last_name LIKE :search")
    fun findUserWithName(search: String): List<User>
}

 

컬럼의 부분집합 반환

 대부분의 경우 엔티티의 몇 가지 필드만 가져와야 하는데, 앱 UI에 표시되는 컬럼만 가져오면 리소스가 절약되고 쿼리도 더 빨리 완료될 수 있다.

data class NameTuple(
    @ColumnInfo(name = "first_name") 
    val firstName: String?,
    
    @ColumnInfo(name = "last_name") 
    val lastName: String?
)

@Dao
interface MyDao {
    @Query("SELECT first_name, last_name FROM user")
    fun loadFullName(): List<NameTuple>
}

 

 first_name, last_name 컬럼에 대한 값을 반환하고 이 값을 NameTuple 클래스의 필드에 매핑한다.

 

인수 컬렉션 전달

아래의 예시처럼 (:regions) 형태로 Collection도 전달할 수 있다.

@Dao
interface MyDao {
    @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
    fun loadUsersFromRegions(regions: List<String>): List<NameTuple>
}

 

Observable 쿼리

 쿼리의 리턴값으로 LiveData를 사용하면 데이터 변경 시 앱 UI를 자동으로 업데이트 할 수 있다.

@Dao
interface MyDao {
    @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
    fun loadUsersFromRegionsSync(regions: List<String>): LiveData<List<User>>
}

 

여러 테이블 쿼리

 여러 테이블에 엑세스 해야할 때 테이블을 조인한다.

@Dao
interface MyDao {
    @Query(
        "SELECT * FROM book " +
        "INNER JOIN loan ON loan.book_id = book.id " +
        "INNER JOIN user ON user.id = loan.user_id " +
        "WHERE user.name LIKE :userName"
    )
    fun findBooksBorrowedByNameSync(userName: String): List<Book>
}

 

 

엔티티 간 관계 정의

1. 일대다 관계 정의

 직접적인 관계를 활용할 수는 없지만 항목 간 외래 키 제약 조건을 정의할 수 있다.

@Entity(tableName = "book"
    foreignKeys = arrayOf(
        ForeignKey(
            entity = User::class, 
            parentColumns = arrayOf("id"), 
            childColumns = arrayOf("user_id"))
    )
)
data class Book(
    @PrimaryKey 
    val bookId: Int,
    
    val title: String?,
    
    @ColumnInfo(name = "user_id")
    val userId: Int
)

 

 user_id 외래 키를 통해 User의 단일 인스턴스에 0개 이상의 Book 인스턴스를 연결할 수 있으므로 이를 활용해 User와 Book간의 일대다 관계를 모델링 할 수 있다.

 

2. 또 다른 엔티티를 내포하는 오브젝트 만들기

 @Embedded 어노테이션을 사용해 테이블 내의 하위 필드를 가지고 있는 엔티티를 만들 수 있다.

data class Address(
    val street: String?,
    
    val state: String?,
    
    val city: String?,
    
    @ColumnInfo(name = "post_code") 
    val postCode: Int
)

@Entity
data class User(
    @PrimaryKey 
    val id: Int,
    
    @ColumnInfo(name = "first_name") 
    val firstName: String?,
    
    @Embedded 
    val address: Address?
)

 

3. 다대다 관계 정의

다대다 관계를 정의하기 위해서는 세 가지 엔티티를 생성해야 한다.

@Entity
data class Playlist(
    @PrimaryKey 
    var id: Int,
    
    val name: String?,
    
    val description: String?
)

@Entity
data class Song(
    @PrimaryKey 
    var id: Int,
    
    @ColumnInfo(name = "song_name")
    val songName: String?,
    
    @ColumnInfo(name = "artist_name")
    val artistName: String?
)

 

@Entity(tableName = "playlist_song_join",
    primaryKeys = arrayOf("playlistId","songId"),
    foreignKeys = arrayOf(
        ForeignKey(entity = Playlist::class,
                   parentColumns = arrayOf("id"),
                   childColumns = arrayOf("playlistId")),
        ForeignKey(entity = Song::class,
                   parentColumns = arrayOf("id"),
                   childColumns = arrayOf("songId"))
    )
)
data class PlaylistSongJoin(
    val playlistId: Int,
    
    val songId: Int
)

 

 

복잡한 데이터 참조

TypeConverter

 룸은 primitive type과 wrapping 타입만 지원하므로 이 외의 다른 타입을 사용할 경우엔 TypeConverter를 사용해서 type을 치환해야 한다.

class Converters {
    @TypeConverter
    fun fromTimestamp(value: Long?): Date? {
        return value?.let { Date(it) }
    }

    @TypeConverter
    fun dateToTimestamp(date: Date?): Long? {
        return date?.time?.toLong()
    }
}

 

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

 

Converters 클래스를 정의한 후 Database 클래스에 @TypeConverters 어노테이션을 추가해 TypeConverter를 사용할 수 있다.

@Entity
data class User(private val birthday: Date?)

 

728x90