Room Database를 이용한 안드로이드 데이터 관리

Room Database

안드로이드 데이터베이스에는 여러가지 옵션이 있지만, 가장 널리 사용되는 것은 SQLite입니다. SQLite는 간단하면서도 효과적인 관계형 데이터베이스이지만, 복잡한 쿼리를 작성하거나 데이터베이스를 적절하게 유지 관리하는 데에는 다소 어려움이 있습니다.

이러한 어려움을 해결하기 위해 Google은 Room Database를 소개하였습니다. Room은 SQLite의 추상화 레이어를 제공하여, 더 간편하고 직관적인 데이터베이스 작업을 가능하게 합니다. 이 글에서는 Room의 주요 특징과 그 사용법을 자세히 살펴보도록 하겠습니다.

Room은 안드로이드 Jetpack의 일부로 Google에서 제공하는 SQLite의 추상화 라이브러리입니다. 이를 통해 SQL 작성 없이 데이터베이스 작업을 수행하며, 컴파일 시간에 쿼리의 유효성을 검사할 수 있습니다.

Room의 주요 구성요소

Room Database를 이해하는 데는 세 가지 핵심 구성요소를 알아야 합니다: Entity, DAO, 그리고 Database입니다.

  • Entity: 데이터베이스 내의 테이블을 나타내는 클래스입니다. 각 필드는 테이블의 열을 나타냅니다.
  • DAO (Data Access Object): 데이터베이스 액세스를 처리하는 메소드를 포함하는 인터페이스 또는 추상 클래스입니다. 이는 SQL 쿼리를 메소드에 매핑하여 애플리케이션 로직에서 데이터베이스 작업을 처리합니다.
  • Database: 데이터베이스 홀더를 포함하며 앱의 영구 저장소에 액세스하는 주요 엑세스 지점 역할을 합니다. 이는 RoomDatabase를 확장한 추상 클래스로서, 데이터베이스와 DAO를 연결합니다.

Room의 작동 원리

Room Database는 애플리케이션의 데이터를 SQLite를 이용해 영구적으로 저장하고, 이 데이터에 액세스할 수 있게 도와줍니다. Entity 클래스를 사용해 테이블을 정의하고, DAO를 통해 이 테이블에 접근하게 됩니다. 이렇게 해서 애플리케이션 내의 데이터를 보다 안전하게, 쉽게, 그리고 효과적으로 관리할 수 있게 됩니다.


Room Database 설정 방법

Gradle 설정

Room을 프로젝트에 포함시키려면 먼저 build.gradle 파일에 의존성을 추가해야 합니다. 다음 코드를 app level의 build.gradle 파일에 추가해주세요.

dependencies {
    def room_version = "2.4.0" // 여기에는 최신 버전을 사용하세요.

    implementation "androidx.room:room-runtime:$room_version"
    annotationProcessor "androidx.room:room-compiler:$room_version"

    // optional - RxJava2 support for Room
    implementation "androidx.room:room-rxjava2:$room_version"

    // optional - RxJava3 support for Room
    implementation "androidx.room:room-rxjava3:$room_version"

    // optional - Guava support for Room, including Optional and ListenableFuture
    implementation "androidx.room:room-guava:$room_version"

    // optional - Test helpers
    testImplementation "androidx.room:room-testing:$room_version"
}

이렇게 하면 Room Database 관련 클래스를 프로젝트에서 사용할 수 있게 됩니다.

Entity, DAO, Database 클래스 생성

  1. Entity 클래스 생성: 각 Entity는 데이터베이스 테이블에 매핑되는 클래스입니다. 예를 들어, ‘User’라는 테이블을 만들기 위해서는 다음과 같이 클래스를 정의할 수 있습니다.
@Entity
data class User(
    @PrimaryKey val uid: Int,
    @ColumnInfo(name = "first_name") val firstName: String,
    @ColumnInfo(name = "last_name") val lastName: String
)
  1. DAO 생성: DAO는 데이터베이스와 상호작용하는 데 사용되는 메소드를 포함합니다. 예를 들어, User 테이블과 상호작용하는 DAO는 다음과 같이 보일 수 있습니다.
@Dao
interface UserDao {
    @Query("SELECT * FROM user")
    fun getAll(): List<User>

    @Query("SELECT * FROM user WHERE uid IN (:userIds)")
    fun loadAllByIds(userIds: IntArray): List<User>

    @Insert
    fun insertAll(vararg users: User)

    @Delete
    fun delete(user: User)
}
  1. Database 클래스 생성: 마지막으로, Database 클래스는 앱의 Room 데이터베이스를 포함하는 홀더입니다. 이는 RoomDatabase를 확장하여, 데이터베이스와 연관된 DAO를 제공합니다. 이 클래스는 싱글턴으로 유지되어야 하며, 모든 앱에 대한 중앙 데이터베이스 액세스 지점을 제공합니다.
@Database(entities = arrayOf(User::class), version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}

이렇게 Entity, DAO, Database 클래스를 설정하면 Room Database를 사용할 준비가 완료된 것입니다.


CRUD 구현

CRUD는 Create(생성), Read(읽기), Update(수정), Delete(삭제)의 약자로, 대부분의 데이터베이스 시스템에서 핵심적인 기능입니다. 이에 대한 자세한 설명은 아래와 같습니다.

  • Create: 데이터를 생성하거나 추가합니다.
  • Read: 데이터를 조회합니다.
  • Update: 데이터를 수정하거나 업데이트합니다.
  • Delete: 데이터를 삭제합니다.

Room Database에서의 CRUD 코드 예제

  1. Create: @Insert 어노테이션을 사용하여 데이터를 생성합니다. 이는 주어진 객체를 데이터베이스에 추가합니다.
@Dao
interface UserDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insert(user: User)
}
  1. Read: @Query 어노테이션을 사용하여 데이터를 조회합니다. 이는 SQL 쿼리문을 사용하여 특정 데이터를 조회하거나 전체 데이터를 조회할 수 있습니다.
@Dao
interface UserDao {
    @Query("SELECT * FROM user")
    fun getAll(): List<User>

    @Query("SELECT * FROM user WHERE uid = :id")
    fun getById(id: Int): User
}
  1. Update: @Update 어노테이션을 사용하여 데이터를 수정하거나 업데이트합니다. 이는 주어진 객체의 데이터를 업데이트하여 데이터베이스에 반영합니다.
@Dao
interface UserDao {
    @Update
    fun update(user: User)
}
  1. Delete: @Delete 어노테이션을 사용하여 데이터를 삭제합니다. 이는 주어진 객체와 일치하는 데이터를 데이터베이스에서 제거합니다.
@Dao
interface UserDao {
    @Delete
    fun delete(user: User)
}

이런 방식으로, Room에서는 CRUD 연산을 직관적으로 표현할 수 있습니다. 이를 통해 데이터베이스의 관리와 유지보수가 훨씬 편리해집니다.

사용자의 상세 정보를 조회하되, 사용자가 속한 그룹의 정보도 함께 조회해보도록 하겠습니다. 이를 위해 ‘Group’ 이라는 새로운 Entity를 추가하고, 각 사용자가 어떤 그룹에 속하는지를 나타내는 ‘UserGroupCrossRef’라는 연결 테이블도 추가합니다.

@Entity
data class Group(
    @PrimaryKey val groupId: Int,
    val groupName: String
)

@Entity(primaryKeys = ["uid", "groupId"])
data class UserGroupCrossRef(
    val uid: Int,
    val groupId: Int
)

data class UserWithGroups(
    @Embedded val user: User,
    @Relation(
        parentColumn = "uid",
        entityColumn = "groupId",
        associateBy = Junction(UserGroupCrossRef::class)
    )
    val groups: List<Group>
)

@Dao
interface UserDao {
    @Transaction
    @Query("SELECT * FROM user WHERE uid = :userId")
    fun getUserWithGroups(userId: Int): List<UserWithGroups>
}

위의 코드에서, UserWithGroups는 사용자와 그에 대응하는 그룹들을 묶은 데이터 클래스입니다. UserDao에 정의된 getUserWithGroups 메소드를 통해 특정 사용자의 상세 정보와 그 사용자가 속한 그룹 정보를 한번의 쿼리로 얻어올 수 있습니다.

이렇게 Room Database는 상당히 복잡한 쿼리를 명확하고 직관적인 코드로 표현할 수 있게 해줍니다. 이를 통해 개발자는 쿼리 로직에 집중하면서도 코드의 가독성과 유지보수성을 높일 수 있습니다.


Room과 LiveData / RxJava / Coroutine 함께 사용

Room은 LiveData, RxJava, Coroutine과 같은 현대적인 프레임워크와도 잘 호환됩니다. 이들을 이용하면 Room 데이터베이스와 연동하여 비동기적으로 데이터를 처리하는 것이 가능해집니다. 각 기술을 적용하는 방법과 예제에 대해 설명하겠습니다.

LiveData와 Room

LiveData는 Android Architecture Components 중 하나로, 데이터의 변화를 관찰하며 UI를 자동으로 업데이트하는 역할을 합니다. Room에서는 LiveData를 이용해 데이터베이스의 변경을 관찰하고 이에 반응하는 것이 가능합니다.

@Dao
interface UserDao {
    @Query("SELECT * FROM user")
    fun getAll(): LiveData<List<User>>
}

위 예제에서, getAll 메소드는 LiveData를 반환하므로 사용자 데이터가 변경될 때마다 자동으로 UI를 업데이트할 수 있습니다.

RxJava와 Room

RxJava는 비동기 프로그래밍을 위한 라이브러리로, 데이터 스트림을 관리하고 변화를 관찰하는 기능을 제공합니다. Room에서는 RxJava를 이용해 비동기적으로 데이터를 조회하거나 수정하는 것이 가능합니다.

@Dao
interface UserDao {
    @Query("SELECT * FROM user")
    fun getAll(): Flowable<List<User>>
}

위 예제에서, getAll 메소드는 Flowable를 반환하므로 사용자 데이터를 비동기적으로 관찰하고 처리할 수 있습니다.

Coroutine과 Room

Coroutine은 Kotlin에서 제공하는 비동기 처리를 위한 기능으로, 비동기 코드를 마치 동기 코드처럼 쓸 수 있게 합니다. Room에서는 Coroutine을 이용해 메인 스레드를 차단하지 않으면서 데이터를 조회하거나 수정하는 것이 가능합니다.

@Dao
interface UserDao {
    @Query("SELECT * FROM user")
    suspend fun getAll(): List<User>
}

위 예제에서, getAll 메소드는 suspend 키워드를 사용하여 Coroutine 내에서 동작하도록 하여, 메인 스레드를 차단하지 않고 데이터를 비동기적으로 처리할 수 있습니다. 이를 통해 UI가 부드럽게 동작하도록 유지할 수 있습니다.


본문을 통해 Room Database를 이용한 안드로이드 데이터 관리에 대해 알아보았습니다. Room은 SQLite의 복잡한 쿼리 작성과 에러 처리 문제를 단순화하고, 객체 지향적인 접근을 가능하게 하여 개발자의 생산성을 향상시킵니다. 또한, LiveData, RxJava, Coroutine과의 효율적인 연동을 통해 비동기적 데이터 처리를 손쉽게 수행할 수 있습니다.

데이터 관리는 어떤 앱을 개발하든 핵심적인 부분입니다. 안정적이고 효율적인 데이터 관리가 사용자 경험을 크게 좌우합니다. 이러한 측면에서 Room은 안드로이드 앱 개발에서 매우 유용한 도구로 작용합니다.

Leave a Comment