에러 상황

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를 사용하기 위해선 세 가지 방법이 있다.

1. 데이터베이스 활용 작업을 백그라운드 스레드에서 수행(Thread, Coroutine)

백그라운드 스레드에서 데이터베이스 작업을 처리하기 위해 Executor, Thread, Coroutine을 활용한다.
나의 경우도 Coroutine을 사용해 해당 에러를 해결했다.

우선 RepositoryDao에서 해당 메서드를 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
                )
            }
        }
    }
}

2. 메인 스레드에서 데이터베이스에 접근해야한다면 비동기 처리 (LiveData, Flow)

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 업데이트
        }
    }
}

3. 선택적으로 allowMainThreadQueries() 사용

메인 스레드에서 직접 데이터베이스 접근이 가능하지만 ANR의 위험이 있으므로 실제 배포 때는 사용하는 것 지양해야 한다.

val db = Room.databaseBuilder(
    context,
    AppDatabase::class.java, "database-name"
)
    .allowMainThreadQueries() // ANR 위험
    .build()
profile
Android 짱이 되고싶은 개발 기록 (+ ios도 조금씩,,👩🏻‍💻)

0개의 댓글

관련 채용 정보

Powered by GraphCDN, the GraphQL CDN