본문 바로가기
Android/Room

[Android] Room에서 코루틴의 suspend함수를 사용할 때 백그라운드 스레드를 직접 생성하지 않아도 되는 이유

by 태크민 2025. 3. 7.

들어가기전에

우리는 데이터베이스(DB)에서 트랜잭션을 처리할 때, 보통 백그라운드 스레드를 생성하여 실행하는 것이 일반적입니다.

이는 DB 연산이 I/O 작업이므로 메인 스레드에서 실행하면 UI가 멈추는 등의 문제가 발생할 수 있기 때문입니다.

과거에는 직접 Thread나 AsyncTask 등을 활용하여 별도의 스레드에서 DB 작업을 수행했습니다.
이후 RxJava가 등장하면서 Scheduler를 이용해 비동기적으로 DB 작업을 수행하는 것이 가능해졌습니다.

 
하지만, 우리가 코루틴을 Room과 함께 사용할 때는, 별도의 백그라운드 스레드를 생성하여 처리하지 않습니다.
 
이것이 가능한 이유는 내부적으로 Room이 suspend 함수 여부를 체크하고, 자체적으로 백그라운드 작업을 처리해주기 때문입니다.
 
코루틴 환경에서 어떻게 이것이 가능한지 이번 포스팅을 통해 알아보도록 하겠습니다.
 

Under the hood

Room 사용자에게 간단한 annotation으로 DB에 접근할수 있는 방법을 제공합니다.

방법이라고는 하나 사실 complie시점에 실제 DB에 접근하기 위한 코드를 대신 생성해 줍니다.

 

내부적으로 Room에서 일반적인 function과 suspend function을 코드로 구현하는지 확인해 보겠습니다.

@Insert
fun insertUserSync(user: User)

@Insert
suspend fun insertUser(user: User)

 

위와 같이 두개의 함수를 만듭니다.

똑같지만 하나는 일반 함수고 하나는 suspend 함수 입니다.  

 

@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에서 호출해서 사용할 수 있습니다.

 

 

마치며..

 Room과 Coroutine을 함께 사용하게 되면 DB 작업들은 UI 쓰레드가 아닌 디스패처에서 실행하게 됩니다.. Room이어서가 아닌, Coroutine이어서 그런 것입니다. suspend 함수는 다른 suspend 함수나 코루틴에서만 실행될 수 있습니다. Room 사용법과 함께 Coroutine에 대한 이해도 충분히 한 후에 사용하는 것을 권장합니다.


참고자료

https://tourspace.tistory.com/272

 

Android Room & Coroutines

해당 내용은 하기 링크를 번역한 내용입니다. https://medium.com/androiddevelopers/room-coroutines-422b786dc4c5 물론 추가적인 의역 및 설명이 추가되어 있습니다.Room 2.1에서 부터 Kotlin coroutine을 지원하기 시작

tourspace.tistory.com

https://dev-repository.tistory.com/54

 

Android Room - Coroutines(코루틴)과 같이 쓰는 법

이번 포스팅에서는 지난 번에 설명한 Room과 Coroutine을 함께 사용하는 방법에 대해 알아 본다. Room은 2.1 버전부터 Coroutine을 지원해주고 있다. DAO 메소드를 suspend로 선언하여 사용할 수 있다. build.gr

dev-repository.tistory.com

 

'Android > Room' 카테고리의 다른 글

[Android] Room DB란?  (0) 2023.09.07