Groo

Room 데이터베이스 라이브러리 본문

Android

Room 데이터베이스 라이브러리

김주엽 2020. 6. 1. 14:13

안녕하세요, 오늘은 안드로이드의 내부 DB에 대해서 한 번 알아보는 시간을 가지려고 합니다
그중 저희는 Room이라는 라이브러리를 활용하여 실제로 안드로이드 속 내부 DB를 구현해보겠습니다.

🤔 내부 DB란 무엇인가?

안드로이드에서는 앱의 데이터를 효율적으로 관리하기 위한 저장소로 SQLite라는 데이터베이스를 제공하고 있습니다. 이는 다른 외부 DB들과 달리 소규모 데이터를 관리하고 사용하는데 적합한 관계형 데이터베이스입니다. 적은 데이터를 관리하는데 최적화된 만큼 속도가 빠르고 가볍다는 장점이 존재하여 현시점의 많은 애플리케이션들이 공통적으로 내부 DB를 활용하고 있는 추세입니다.

 

간단한 예시를 들어보겠습니다. 현대의 대다수의 사람들은 카카오톡을 사용하고 있습니다. 카카오톡을 통해 다른 사람들과 연락을 하기 위해서는 데이터 즉 네트워크에 연결되어있어야합니다. 하지만 저희는 네트워크에 연결되지 않은 상태에서도 아래와 같이 이전 대화 내용과 친구 목록 등 일부 항목들을 이용할 수 있습니다. 이것이 가능한 이유는 프로그램 내에 내부 DB를 활용했기 때문입니다.

 

🏘 Room 라이브러리는?

안드로이드 아키텍처 컴포넌트에 속하는 Room 라이브러리는 안드로이드 앱에서 SQLite 데이터베이스를 쉽고 편리하게 사용할 수 있도록 하는 기능입니다. 이는 구글에서 새롭게 출시한 ORM이라고 할 수 있으며 룸 라이브러리에는 엔티티(Entity), 데이터 접근 객체(DAO), 룸 데이터베이스(Room Database), 총 세 개의 구성 요소를 통해 Room 데이터베이스 라이브러리가 구성됩니다.

 

🎨 Room 라이브러리의 구성요소

Room 데이터베이스 라이브러리를 본격적으로 사용하기 이전에 위에서 간략히 설명한 Room DB 라이브러리의 구성 요소에 대해서 자세히 살펴보겠습니다. 아래의 구조도를 본다면 각 구성요소들의 역할과 범위에 대해서 알 수 있으며 서로 간의 의존 관계가 성립한다는 것을 알 수 있습니다. 여러분들은 꼭 이 세 가지의 구성요소를 확실히 이해한 후 다음 단계로 넘어가시는 것을 추천드립니다.

 

 

1. 엔티티(Entity)

데이터베이스 내의 릴레이션 즉 테이블을 뜻하며 DB에 저장할 데이터 형식을 정의합니다.
2. 데이터 접근 객체(Data Access Object)

데이터베이스에 접근하여 수행할 작업을 메소드 형태로 정의합니다.
3. 룸 데이터베이스(Room Database)

데이터베이스의 전체적인 소유자 역할을 하며 DB를 새롭게 생성하거나 버전을 관리합니다.

💊 Room과 SQLite의 비교

안드로이드에서 내부 DB를 구현하고 싶다면 2가지의 방법이 존재합니다. 한 가지는 앞에서 계속 말했던 Room 데이터베이스 라이브러리이며 반면에 또 다른 방법은 SQLite 데이터베이스 라이브러리입니다. Room DB 라이브러리가 출시되기 이전까지 구글에서는 SQLite DB 라이브러리 사용을 추천하였으며 그에 따라 많은 개발자들은 SQLite 라이브러리를 활용하여 기능을 구현했습니다.

 

하지만 이를 사용하기 위해서는 상당한 시관과 노력 등 사용 방법이 매우 복잡하여 구글에서는 이 문제점을 해결하고자 새롭게 룸 데이터베이스를 출시하였습니다. 그에 따라 SQLite의 사용 빈도는 줄어드고 Room 라이브러리의 사용 빈도는 증가하고 있습니다.

 

또한 아래와 같이 SQLite 데이터베이스 라이브러리에서는 허용되지 않던 기능들이 반면에 Room 데이터베이스 라이브러리에서는 새롭게 추가되면서 조금 더 개발자들이 내부 DB를 간편하게 구현할 수 있도록 설계된 모습을 쉽게 볼 수 있는 것을 알 수 있습니다.

 

1. 컴파일 도중 SQL에 대한 유효성 검사 기능

2. Schema 스키마가 변경될 시 자동으로 업데이트 기능

3. Java 데이터 객체를 변경하기 위해 상용구 코드 없이 ORM 라이브러리를 통해 매핑 기능

4. LiveData와 RX Java를 위한 Observation 생성 및 동작 기능

👩‍🏫 Build.Gradle 파일 설정하기

그럼 이제 본격적으로 Room 데이터베이스 라이브러리를 활용하여 내부 DB를 구현해보기 위해서 프로젝트 파일 속 Build.Gradle 파일에 아래와 같은 코드를 추가해주시기 바랍니다. 최신 버전 정보와 기타 정보들은 구글 공식 홈페이지를 참고해주시기 바랍니다.

 

apply plugin: 'kotlin-kapt'
...
dependencies {
    def room_version = "2.2.5"

    // Room DB
    implementation "androidx.room:room-runtime:$room_version"
    kapt "androidx.room:room-compiler:$room_version"
}

🧑‍🔧 간단한 예제 구현하기

Room DB 라이브러리를 활용하여 구현해볼 간단한 예제는 바로 도서 관리 시스템입니다. 이 시스템의 기능은 도서 조회, 추가, 수정, 삭제가 가능합니다. 그러나 이번 예제에서는 따로 XML UI 관련 작업을 진행하지 않고 오직 콘솔로만 결과를 확인하도록 하겠습니다.

[BookEntity]

@Entity 어노테이션과 함께 데이터베이스 내의 릴레이션을 생성하고 있습니다. 클래스의 이름은 BookEntity이지만 tableName을 book으로 설정하였기 때문에 클래스 이름과는 별개로 방금 생성한 데이터베이스 내의 릴레이션의 이름은 book으로 설정됩니다.

 

그 후 클래스의 생성자로 릴레이션 내의 각각의 속성들을 정의하고 있습니다. 저희는 현재 도서 관리 시스템을 구현하고 있기 때문에 그와 관련되는 속성들을 지정하였으며 @PrimaryKey 어노테이션은 5개의 속성 중 기본키를 설정하는 것이고 @ColumnInfo 어노테이션은 각 속성의 이름을 변수의 이름과 상관없이 임의로 지정할 수 있도록 도와주는 것입니다. 현재 상황에서는 생략 가능합니다.

@Entity(tableName = "book")
class BookEntity(@PrimaryKey val idx : Int,
                 @ColumnInfo(name = "name") val name : String,
                 val author : String,
                 val kind : String,
                 val price : Int)

[BookDao]

@Dao 어노테이션과 함께 데이터베이스에 접근하여 수행할 작업들을 메서드 형태로 지정합니다. 이 메소드들은 interface 내에 포함되므로 모두 추상 메소드들이며 조회, 추가, 수정, 삭제 기능을 구현합니다. 또한 아래의 insert 메소드 부분을 본다면 onConflict 설정을 통해 만약 데이터베이스 내에 중복된 투플 값이 존재함에도 불구하고 데이터를 추가한다면 그 값 위에 바로 덮어 씌웁니다.

@Dao
interface BookDao{

    @Query("SELECT * FROM book")
    fun getAllBook() : List<BookEntity>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insert(bookEntity : BookEntity)

    @Query("UPDATE book set name = :name, author = :author, kind = :kind, price = :price WHERE idx = :idx")
    fun update(idx : Int, name : String, author : String, kind : String, price : Int)

    @Query("DELETE FROM book WHERE idx = :idx")
    fun delete(idx : Int)
}

[BookDatabase]

@Database 어노테이션과 함께 데이터베이스의 전체적인 소유자 역할을 하고 있으며 앞에서 생성한 Entity, DAO 클래스를 통합적으로 묶어 데이터베이스를 생성하거나 버전 관리를 진행합니다. entities 값을 BookEntity로 설정하여 이전에 설정한 Entity 클래스를 추가해주며 version 정보 또한 함께 설정해줍니다. 아래의 getInstance 메서드는 추후 테스트를 진행할 때 사용될 것입니다.

@Database(entities = [BookEntity::class], version = 1)
abstract class BookDatabase : RoomDatabase(){

    abstract fun getBookDao() : BookDao

    companion object{

        private var INSTANCE : BookDatabase? = null

        fun getInstance(context : Context) : BookDatabase?{
            if(INSTANCE == null){
                synchronized(BookDatabase::class){
                    INSTANCE = Room.databaseBuilder(
                        context.applicationContext,
                        BookDatabase::class.java,
                        "book.db")
                        .build()
                }
            }
            return INSTANCE
        }
    }
}

[MainActivity]

마지막으로 Room 데이터베이스가 정상적으로 작동하는지를 테스트하기 위해 MainActivity에 아래와 같은 코드를 작성해줍니다. 앱을 실행한다면 도서 데이터는 추가, 수정, 삭제, 조회 순서로 작동될 것이며 각 기능들 마다 새로운 스레드가 생성될 것입니다.

 

만약 메인 스레드에서 이와 같은 작업들을 모두 수행한다면 룸 관련 업무 처리 시간이 길어질 시 다른 활동은 아무것도 못한 채 계속 대기만 해야 하는 상황이 발생할 수 있기 때문입니다. 또한 저는 각 기능 별로 약 1초 정도의 간격을 두어 순서대로 각 스레드가 동작할 수 있도록 하였습니다. 각 기능들을 구현하기 위해 저는 BookDao 클래스와 BookDatabase에서 생성한 코드를 활용했습니다.

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        Log.e("bookList", "도서 데이터를 추가합니다.")
        val addBook = AddBook(applicationContext)
        addBook.start()

        Thread.sleep(1000)

        Log.e("bookList", "도서 데이터를 수정합니다.")
        val updateBook = UpdateBook(applicationContext)
        updateBook.start()

        Thread.sleep(1000)


        Log.e("bookList", "도서 데이터를 삭제합니다.")
        val deleteBook = DeleteBook(applicationContext)
        deleteBook.start()

        Thread.sleep(1000)

        Log.e("bookList", "도서 데이터를 조회합니다.")
        val getBook = GetBook(applicationContext)
        getBook.start()
    }
}

class AddBook(val context: Context) : Thread() {
    override fun run() {
        val book = BookEntity(1, "공간이 만든 공간", "유현준", "교양 인문학", 16500)
        BookDatabase
            .getInstance(context)!!
            .getBookDao()
            .insert(book)

        val book2 = BookEntity(2, "TED TALKS 테드 토크", "크리스 앤더슨", "자기계발", 16000)
        BookDatabase
            .getInstance(context)!!
            .getBookDao()
            .insert(book2)

        val book3 = BookEntity(3, "철학은 어떻게 삶의 무기가 되는가", "야마구치 슈", "철학", 16000)
        BookDatabase
            .getInstance(context)!!
            .getBookDao()
            .insert(book3)
    }
}

class UpdateBook(val context: Context) : Thread(){
    override fun run() {
        BookDatabase
            .getInstance(context)!!
            .getBookDao()
            .update(1, "2020 정보처리기능사 실기 기본서", "길벗알앤디", "컴퓨터 IT", 19000)
    }
}

class GetBook(val context: Context) : Thread(){
    override fun run() {
        val items = BookDatabase
            .getInstance(context)!!
            .getBookDao()
            .getAllBook()

        for(i in items){
            Log.d("bookList", "${i.idx} | ${i.name} ${i.author} | ${i.kind} | ${i.price}")
        }
    }
}

class DeleteBook(val context: Context) : Thread(){
    override fun run() {
        BookDatabase
            .getInstance(context)!!
            .getBookDao()
            .delete(3)
    }
}

👍 글을 마치며

오늘은 안드로이드 앱에서 내부 DB의 개념과 실제로 Room 데이터베이스 라이브러리를 활용하여 이를 구현해보았습니다. 솔직히 말하면 오늘 배우는 Room DB 라이브러리를 사용해보기 전 SQLite DB 라이브러리를 먼저 접해보시는 것을 추천드립니다. 그래야만 Room DB 라이브러리가 얼마나 편리하고 간편한지 더욱 깊게 느낄 수 있을 것입니다. 이러한 내부 DB 방식을 활용한다면 사용자는 네트워크에 연결되어 있지 않은 상태에서도 사용할 수 있는 기능의 폭이 넓어지고 훨씬 유용할 것입니다. 하지만 현대의 대부분 모바일 애플리케이션들을 보면 네트워크와 소통을 하지 않는 모바일 앱은 찾아보기 힘들며 대부분 서버와의 연결과 내부 DB를 함께 활용하여 사용자들에게 조금 더 폭넓은 기능들을 제공하고 있습니다. 다음 시간에는 실제로 안드로이드에서 서버와 소통을 할 수 있도록 도와주는 Retrofit 라이브러리에 대해서 공부해보는 시간을 가지려고 하며 이전에 배운 REST API 방식을 활용할 것입니다.

Comments