오늘은 android의 Room DB에 대해 정리하겠습니다.
1. Room
1-1. Room
정식명칭은 Room Persistence library이고요.
ORM(Object Relational Mapping)으로서,
SQLite 데이터베이스를 사용하기 쉽도록,
데이터베이스를 객체로 매핑해 주는 역할을 합니다.
SQLite를 Annotation을 이용해서 좀 더 사용하기 쉽게 추상화했다고 보면 될 것 같은데요.
1-2. Types
Room은 SQLite를 바탕으로 하지만,
실제 지원하는 타입은 훨씬 다양합니다.
다만, 복잡한 객체나 컬렉션(예: List, Map)을 직접 지원하지는 않습니다.
지원타입 | 설명 |
원시타입 | char, int, short, long, float, double, byte, boolean |
Wrapping Type | Character, Integer, Short, Long, Float, Double, Byte, Boolean |
기타 타입 | String, Date타입(TypeConverter 필요) BigDecimal, BigInteger |
2. Room의 3가지 Components
Room에는 3가지 주요 Component가 있습니다.
Entity, Database, Dao인데요.
하나씩 개념들만 살펴 보도록 하겠습니다.
2-1. Entity
관계형 데이터에서 사용하는 아래와 같은 Table 데이터를,
data클래스로 매핑해서 작성해 놓은 것이 Entity 입니다.
데이터가 들어있는 각 한줄한줄을 Row라고 합니다.
Student Entity(테이블) | ||
Name | Score | age |
김 | 88 | 12 |
빅 | 66 | 13 |
2-2. Database
관계형 데이터 베이스의 액세스 포인트이며, 데이터베이스 홀더인데요.
위의 Entity(관계형 데이터베이스에서의 Table)들이 묶인 것이 바로 이 Database 입니다.
예로 들면, 학교전산시스템의 데이터베이스에는 아래와 같은 entity들이 있을 수 있겠지요.
- Teacher
- Student
- Parent
- Admin
이러한 entity들을 통틀어서 Database라고 합니다.
Room에서는 Database클래스를 상속받는 abstract Class 이구요.
entity의 리스트들을 가지고 있습니다.
2-3. Dao
Database내의 Entity들의 Row들에 저장된 데이터에 접근하기 위해서,
data access objects의 줄임말인 DAO에 접근하게 됩니다.
DAO는 data에 액세스 하는 객체입니다.
액세스에 필요로 하는 메소드들을 가지고 있게 구성해서 사용하지요.
앱에서 데이터를 얻어오거나 쓰기 위해서,
이 클래스의 함수들을 이용하게 됩니다.
이제 중요 구성요소에 대해 이해했으니,
본격적으로 라이브러리를 추가하고, 직접 코드를 보면서 이해해 보도록 하겠습니다.
3. Room 라이브러리 Implement
Room과 관련해서 몇 가지 라이브러리를 추가해 주어야 하는데요.
보통 DB를 위한 라이브러리보다는 조금 많습니다.
가장 처음이 room-runtime라이브러리이고요.
annotation을 위한 kapt 그리고 kotlin Extenstion을 위한 room-ktx라이브러리가 있습니다.
마지막 줄은 test를 위한 라이브러리입니다.
dependencies {
def room_version = "2.6.1"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"
testImplementation "androidx.room:room-testing:$room_version"
}
4. 3가지 Component의 구현
Room라이브러리를 구현한다는 것은,
위에서 언급한 3개의 Component를 구현하는 것 입니다.
Room을 구현할 때는,
Entity, Dao, Database,
순서로 구현한다는 것을 염두해 두면 좋습니다.
그럼, Data class를 시작으로 하나씩 구현해 보도록 하겠습니다.
4-1. Entity( Data Class 작성)
Room에서 사용하는 Data 클래스명 위에는,
아래와 같이 @Entity annotation을 붙여 주어야 합니다.
data클래스를 이용해 테이블과 매핑하는 것이라 볼 수 있습니다.
- PrimaryKey - @PrimaryKey라고 애노테이션을 붙여줍니다.
- Column - @ComlumnInfo 애노테이션을 붙여줍니다.
테이블 안에 컬럼명을 다르게 설정하기 위해서는,
("name")을 다른 값으로 정해주면 됩니다.
참고로 PrimaryKey가 자동으로 생성되게 하기 위해서는,
annotation에 autoGenerate옵션을 true로 설정해 주면 됩니다.
위에서 사용할 수 있는 애노테이션 이외에도 사용할 수 있는 키워드가 많은데요.
@Ignore를 사용하면 해당 필드는 무시할 수 있고요.
@foreignKey를 이용해, parentColums와 childColumns를 아래와 같이 사용할 수도 있습니다.
4-2. Dao
Dao는 Data Access Object의 약자인데요.
여기서는 데이터에 액세스 하는 메소드들을 가진 interface를 정의해주면 됩니다.
메소드를 정의할 때,
Insert, Update, Delete, Query 애노테이션을 이용해,
CRUD를 구현합니다.
Insert나 Delete에는 SQL문을 직접 작성할 필요는 없구요.
Query애노테이션의 괄호안에는 SQL문을 작성해서 넣어주면 됩니다.
메소드의 인자로 받은 변수는 ":"(콜론)을 이용해서 애노테이션의 Query문에서 사용할 수 있습니다.
예를 들어, 아래의 loadAllByIds는 studentIds를 인자로 받아서 사용하고 있고요.
그 아래의 findByName메소드는 first와 last를 인자로 받아서 LIKE로 사용하고 있습니다.
참고로 insertAll()함수의 인자에 들어간,
"vararg" 키워드는 kotlin에서의 가변인자를 가리킵니다.
한가지 주의할 점이 있는데요.
Room이 SQLite 기반이기는 하지만,
SQLite에서 true 나 false는 1과 0으로 표현한다는 점 입니다.
따라서, 위의 query에서 작성할 때, true 혹은 false는 반드시 1과 0으로 표현해주어야 합니다.
실수를 방지하기 위해서 Constant를 아래와 같이 사용할 수도 있습니다.
SQLConstants 같은 파일을 만들어서, 아래와 같이 Top-levle 상수를 선언해 주는 것도 방법입니다.
const val SQL_TRUE = 1
const val SQL_FALSE = 0
위에서 정의한 상수를 아래와 같이 사용할수 있습니다.
@Query("SELECT * FROM tests WHERE isFavorite = :favorite")
fun getFavoriteTests(favorite: Int = SQL_TRUE): Flow<List<Test>>
4-3. Type Converter
커스텀한 데이터객체를 저장할 필요가 있을 경우에 룸에서는 어떻게 해야 할까요?
공식문서에 따르면, 아래와 같이 Convertes클래스를 만들어서,
특정 timestamp와 Date객체를 변환할 수 있도록 하는 방법을 사용하라고 되어있습니다.
class Converters {
@TypeConverter
fun fromTimestamp(value: Long?): Date? {
return value?.let { Date(it) }
}
@TypeConverter
fun dateToTimestamp(date: Date?): Long? {
return date?.time?.toLong()
}
}
이런 Converter들은 직접 필요한 Entity에 추가하여서 적용되도록 할 수 있습니다.
@Entity(tableName = "chat_messages")
data class ChatMessage(
...
@TypeConverters(ChatMessageTypeConverter::class)
val messageType: MessageType,
...
)
enum class MessageType {
TEXT,
IMAGE,
VIDEO,
AUDIO
}
4-4. Database
이제 마지막 컴포넌트인 Database를 구현해 주면 되는데요.
아래에서는 studentDatabase파일을 만들었습니다.
@Database 애노테이션을 붙여주면 Database 클래스로 정의할 수 있습니다.
여기서 abstract class를 사용한 이유는, 필요한 SQL코드등의 구현을 RoomLibrary에 맡기기 때문입니다.
우리가 Entity와 DAO에서 정의해 놓은 코드에 따라,
룸라이브러리가 필요한 SQL코드들을 생성해 처리해 주기 때문입니다.
entities에는 위에서 정의한 entity들을 array형태로 넣어주면 됩니다.
만약 위에서 본 것처럼,
TypeConverts클래스를 가지고 있고 Entity에서 적용시키지 않았다면,
Annotation을 @Database Annotation의 아래에 추가해 줄 수 있습니다.
@TypeConverters(Converters::class)
이제 이렇게 생성한 studenDatabase를 객체로 만들어서 사용해 주면 되는데요.
참고로 아래의 fallbackToDestructiveMigration()은 데이터 베이스가 수정되면,
기존 데이터를 삭제하는 메소드인데요.
그렇지 않고 Migration을 해 줄 경우에는 addMigration메소드를 사용해 주면 됩니다.
공식문서에서는 아래와 같이,
RoomDatabase 객체를 만드는 것이 비싼 작업이므로,
싱글턴 패턴을 사용하는 것을 추천하고 있습니다.
싱글턴 패턴을 이용하기 위해서 companion object를 사용하는 것도 방법이지만,
Hilt나 Koin같은 Dependency Injection을 이용하는 것도 좋을 방법입니다.
굳이 DI를 사용하지 않으실 분들은 아래 부분은 패스하셔도 무방합니다.
5. Dao 에서 Query문 사용시 팁
5-1. 가장 최신 것 부터 불러오기
아래의 Query는 가장 최신순으로 chat_messages를 보여주도록 query하고 있는데요.
ORDER BY는 어떤 컬럼을 기준으로 소팅할 것인지를 정하기 위한 키워드 이구요.
여기서는 createdAt이라는 컬럼을 기준으로 하였습니다.
DESC 는 descending즉 내림차순을 의미하는데요. 반대되는 개념이 ASC ascending 입니다.
DESC가 내려가는 것이므로, 가장 큰 것에서 작은 것을 정렬이 된다고 생각하면 됩니다.
createdAt에는 System.currentTimeMillis() 같이 시간이 갈수록 높은 값을 반환하는 값을 저장하고,
이를 DESC로 내림차순으로 query한다면, 반환값은 최신의 값부터 나오게 되겠지요.
SELECT * FROM chat_messages ORDER BY createdAt DESC
5-2. PrimaryKey의 auto-generate 사용시 주의할 점
PrimaryKey의 auto-generate을 true로 해놓고,
Room Entity 데이터클래스로 객체를 생성할 때 설정된 값이,
이 객체에 설정된 id 값이 그대로 DB에 반영되는 값이라고 생각하면 안됩니다.
auto-generate 되는 시점에 대해서 알고 있어야 하는데요.
바로, Insert 메소드가 실행될 때 입니다.
객체를 생성할 때가 아닙니다.
따라서, Room Entity데이터 클래스의 객체를 생성하고,
id 부분을 바로 사용해야 한다면, Insert 되고 난 시점에 사용해 주어야 합니다.
6. Coroutine suspend 함수선언과 Room DB
dao함수 작성시 suspend 함수로 선언해 주면,
자체적으로 withContext(Dispatchers.IO)를 처리하고 있어,
별도로 Dispatcher를 지정할 필요가 없습니다.
@Dao
interface UserDao {
@Query("UPDATE users SET level = :level WHERE id = :userId")
suspend fun updateUserLevel(userId: Long, level: Int)
}
그럼 아래와 같이,
Coroutine에서 Dispatcher지정이 없어도,
메인쓰레드가 아닌 비동기 쓰레드에서 동작하게 됩니다.
suspend fun updateUserLevel(userId: Long, newLevel: Int) {
userDao.updateLevel(userId, newLevel)
}
7. Hilt 또는 Koin을 이용한 객체 주입
7-1. Hilt
안드로이드에서 Hilt를 사용하는 방법에 대해서는 아래 글을 참조해 주세요.
>> HILT 에 대해서 정리해 보겠습니다. # DI Dependency Injection
이 글에서는 Room DB를 적용하는 방법에 대해서만 정리하겠습니다.
@Entity, @Dao, @Database를 만들었다면, 이제 Hilt가 의존성을 생성해 주면 되는데요.
아래와 같이 Module을 만들어서, Database의 객체를 생성하는 방법을 Provides Annotation을 이용해서 Hilt에게 알려주기만 하면 됩니다.
구현 방법은 사람마다 다르겠지만, MVVM패턴을 이용해서,
Repository 인터페이스를 아래와 같이 만들어 주고요.
이 인터페이스를 dao객체를 가지고 구현하는 구현클래스를 만들어 줍니다.
이후에 그것을 AppModule에서 Provides 애노테이션을 이용해서 Hilt에게 어떻게 만드는지 알려줍니다.
첫 번째는 Room Database객체를 만드는 방법이고요., 두 번째는 위에서 Room을 이용하는 Repository객체를 만드는 방법입니다.
db는 viewModel에서 가져다가 사용하게 되는데요.
위의 Module에서 Provide에게 어떻게 roomDB와 그것을 이용하는 Repository를 만드는지 Hilt에게 알려주었으니,
저희는 인자에 넣어서 사용만 하면 됩니다.
7-2. Koin
참고로 Koin에 관해서는 아래 글을 참조해 주시고요.
>> KOIN을 이용한 Dependency Injection (DI) 구현하기
아래와 같이 Koin을 사용해 볼 수도 있습니다.
먼저 StudentDatabase객체를 만들어 주는 클래스를 만들고요.
아래와 같이 Koin Module을 구현해 줍니다.
그 다음 ApplicationClass에서 startKoin을 해주면 되겠지요.
이제, 다음과 같이 객체를 주입해 줍니다.
이렇게 주입된 객체를 아래와 같이 사용해 주기만 하면 되겠습니다.
이상으로 Room Persistence Library에 대해서 정리해 보았고요.
더 좋은 내용이 있다면, 이 글을 통해서 업데이트하도록 하겠습니다.
'Android 개발 > Room, Realm, Databases' 카테고리의 다른 글
Room DB 에서 검색 구현하기 # Like ExactMatch SQL (0) | 2023.04.24 |
---|---|
RoomDB 에서 One to Many 관계 구현하기 # 채팅 DB 구현 (0) | 2023.04.07 |
Room DB 에 DB파일 넣어서 사용하기 # 로컬데이터베이스 SQLite (0) | 2022.11.04 |
Room을 이용해서 Offline Cache 구현하는 방법 정리 #Android (0) | 2020.05.14 |
미리 작성된 Realm 데이터베이스를 앱에 넣어서 사용하기 (0) | 2017.06.19 |
Hot Observable 과 Cold Observable은 무엇인가요? (0) | 2017.03.19 |
Realm 에서 Thread Error 없이 데이터 조회후 삭제하기 (0) | 2017.03.08 |
Realm 에서 Migration 하는 방법 (0) | 2017.03.01 |
Realm Module 을 이용해서 멀티 Schema 활용하기 (0) | 2017.02.25 |
Android의 Realm 데이터베이스 파일 은 특별한 곳에 있지 않다 (0) | 2017.01.10 |
댓글