본문 바로가기

Android

Room에 데이터 Insert 시 보다 효율적인 방법은?

반응형

Room을 통해 데이터베이스에 데이터를 집어넣을 때, ANR(Application Not Responding, 어플리케이션 응답 없음) 에러를 피하기 위해 UI 스레드가 아닌, 백그라운드 스레드에서 작업하여야 한다. 또한, 코틀린 환경이라면 백그라운드 작업을 위해 Coroutine을 사용하는 것이 일반적이다.

그렇다면, 어떻게 집어넣는 것이 이상적일까? 하나의 코루틴 스코프에서 insert를 여러 번 하는 것이 나을까, 코루틴을 마구 만들어서 집어넣는 것이 나을까?

다음 세 가지 상황을 가정하고 실험을 해봤다.

첫 번째 케이스: 하나의 coroutine scope, vararg를 이용한 insert

val job1: Deferred<Unit>
val time1 = measureTimeMillis {
  job1 = GlobalScope.async {
    diaryDao.insert(*(1L..1000L).map { Diary(it) }.toTypedArray())
  }
}
val time2 = measureTimeMillis { job1.await() }
Log.d(TAG, "databaseInsertSpeed_measure: $time1 $time2")

두 번째 케이스: 하나의 coroutine scope, 하나씩 insert

val job2: Deferred<Unit>
val time3 = measureTimeMillis {
  job2 = GlobalScope.async {
    (1L..1000L)
      .map { Diary(it) }
      .forEach {
        diaryDao.insert(it)
      }
  }
}
val time4 = measureTimeMillis { job2.await() }
Log.d(TAG, "databaseInsertSpeed_measure: $time3 $time4")

세 번째 케이스: 하나씩 insert, 각각 coroutine scope를 갖도록

val job3: List<Deferred<Unit>>
val time5 = measureTimeMillis {
  job3 = (1L..1000L).map {
    GlobalScope.async {
      diaryDao.insert(Diary(it))
    }
  }
}
val time6 = measureTimeMillis {
  job3.forEach { it.await() }
}
Log.d(TAG, "databaseInsertSpeed_measure: $time5 $time6")

예상

단순 지연이 있는 작업 같은 경우에는 작업을 병렬 수행 할 수 있어 2, 3 중 3이 더 빠르다. 한 가지 더 고려해야 할 점은, 데이터베이스 작업의 경우 작업의 유일성을 지키기 위해 lock이 걸린다는 점이다. lock을 기다려야 한다면 병렬성, 동시성은 의미가 없어지기에 1 >= 2 = 3 정도의 큰 차이 없는 결과를 예측했다.

결과

결과는 1 > 3 > 2였으며, 최대 17배의 성능 차이를 보였다. 하나의 coroutine scope에 vararg를 이용하여 insert 하는 방식(1)이 가장 빠르고, 하나의 coroutine scope에 하나씩 insert 하는 방식(2)이 제일 오래 걸린다. 3의 경우, 많은 양의 스코프를 선언하기 때문에 작업을 선언하는 데 가장 오래 걸리지만, 선형적으로 증가하진 않는 모습이다. 또한, 3이 2보다 빠른 이유는, 3이 Lock을 갖고 진입해야 하는 Critical Section을 제외한 나머지 부분을 동시적으로 수행하기 때문이다.

코드

전체 테스트 코드

package com.roomedia.dakku

import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.roomedia.dakku.persistence.Diary
import com.roomedia.dakku.repository.DiaryRepository
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.system.measureTimeMillis

@RunWith(AndroidJUnit4::class)
class DatabaseMeasureTimeTest {

    private val diaryRepository = DiaryRepository()
    private val diaryDao = diaryRepository.dao

    @Test
    fun databaseInsertSpeed_measure(): Unit = runBlocking {
        diaryRepository.deleteAll()
        val job1: Deferred<Unit>
        val time1 = measureTimeMillis {
            job1 = GlobalScope.async {
                diaryDao.insert(*(1L..1000L).map { Diary(it) }.toTypedArray())
            }
        }
        val time2 = measureTimeMillis { job1.await() }
        Log.d(TAG, "databaseInsertSpeed_measure: $time1 $time2")

        diaryRepository.deleteAll()
        val job2: Deferred<Unit>
        val time3 = measureTimeMillis {
            job2 = GlobalScope.async {
                (1L..1000L)
                    .map { Diary(it) }
                    .forEach {
                        diaryDao.insert(it)
                    }
            }
        }
        val time4 = measureTimeMillis { job2.await() }
        Log.d(TAG, "databaseInsertSpeed_measure: $time3 $time4")

        diaryRepository.deleteAll()
        val job3: List<Deferred<Unit>>
        val time5 = measureTimeMillis {
            job3 = (1L..1000L).map {
                GlobalScope.async {
                    diaryDao.insert(Diary(it))
                }
            }
        }
        val time6 = measureTimeMillis {
            job3.forEach { it.await() }
        }
        Log.d(TAG, "databaseInsertSpeed_measure: $time5 $time6")
    }

    companion object {
        private const val TAG = "measured"
    }
}

dao insert 코드

package com.roomedia.dakku.persistence

import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Update

interface CommonDao<T> {
    @Insert
    fun insert(vararg entities: T)

    @Update
    fun update(vararg entities: T)

    @Delete
    fun delete(vararg entities: T)
}

 

반응형