Room Database를 활용해 안드로이드 앱 내부에서 CRUD를 구현하던 중, 할 일을 작성 후 할 일 목록에서 작성한 글 불러오기를 할 때 다음 에러가 발생하면서 앱이 강제종료 되었다.
java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.
at androidx.room.RoomDatabase.assertNotMainThread(RoomDatabase.kt:439)
에러의 원인은 Room 데이터베이스 접근을 메인 스레드에서 시도한 것이다.
Room은 메인 스레드에서의 데이터베이스 작업을 기본적으로 금지해서 UI 블락으로 인한 ANR을 방지한다.
📄 RoomDatabase.Builder의 메인 스레드 엑세스 관련 공식 문서
메인 스레드에서의 작업을 금지하는 Room의 database를 사용하기 위해선 세 가지 방법이 있다.
백그라운드 스레드에서 데이터베이스 작업을 처리하기 위해 Executor, Thread, Coroutine을 활용한다.
나의 경우도 Coroutine을 사용해 해당 에러를 해결했다.
우선 Repository
와 Dao
에서 해당 메서드를 suspend
함수로 정의했다.
// Repository
class DailyTodoRepository(private val dailyTodoDao: DailyTodoDao) {
suspend fun getTodoById(todoId: Int): DailyTodoEntity? {
return dailyTodoDao.getTodoById(todoId) // DAO에서 suspend 함수 호출
}
}
// Dao
@Dao
interface DailyTodoDao {
@Query("SELECT * FROM daily_table WHERE id = :todoId")
suspend fun getTodoById(todoId: Int): DailyTodoEntity?
}
ViewModel에서 viewModelScope.launch
를 통해 백그라운드 스레드에서 데이터베이스 작업이 실행되도록 수정했다.
// ViewModel
class DailyTodoViewModel(private val repository: DailyTodoRepository) : ViewModel() {
// StateFlow => 현재 선택된 Todo 데이터 관리(UI)
private val _selectedTodo = MutableStateFlow<DailyTodoEntity?>(null)
val selectedTodo: StateFlow<DailyTodoEntity?> get() = _selectedTodo
// Todo 업데이트
fun updateDaily(todo: DailyTodoEntity) {
viewModelScope.launch {
repository.update(todo)
}
}
// Todo 데이터 id로 데이터 가져오기
fun loadDailyTodoById(todoId: Int) {
viewModelScope.launch {
val todo = repository.getTodoById(todoId)
_selectedTodo.value = todo
}
}
}
UI에서 LaunchedEffect
안에서 ViewModel의 메서드를호출하고 selectedTodo를 통해 데이터를 구독한다.
@Composable
fun DetailScreen(navController: NavController, viewModel: DailyTodoViewModel, todoId: Int) {
// todoId 변경 시 데이터 로드
LaunchedEffect(todoId) {
viewModel.loadTodoById(todoId)
}
// ViewModel의 Flow로부터 데이터를 구독
val todo = viewModel.selectedTodo.collectAsState().value
Scaffold(
topBar = { TopAppBar(screen = "Detail") },
bottomBar = { BottomAppBar(navController = navController) }
) { paddingValues ->
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp)
.background(MaterialTheme.colorScheme.background),
) {
item {
todo?.let {
DetailTodoListSection(it, viewModel)
} ?: Text(
text = "데이터를 찾을 수 없습니다.",
fontSize = 16.sp,
color = MaterialTheme.colorScheme.error
)
}
}
}
}
Room은 메인 스레드에서의 데이터베이스 접근을 기본적으로 금지하지만, LiveData나 Flow를 활용하면 비동기적으로 데이터 변경을 감지하고 UI를 업데이트할 수 있다.
// ver1. LiveData 사용
val tasks: LiveData<List<Task>> = taskDao.getAllTasks()
tasks.observe(this, Observer { taskList ->
// UI 업데이트
})
// vwe2. Flow 사용
fun observeTasks() {
CoroutineScope(Dispatchers.Main).launch {
taskDao.getAllTasksFlow().collect { taskList ->
// UI 업데이트
}
}
}
메인 스레드에서 직접 데이터베이스 접근이 가능하지만 ANR의 위험이 있으므로 실제 배포 때는 사용하는 것 지양해야 한다.
val db = Room.databaseBuilder(
context,
AppDatabase::class.java, "database-name"
)
.allowMainThreadQueries() // ANR 위험
.build()