[Android] offline-first App Crash Bug 해결과정

D.O·2024년 4월 27일
0
post-thumbnail

offline first 앱 구현 도중 네트워크가 연결되어 있지 않는 상황에서 검색 실행 시 아래와 같이 앱이 크래시 되는 버그가 발생했다.

offline을 지원하는 앱인데 offline 모드에서 동작 시 앱이 죽다니.. 수정이 필요했다.

Screen_recording_20240427_153634-ezgif com-video-to-gif-converter

일단 내가 구현한 Search 로직부터 설명하겠다.

먼저 해당 애플리케이션은
1. 페이징을 이용
2. offline-first를 위한 페이징 데이터 데이터 베이스 캐시

효율적인 페이징 처리를 위해서 데이터베이스 캐싱이 어디 페이지 까지 이루어졌는지를 관리하기 위해 페이징키 또한 따로 데이터베이스로 관리한다. 또한 캐싱 데이터에 유효기간을 위해서 lastUpdate 또한 관리한다.

실제 해당 picsum api 페이징 요청을 하면 이전 페이지와 다음 페이지의 Link를 response 헤더에서 제공 하는데 이를 통해 마지막 페이지 인지, 첫번째 페이지 인지 확인이 가능하다.

즉 해당 응답 헤더에 next에 대한 정보가 없다면 마지막 페이지 prev에 대한 정보가 없다면 첫 페이지라는 것이다.

아래 페이지 3의 경우 prev와 next가 둘다 존재한다.

마지막 페이지인 34는 어떨가

prev는 존재하지만 next는 존재하지 않는다.

이제 검색 쿼리를 처리하는 로직을 보자

나는 검색이라는 것은 완전성을 보장해야한다고 생각을하고 현재 로드된 정보에 대해서만 검색을 하는게 아닌 검색을 위한 쿼리가 일어나기전에 모든 데이터가 들어와야한다고 생각한다.

사실 조금 비효율적으로 보일수도 있다.
데이터 완전성을 보장하면서 내가 생각한 현재 최선은 이거였다.

[1] 검색이 일어난다.
[2.1] keyInfo를 확인하여 마지막 페이지까지 로드가 되었다면 3단계 실행
[2.2] 만약 마지막 페이지 까지 로드가 되지않았다면 모든 데이터 로드 후 데이터베이스에 삽입 후 3단계 실행
[3] database에서 쿼리를 실행하여 검색 결과를 반환

여기서 오류가 발생했던 부분은 2.2이다.

private suspend fun loadAllDataForQuery(nextPage: Int) {
        var currentPage = nextPage
        var shouldContinue = true
        val tasks = mutableListOf<Deferred<Boolean>>()

        withContext(ioDispatcher) {
            while (currentPage <= 100 && shouldContinue) {
                val task = async {
                    val response = retrofitPsNetwork.getFeeds(page = currentPage, limit = PAGE_SIZE)
                    val entities = response.feedList

                    if (entities.isNotEmpty()) {
                        psDatabase.withTransaction {
                            psDatabase.getFeedResourceDao.upsertAll(entities.map { it.asEntity() })
                            val keys = entities.map { feed ->
                                FeedKeyInfoEntity(
                                    id = feed.id.toInt(),
                                    prevPage = response.prevPage,
                                    nextPage = response.nextPage,
                                    lastUpdated = System.currentTimeMillis()
                                )
                            }
                            psDatabase.getFeedKeyInfoDao.replace(keys)
                        }
                        response.nextPage != null
                    } else {
                        false
                    }
                }
                tasks.add(task)
                currentPage++

                if (tasks.size >= 5) {
                    shouldContinue = tasks.awaitAll().all { it }
                    tasks.clear()
                }
            }

            if (tasks.isNotEmpty()) {
                shouldContinue = tasks.awaitAll().all { it }
            }
        }
    }

ioDispatcher에서 실행되는 여러 코루틴을 통해 데이터를 비동기적으로 로드합니다.
nextPage 매개변수에서 시작하여, 각 페이지의 네트워크 응답을 처리합니다. 응답받은 데이터가 비어있지 않고, nextPage가 null이 아닌 경우에만 다음 페이지 데이터의 로딩이 계속됩니다.

빠른 결과 응답과 효율적인 데이터 처리를 달성하기 위해 이렇게 설계했습니다.

이 부분에서는 문제가 있습니다.
네트워크가 되지 않을때 예외 처리를 하는 부분이 누락되어 있습니다.

따라서 현재 에러가 발생하는 상황은

오프라인 모드인 상황에서 검색을 실행하는데 모든 페이지가 로드되지않아서 loadAllDateForQuery가 실행되어야 할 부분에서 이러한 네트워크 오류로 인해 앱 크래시가 발생했습니다.


				try {
                        val response = retrofitPsNetwork.getFeeds(page = currentPage, limit = PAGE_SIZE)
                        val entities = response.feedList

                        if (entities.isNotEmpty()) {
                            psDatabase.withTransaction {
                                psDatabase.getFeedResourceDao.upsertAll(entities.map { it.asEntity() })
                                val keys = entities.map { feed ->
                                    FeedKeyInfoEntity(
                                        id = feed.id.toInt(),
                                        prevPage = response.prevPage,
                                        nextPage = response.nextPage,
                                        lastUpdated = System.currentTimeMillis()
                                    )
                                }
                                psDatabase.getFeedKeyInfoDao.replace(keys)
                            }
                            response.nextPage != null
                        } else {
                            false
                        }
                    } catch (e: Exception) {
                        Log.e("LoadData", "Error : ${e.message}")
                        false // 오류 발생 시 계속 진행하지 않음
                    }

간단하게 에러가 발생하면 task가 false를 반환하게하여 더 이상 진행하지 않게 만들었습니다.

결과

profile
Android Developer

0개의 댓글