본문 바로가기

Android

[Android Jetpack] 앱 속 DB, Room

 

우리는 네트워크 연결이 없을 때에도 데이터를 불러온 경험이 있을 것이다. (카카오톡 채팅 화면)

이 때는 서버에서 데이터를 불러오는 것이 아니라 로컬 DB에 저장해두고 불러오는 경우다.

 

상당한 양의 구조화된 데이터를 처리하는 앱은 데이터를 로컬에 유지하여 매우 큰 이익을 얻을 수 있다.

가장 일반적인 사용 사례는 기기가 네트워크에 액세스할 수 없을 때도 사용자가 오프라인 상태로 계속 콘텐츠를 탐색할 수 있도록 관련 데이터를 캐시하는 것이다.

 

원래는 SQLite를 활용해 어플리케이션 내부 저장소를 만들어 저장했었다.

그러나 SQLite를 사용할 때에는 다음과 같은 불편함이 있었다.

  • Query의 유효성 검사 기능을 제공하지 못했던 점
  • Scheme가 바뀔 떄 자동적으로 업데이트를 하지 못했던 점
  • ORM 지원이 안 되어 데이터를 객체로 변환시키기 위해 데이터 처리를 더 해야했던 점
  • Observer 패턴의 데이터(LiveData, RxJava)를 생성하고 동작시키기 위해 추가적인 Boiler Plate 코드를 작성해야하는 점

공식 홈페이지도 아래와 같이 주의를 요한다.

 

 

그래서 Android X 버전 이후로 어플리케이션 내부 DB로 Room을 추천하고 있다.

Room 지속성 라이브러리는 SQLite를 완벽히 활용하면서 원활한 데이터베이스 액세스가 가능하도록 SQLite의 추상화 계층을 제공한다.

특히 Room을 사용하면 다음과 같은 이점이 있다.

  • SQL 쿼리의 컴파일 시간 확인
  • 반복적이고 오류가 발생하기 쉬운 상용구 코드를 최소화하는 편의 주석
  • 간소화된 데이터베이스 이전 경로

이러한 점을 고려할 때 SQLite API를 직접 사용하는 대신 Room을 사용하는 것이 좋다.

 

ROOM

SQLite의 추상화된 버전이라 생각하면 조금 더 편할 것 같다.

이전에는 SQLite에 직접 데이터를 가공하여 DB에 맞는 형식으로 데이터를 CRUD하는 방식이었다면,

Room에서는 객체로 데이터를 주고받을 수 있고 반응형 데이터로도 데이터를 주고받을 수 있게 설계가 되어있다.

 

 

ROOM의 구성 요소

  • Entity : DB Table
  • Data Access Object (DAO) : 데이터에 접근할 수 있는 객체
    쿼리, 업데이트, 삭제 등 메서드 제공
  • Database Class : 데이터 연결을 위한 기본 엑세스 포인트 역할


위 그림을 보면 Room Database 에서 DAO 인스턴스를 받는다.

Data Access Object는 DB에서 entities를 얻고, back에서 지속적으로 DB를 change할 수있다

Entities 에서 data get / set 할 수있다.

어떤 데이터를 관리할 것인지 Entity를 정의한다.
Data Access Object (DAO)를 구현해 Database의 데이터에 접근한다.
Database Class로 데이터를 연결한다.

 

각 구성요소를 사용해 room을 구현해보자.

1. Gradle 추가

plugins {
    id 'kotlin-kapt'
}

dependencies {
    // Room
    implementation 'androidx.room:room-runtime:2.5.0'
    kapt 'androidx.room:room-compiler:2.5.0'
}

 

2. Entity 정의

데이터베이스의 Table(데이터 구조) 역할을 맡고 있는 Entity이다. 이 테이블들이 모여서 Database를 구성한다.

Room에서의 Entity는 테이블 형식과 달리 Data Class의 형태로 구성되어있다.

이런 구조는 Room에서 데이터 입출력을 할 때 객체로 입출력을 한다는 것을 드러낸다.

 

Entity는 어노테이션(@ 형태)로 클래스를 정의한다.

어노테이션의 종류에 대해 알아보자.

@Entity

이 클래스가 Entity임을 알려주는 어노테이션이다. 다음과 같은 속성들을 사용할 수 있다.

  • tableName (테이블명을 설정)
    :  Entity의 이름을 정해준다. default는 클래스 이름의 camelCase이다.
@Entity(tableName = "text_table")
  • primaryKeys = arrayOf() 
    :  Entity의 Primary Key가 여러 개일경우 어노테이션 속성으로 선언해준다.

entity에 복합 기본 키(2개의 필드가 1개의 기본키 역할을 한다.)가 있으면 다음 코드 스니펫과 같이 @Entity 주석의 primaryKeys 속성을 사용할 수 있다.

@Entity(primaryKeys = arrayOf("firstName", "lastName"))

 

@PrimaryKey

DB의 기본 키를 나타내는 어노테이션이다. 클래스의 멤버변수에 사용한다.
 entity는 하나 이상의 필드를 기본 키로 정의해야 한다. 

필드가 하나만 있는 경우에도 @PrimaryKey 주석을 사용하여 필드에 주석을 달아야 한다.

또한 Room에서 항목에 자동 ID를 할당하게 하려면 @PrimaryKey autoGenerate 속성을 설정하면 된다.

  • autoGenerate
    - 이 속성을 활용해서 DB Row의 id를 부여할 수 있다.

@ColumnInfo(name = "any_name")

관계형 DB의 Column의 이름을 변수명을 짓고 싶지 않을 때 사용하는 어노테이션이다.

 

@Entity(tableName = "text_table")
data class TextEntity (

    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "id")
    var id : Int,
    @ColumnInfo(name = "text")
    var text : String

)

 

 

 

 

3. DAO: Database Access Object

Room의 데이터를 접근하기 위해서는 Database를 접근할 수 있는 객체를 정의해서 그 객체를 통해 접근해야 한다.

이 객체를 DAO라 한다.

 

DAO 형태를 알아보자.

@Dao
interface TextDao {

    @Query("SELECT * FROM text_table")
    fun getAllData() : List<TextEntity>

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun insert(text : TextEntity)

    @Query("DELETE FROM text_table")
    fun deleteAllData()

}

DAO는 interface(혹은 추상 클래스)로 정의할 수 있다.

그리고 쿼리를 써야하는 기존 DB의 CRUD 방식과는 달리,

조금 더 친절하게 데이터를 다룰 수 있도록 어노테이션을 기반으로 하여 데이터 입출력을 정의한다.

@Insert

: DB에 row 데이터를 삽입한다.

위에서는 return 값을 정의 안 했지만,

만약 정의한다면 long 타입 삽입된 row의 id 값(여러 개를 삽입했으면 id들 long의 리스트 형)이 반환된다.

@Update

: DB의 row 데이터를 수정한다.

Data Class 객체를 전달해준다면 id 값을 제외한 최소 한 가지 이상의 데이터가 수정된 채로 전달될 것이니 이를 기반으로 row 데이터를 수정한다.

@Delete

: DB의 row 데이터를 삭제한다.

@Query("query")

: 기존 방식과 같이 query를 활용하여 데이터를 CRUD할 수 있게 만드는 어노테이션이다.

위의 연산들보다 조금 더 복잡한 연산이 필요한 경우에 이런 쿼리 어노테이션을 활용하여 데이터를 가져올 수 있다.

 

 

4. Database

  • @Database를 통해 데이터베이스와 연결할 entities 항목과 version 정보를 포함해 상단에 배치
  • RoomDatabase를 확장하는 추상클래스여야 한다.
  • DAO클래스의 인스턴스를 반환하는 추상메서드를 정의해야한다.
@Database(entities = [TextEntity::class], version = 1)
abstract class TextDatabase : RoomDatabase() {

    abstract fun textDao(): TextDao

    companion object {
        @Volatile
        private var INSTANCE: TextDatabase? = null

        fun getDatabase(
            context: Context,
        ): TextDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    TextDatabase::class.java,
                    "text_database",
                )
                    .fallbackToDestructiveMigration()
                    .build()
                INSTANCE = instance
                instance
            }
        }
    }
}

 

 

Room 데이터베이스에서 Dao를 가져와 이 객체를 통해 데이터를 CRUD한다.

즉 Room Database는 수 많은 Dao들을 관리하기 위한 Dao Manager 클래스라고 생각하면 될 것 같다.

@Database

이 클래스가 Database임을 알려주는 어노테이션이다.

  • entities
    : 이 DB에 어떤 테이블들이 있는 지 명시해준다.
  • version
    : Scheme가 바뀔 때 이 version도 바뀌어야 한다.
  • exportSchema
    : Room의 Schema 구조를 폴더로 Export 할 수 있다.
      데이터베이스의 버전 히스토리를 기록할 수 있다는 점에서 true로 설정하는 것이 좋다.
     하지만 Test 상황에서는 굳이 true로 할 필요까진 없다.

 

데이터베이스의 여러 인스턴스가 동시에 접근하는 것을 막기 위해 싱글톤을 사용하는 것을 추천한다.

싱글톤 객체와 synchronized

  • 싱글톤 객체
    :Database 객체를 생성하는 것 자체가 리소스를 상당히 많이 소비하는 작업이기 때문에
    데이터베이스는 전체 프로젝트에서 하나만 생성하는 싱글턴 객체로 선언을 해야한다.

    그러나 멀티 스레딩 환경(동시에 여러 유저들이 DB로 접근할 수 있는 상황)에서는 DB에 동시에 조회하여 데이터들을 Update 하는 경우, 데이터베이스의 건전성에 문제가 발생할 수 있다.

    따라서, 스레드 하나만 들어올 수 있게 DB 객체는 synchronized로 스레드를 동기화 시켜버린다.

 

MainActivity

class MainActivity : AppCompatActivity() {

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

        val db = TextDatabase.getDatabase(this)

        CoroutineScope(Dispatchers.IO).launch {
            db.textDao().insert(TextEntity(0, "HELLO"))
            Log.d("MAINACTIVITY", db.textDao().getAllData().toString())

        }
    }
}

 

MainActivity에서 데이터베이스를 가져와 새로운 내용을 삽입해보고 전체를 조회하는 코드이다.

이렇게 room을 이용해 앱 내에서 데이터를 활용할 수 있다.

'Android' 카테고리의 다른 글

[Android] View Binding  (1) 2023.08.30
[Android] AAC (Android Architecture Components)  (0) 2023.08.30
[Android] DataBinding  (0) 2023.08.30
[Android] Multi-ViewType RecyclerView  (1) 2023.08.09
[Android] RecyclerView  (1) 2023.08.03