해당 내용은 하기 링크를 번역한 내용입니다.
https://medium.com/androiddevelopers/room-coroutines-422b786dc4c5
물론 추가적인 의역 및 설명이 추가되어 있습니다.
Room 2.1에서 부터 Kotlin coroutine을 지원하기 시작했습니다. 2.2 버전부터는 return값으로 flow도 제공하지요~
Room에서 coroutine을 지원하면서 DAO에서 suspend function을 만들어 선언해 놓으면 해당 function들은 main thread에서 동작하지 않습니다.
따라서 DB 접근할때 따로 background thread를 만들어서 접근할 필요가 없어졌습니다.
아래에서는 이를 사용하는 방법과 내부적으로 어떻게 동작하는지와 test는 어떻게 진행해야하는지에 대해 설명합니다.
Add some suspense to your database
추가적으로 kotlin은 1.3.0 이상, coroutine은 1.0.0 이상이 필요합니다.
현재 버전으로 kotlin은 1.3.6, 코루틴은 1.3.2가 최신입니다 (2019.11.25 기준)
Dao의 경우 아래 예제처럼 suspend function을 정의하면 됩니다.
기본 function에서 단순히 suspend만 붙임으로써 cortouine을 이용하여 background에서 동작하는 함수를 생성할 수 있습니다.
@Dao
interface UsersDao {
@Query("SELECT * FROM users")
suspend fun getUsers(): List<User>
@Query("UPDATE users SET age = age + 1 WHERE userId = :userId")
suspend fun incrementUserAge(userId: String)
@Insert
suspend fun insertUser(user: User)
@Update
suspend fun updateUser(user: User)
@Delete
suspend fun deleteUser(user: User)
}
@Transaction역시 suspend 함수로 만들 수 있으며, 다른 suspend function에서 호출될 수 있습니다.
@Dao
abstract class UsersDao {
@Transaction
open suspend fun setLoggedInUser(loggedInUser: User) {
deleteUser(loggedInUser)
insertUser(loggedInUser)
}
@Query("DELETE FROM users")
abstract fun deleteUser(user: User)
@Insert
abstract suspend fun insertUser(user: User)
}
또한 다른 DAO에서 transaction을 만들어 suspension 함수를 호출 할 수도 있습니다.
class Repository(val database: MyDatabase) {
suspend fun clearData(){
database.withTransaction {
database.userDao().deleteLoggedInUser() // suspend function
database.commentsDao().deleteComments() // suspend function
}
}
}
여기서 사용된 withTransaction은 suspend 함수 입니다.
따라서 이 extension function을 사용하려면 coroutineScope 내부에서 호출되어야 합니다.
내부 구현 사항을 따라가 보면 Transaction을 열고 해당 block의 함수를 수행후 Transaction을 닫도록 되어 있습니다.
또한 setTransctionExecutor 또는 setQueryExecutor를 사용하여, room database를 build 할 때 room의 쿼리를 동작시킬 executor를 지정해 줄수도 있습니다.
상세한 내용은 아래 링크에서 확인 가능합니다.
- non-blocking asynchronus transction query나 일반 query를 실행할 executor를 따로 세팅 할수 있다!
- 이렇게 설정한 executor는 LiveData의 유효성 검사, Flowable scheduling, ListenableFuture task에도 쓰인다.
- setTransctionExecutor와 setQueryExecutor 둘다 세팅하지 않은경우 AAC lib에서 제공하는 thread를 공휴해서 쓴다.
- setTransctionExecutor를 설정하지 않았지만 setQueryExecutor을 설정하면 transaction에서도 설정된 executor를 사용한다.
Testing DAO suspension functions
@Test fun insertAndGetUser() = runBlocking {
// Given a User that has been inserted into the DB
userDao.insertUser(user)
// When getting the Users via the DAO
val usersFromDb = userDao.getUsers()
// Then the retrieved Users matches the original user object
assertEquals(listOf(user), userFromDb)
}
runBlocking으로 coroutine scope을 만들어서 테스트하면 됩니다.
Under the hood
@Insert
fun insertUserSync(user: User)
@Insert
suspend fun insertUser(user: User)
위와 같이 두개의 함수를 만듭니다.@Override
public void insertUserSync(final User user) {
__db.beginTransaction();
try {
__insertionAdapterOfUser.insert(user);
__db.setTransactionSuccessful();
} finally {
__db.endTransaction();
}
}
일반적인 함수 (synchronous 하게 동작하는)는 transaction을 열고 해당 구문을 실행후 transaction을 닫습니다.
해당 함수는 해당 함수를 호출한 thread에서 수행합니다.
@Override
public Object insertUserSuspend(final User user,
final Continuation<? super Unit> p1) {
return CoroutinesRoom.execute(__db, new Callable<Unit>() {
@Override
public Unit call() throws Exception {
__db.beginTransaction();
try {
__insertionAdapterOfUser.insert(user);
__db.setTransactionSuccessful();
return kotlin.Unit.INSTANCE;
} finally {
__db.endTransaction();
}
}
}, p1);
}
위에 suspend function의 구현부분을 보면 해당 insert 구문이 UI thread에서 수행되지 않다는걸 확인할 수 있습니다.
일반 함수 (synchronous 함수)의 내용이 Callable로 wrapping되어서 생성되며 이 Callable은 CoroutinesRoom.execute 라는 suspend function에 의해 수행됩니다.
@JvmStatic
suspend fun <R> execute(
db: RoomDatabase,
inTransaction: Boolean,
callable: Callable<R>
): R {
if (db.isOpen && db.inTransaction()) {
return callable.call()
}
// Use the transaction dispatcher if we are on a transaction coroutine, otherwise
// use the database dispatchers.
val context = coroutineContext[TransactionElement]?.transactionDispatcher
?: if (inTransaction) db.transactionDispatcher else db.queryDispatcher
return withContext(context) {
callable.call()
}
}
Case 1. The database is opened and we are in a transaction
DB가 open된 상태이고 이미 transaction 상태이 있다면 callable의 call() 함수를 그냥 수행시킵니다.
그럼 내부에 선언된 insert문이 호출되겠죠?
Case 2. Otherwise
room은 Callable의 call()함수의 내용이 background thread에서의 실행을 보장합니다.
이때 Room은 transactions 작업과 query 작업에 다른 dispatcher를 사용합니다.
이 dispatcher는 room을 빌드할때 설정 할 수 있으며, 만약 따로 설정하지 않았다면 Architecture Component의 IO executor를 사용합니다.
(이건 LiveData의 background 작업을 처리하는 executor와 동일한 executor를 사용합니다.)
결론적으로 Room에서 suspend function을 사용하면 database의 작업이 non-UI Dispatcher에 동작함을 보장합니다.
따라서 DAO를 만들때 suspend를 붙여서 다른 suspend function이나 coroutine에서 호출해서 사용할 수 있습니다.
'개발이야기 > Android' 카테고리의 다른 글
Android view binding (뷰 바인딩) (2) | 2020.03.04 |
---|---|
[Rxbinding] RxJava를 이용한 Android의 이벤트 처리 (2) | 2020.01.08 |
Android Dev Summit 2019 - Debugging Tips n' Tricks (0) | 2019.11.20 |
Android Dev Summit 2019 - What's new in Room (5) | 2019.11.18 |
Android Dev Summit 2019 - Testing Coroutines on Android (0) | 2019.11.15 |