Room
: SQLite(스마트폰 내장 DB)에 데이터를 저장하기 위해 사용하는 라이브러리
- SQLite를 활용해서 객체 매핑을 해주는 역할을 한다.(SQLite에 추상화 계층을 제공)
- Room을 사용하면 다음과 같은 이점이 있습니다.
- SQL 쿼리의 컴파일 시간 확인
- 반복적이고 오류가 발생하기 쉬운 상용구 코드를 최소화하는 편의 주석
- 간소화된 데이터베이스 이전 경로
0.설정
dependencies {
val roomVersion = "2.4.2"
implementation("androidx.room:room-runtime:$roomVersion")
annotationProcessor("androidx.room:room-compiler:$roomVersion")
// To use Kotlin annotation processing tool (kapt)
kapt("androidx.room:room-compiler:$roomVersion")
// To use Kotlin Symbolic Processing (KSP)
ksp("androidx.room:room-compiler:$roomVersion")
ㄴ> 사용 부분에 따라 아래와 같이 의존성 더 추가
// optional - Kotlin Extensions and Coroutines support for Room
implementation("androidx.room:room-ktx:$roomVersion")
// optional - RxJava2 support for Room
implementation("androidx.room:room-rxjava2:$roomVersion")
// optional - RxJava3 support for Room
implementation("androidx.room:room-rxjava3:$roomVersion")
// optional - Guava support for Room, including Optional and ListenableFuture
implementation("androidx.room:room-guava:$roomVersion")
// optional - Test helpers
testImplementation("androidx.room:room-testing:$roomVersion")
// optional - Paging 3 Integration
implementation("androidx.room:room-paging:2.5.0-alpha02")
}
Room 구성요소 3가지
- 데이터베이스 클래스: 데이터베이스를 보유하고 앱의 영구 데이터와의 기본 연결을 위한 기본 액세스 포인트 역할을 합니다.
- 데이터 항목(Entities) : 앱 데이터베이스의 테이블
- 데이터 액세스 객체(DAO, Data Access Objects): 앱이 데이터베이스의 데이터를 쿼리, 업데이트, 삽입, 삭제하는 데 사용할 수 있는 메서드를 제공합니다.
- 동작순서
1) '데이터베이스 클래스'는 데이터베이스와 연결된 'DAO 인스턴스'를 '앱'에 제공합니다.
2) 그러면 '앱'은 'DAO'를 사용하여 데이터베이스의 데이터를 연결된 데이터 항목 객체의 인스턴스로 검색할 수 있게 됩니다.
3) 앱은 정의된 데이터 항목을 사용하여 상응하는 테이블의 행을 업데이트하거나 삽입할 새 행을 만들 수도 있습니다.
ㄴ> 위 사진에서 Room Database, Data Access Objects, Entities 이렇게 3개가 Room의 구성 요소이고
Rest of The App은 앱의 나머지 부분을 뜻한다.
1. Room 구성요소들 생성
1-1) Entity를 생성해야 한다. (음...DB 테이블을 만든다고 보자)
- data class에 @Entity 어노테이션을 붙여주고 저장하고 싶은 속성의 변수 이름과 타입을 정해준다.
- primaryKey는유일한(Unique) 값이어야 한다. 직접 지정해도 되지만 autoGenerate를 true로 주면 자동으로 값을 생성한다.
@Entity
data class User(
@PrimaryKey(autoGenerate = true) val uid: Int,
@ColumnInfo(name = "first_name") val firstName: String?,
@ColumnInfo(name = "last_name") val lastName: String?
)
1-2) DAO를 생성
- @Dao 어노테이션을 붙이고 그 안에 메서드를 정의하게 되는데
@Insert를 붙이면 테이블에 데이터 삽입
@Update를 붙이면 테이블의 데이터 수정
@Delete를 붙이면 테이블의 데이터 삭제
@Query 어노테이션을 붙이고 그 안에 어떤 동작을 할 건지 sql 문법으로 작성
@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>
@Query("SELECT * FROM user WHERE first_name LIKE :first AND " +
"last_name LIKE :last LIMIT 1")
fun findByName(first: String, last: String): User
@Insert
fun insertAll(vararg users: User)
@Delete
fun delete(user: User)
}
ㄴ> DAO에 관한 다양한 활용은 Room DAO를 사용하여 데이터 액세스를 참고
1-3) 데이터베이스를 보유할 'Database 클래스'를 정의
- @Database 어노테이션으로 데이터베이스임을 표시하고,
데이터베이스와 연결된 데이터 항목을 모두 나열하는 entities (<< 1-1번에서 생성함) 배열이 포함되어야 한다.
ㄴ> version : 앱을 업데이트하다가 entity의 구조를 변경해야 하는 일이 생겼을 때 이전 구조와 현재 구조를 구분해주는 역할 (만약 구조가 바뀌었는데 버전이 같다면 에러가 뜨며 디버깅이 되지 않는다.) (처음은 그냥 1을 넣어주면 된다.)
- RoomDatabase를 확장하는 추상 클래스
- 데이터베이스와 연결된 각 DAO 클래스에서 데이터베이스 클래스는 인수가 0개이고 DAO 클래스의 인스턴스를 반환하는 추상 메서드를 정의해야 합니다.
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
- 여러 엔티티 사용 시
@Database(entities = arrayOf(User::class, Student::class), version = 1)
abstract class UserDatabase: RoomDatabase() {
abstract fun userDao(): UserDao
}
- 참고) 앱이 단일 프로세스에서 실행되면 AppDatabase 객체를 인스턴스화할 때 싱글톤 디자인 패턴을 따르고,
여러 프로세스에서 실행되는 경우 데이터베이스 빌더 호출에 enableMultiInstanceInvalidation()을 포함
참고: 앱이 단일 프로세스에서 실행되면 AppDatabase 객체를 인스턴스화할 때 싱글톤 디자인 패턴을 따라야 합니다. 각 RoomDatabase 인스턴스는 리소스를 상당히 많이 소비하며 단일 프로세스 내에서 여러 인스턴스에 액세스해야 하는 경우는 거의 없습니다.
앱이 여러 프로세스에서 실행되는 경우 데이터베이스 빌더 호출에 enableMultiInstanceInvalidation()을 포함하세요. 이렇게 하면 각 프로세스에 AppDatabase 인스턴스가 있을 때 한 프로세스에서 공유 데이터베이스 파일을 무효화할 수 있으며 이 무효화는 다른 프로세스 내의 AppDatabase 인스턴스로 자동 전파됩니다.
ㄴㄴ> AppDatabase 객체를 인스턴스화할 때 싱글톤 디자인 패턴으로 ▽
@Database(entities = [User::class], version = 1)
abstract class UserDatabase: RoomDatabase() {
abstract fun userDao(): UserDao
companion object {
private var instance: UserDatabase? = null
@Synchronized
fun getInstance(context: Context): UserDatabase? {
if (instance == null) {
synchronized(UserDatabase::class){
instance = Room.databaseBuilder(
context.applicationContext,
UserDatabase::class.java,
"user-database"
).build()
}
}
return instance
}
}
}
2. Room 사용
// 1. 데이터베이스 객체 정의
// 싱글톤 패턴을 사용한 경우
val db = UserDatabase.getInstance(applicationContext)
db!!.userDao().insert(newUser)
// 싱글톤 패턴을 사용하지 않은 경우
/**
val db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "database-name"
).build()
*/
// 2. AppDatabase의 추상 메서드를 사용하여 DAO 인스턴스를 가져와 데이터베이스와 상호작용
val userDao = db.userDao()
val users: List<User> = userDao.getAll()
var newUser = User("김동길", "20", "010-1111-5555")
userDao.insert(newUser)
이제 실행하면
"Cannot access database on the main thread since it may potentially lock the UI for a long period of time" 에러나온다.
메인 스레드에게 DB 같은 요런 거 시키지 말라고 하는 말이니 비동기 처리한다.
개인적으로 사용해야 하므로 코루틴을 사용하겠다.
▼
class MainActivity : AppCompatActivity() {
private lateinit var db: UserDatabase
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
db = UserDatabase.getInstance(applicationContext)!!
getAllUserList()
}
private fun getAllUserList(){
var userList: String
CoroutineScope(Dispatchers.Main).launch {
val users = CoroutineScope(Dispatchers.IO).async {
db.userDao().getAll()
}.await()
for(user in users){
userList += "name: ${user.name}, age: ${user.age}, phone: ${user.phone}\n"
}
Log.d(TAG, ">> getAllUserList \n ${userList}")
}
}
}
기타
+) 대량의 데이터를 처리하게 될 경우는 Room보다 Realm을 사용하면 좋다.
+) 정말 간단한 정보를 저장할 경우 Room보다 sharedpreferences를 사용하면 좋다.
[출처]
- https://developer.android.com/training/data-storage/room?hl=ko
- https://todaycode.tistory.com/39
-