우리는 네트워크 연결이 없을 때에도 데이터를 불러온 경험이 있을 것이다. (카카오톡 채팅 화면)
이 때는 서버에서 데이터를 불러오는 것이 아니라 로컬 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 |