
서론
스마트폰에서 인터넷 연결이 끊기는 상황은 가장 흔하게 만날 수 있습니다. 저는 아이폰을 사용하고 있는데요, 아이폰은 연결 가능한 와이파이가 있다면 자동으로 연결해 주는 기능이 있습니다.
때로는 이 기능이 편하기도 하지만 연결 상태가 좋지 않은 와이파이에 연결될 경우 인터넷 사용이 불가능한 상황이 자주 일어납니다. 이렇듯 가장 흔하게 많이 일어나는 이슈인 만큼 앱에서 적절한 에러처리를 해줘야 합니다.
오늘은 다온길을 개발하면서 어떻게 네트워크 에러를 처리했는지 소개하겠습니다.
실시간으로 네트워크 상태를 받아오자
우선 기기의 현재 네트워크 연결 상태를 확인하기 위해서 Connectivity를 사용해야 합니다. 안드로이드에선 사용자에게 다양한 형태의 네트워크 작업을 할 수 있도록 Wi-Fi에 연결되었을 때만 업로드/다운로드 실행 여부, 로밍 중 데이터 사용 여부등을 다양한 기능을 구현할 수 있는 클래스를 제공합니다.
그리고 공식문서에 따르면 이 중 Connectivity는 네트워크 연결이 변경될 때 어플리케이션에 상태를 전달해 줍니다.
네트워크 상태를 관리하는 인터페이스를 하나 선언해주었습니다. 이 인터페이스는 네트워크 연결 상태를 나타내는 Status enum 클래스를 정의합니다. Status enum은 다음과 같은 네 가지 값을 가질 수 있습니다.
- Available: 네트워크에 연결되어 있고 인터넷에 접근할 수 있는 상태, 앱은 정상적으로 네트워크 작업을 수행할 수 있습니다.
- Unavailable: 네트워크에 연결되어 있지 않거나 인터넷에 접근할 수 없는 상태,. 앱은 네트워크 작업을 수행할 수 없습니다.
- Losing: 네트워크 연결이 곧 끊어질 것으로 예상되는 상태, 앱은 네트워크 작업을 완료하거나 일시 중지할 준비를 해야 합니다.
- Lost: 네트워크 연결이 끊어진 상태,. 앱은 네트워크 작업을 중단하고 연결이 복구될 때까지 기다려야 합니다.
interface ConnectivityObserver {
fun getFlow(): Flow<Status>
enum class Status{
Available, Unavailable, Losing, Lost
}
}
ConnectivityObserver 인터페이스의 구현체 클래스를 생성합니다. NetworkConnectivityObserver는 context를 전달받아 connectivityManager를 생성합니다. Status는 실시간으로 네트워크 상태를 받아오기 위해 Flow로 선언해 주었습니다.
단, Flow는 콜드 스트림으로 실제로 collect를 호출하여 Flow를 구독하기 전까지는 네트워크 상태를 수집하지 않고 방출하지도 않습니다. 이를 핫 스트림으로 만들어 구독자가 항상 최신 상태만 가질 수 있도록 만들기 위해 핫 스트림인 StateFlow로 변환하는 stateIn 함수를 사용했습니다.
class NetworkConnectivityObserver(
context: Context
): ConnectivityObserver {
private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
@OptIn(DelicateCoroutinesApi::class)
private val status: Flow<ConnectivityObserver.Status> =
observe().stateIn(GlobalScope, WhileSubscribed(5000), ConnectivityObserver.Status.Unavailable)
override fun getFlow(): Flow<ConnectivityObserver.Status> {
return status
}
private fun observe(): Flow<ConnectivityObserver.Status> {
return callbackFlow {
val callback = object : ConnectivityManager.NetworkCallback(){
override fun onAvailable(network: Network) {
super.onAvailable(network)
launch { send(ConnectivityObserver.Status.Available) }
}
override fun onLosing(network: Network, maxMsToLive: Int) {
super.onLosing(network, maxMsToLive)
launch { send(ConnectivityObserver.Status.Losing) }
}
override fun onLost(network: Network) {
super.onLost(network)
launch { send(ConnectivityObserver.Status.Lost) }
}
override fun onUnavailable(){
super.onUnavailable()
launch { send(ConnectivityObserver.Status.Unavailable) }
}
}
connectivityManager.registerDefaultNetworkCallback(callback)
awaitClose{
connectivityManager.unregisterNetworkCallback(callback)
}
}.distinctUntilChanged()
}
companion object{
@Volatile
private var INSTANCE: NetworkConnectivityObserver? = null
fun getInstance(context: Context): ConnectivityObserver{
return INSTANCE ?: synchronized(this){
INSTANCE ?: NetworkConnectivityObserver(context).also { INSTANCE = it }
}
}
}
}

stateIn 함수는 세 가지 인자를 전달받습니다.
- scope: StateFlow의 수명 주기를 관리할 CoroutineScope입니다. StateFlow는 이 스코프가 활성화되어 있는 동안 값을 방출하고, 스코프가 취소되면 더 이상 값을 방출하지 않습니다. 여기선 NetworkConnectivityObserver를 싱글톤으로 만들기 때문에 앱 전체에서 사용할 수 있도록 GlobalScope를 전달해 주었습니다.
- started: Flow의 공유를 시작하는 방식을 지정하는 SharingStarted 값입니다.
- SharingStarted.Eagerly: stateIn 함수가 호출되는 즉시 Flow의 공유를 시작합니다.
- SharingStarted.Lazily: 첫 번째 구독자가 나타날 때 Flow의 공유를 시작합니다.
- SharingStarted.WhileSubscribed(): 구독자가 있는 동안 Flow의 공유를 유지하고, 일정 시간 동안 구독자가 없으면 공유를 중단합니다.
- initialValue: StateFlow의 초기 값입니다. Flow에서 첫 번째 값을 방출하기 전에 StateFlow는 이 값을 갖습니다.
observe 함수는 ConnectivtyManager의 NetworkCallback을 사용해 네트워크 상태에 따라 콜백 메소드들이 호출되고 이 호출된 콜백이 Flow 형태로 전달됩니다.
- callbackFlow는 callbackFlow는 코루틴 빌더 중 하나로 문자 그대로 callback 함수의 결과를 Flow로 반환합니다.
- send( )를 사용해 callbackFlow 내부에서 콜백 결과를 Flow로 전달합니다.
- 주의할 점은 callbackFlow는 한번 등록한 콜백을 스스로 해제하지 못합니다. 이 때문에 불필요한 메모리릭을 방지해야 하는데, awaitClose을 사용해 명시적으로 더 이상 사용하지 않는 콜백의 등록을 해제합니다.
- 콜백 메서드는 메인 스레드에서 호출될 수 있으므로, Flow의 send() 함수를 호출하기 위해서는 별도의 코루틴을 사용해야 합니다.
- distinctUntilChanged() 연산자는 연속적으로 동일한 값이 방출되는 것을 방지합니다.
- 네트워크 상태가 변경되지 않았는데도 불필요하게 UI 업데이트가 발생하는 것을 막기 위해 사용합니다.
해당 클래스를 사용해 실제 동작한 영상은 동영상 첨부가 안 되는 관계로 링크에 남기겠습니다.
마무리
오늘은 다소 짧은 내용으로 적어봤습니다. 다온길의 기존 인터넷 에러 처리 방식은 다음과 같았습니다. 화면이 전환되면 필요한 데이터를 API에 호출하고 인터넷이 끊겼기 때문에 서버 응답으로 ConnectionExcepion을 반환받고 대응했습니다.
화면 전환 -> 인터넷 연결 끊김 -> API 호출 -> ConnectError 반환 -> 에러처리 -> 인터넷 연결 -> API 호출
하지만 이번 내용을 바탕으로 네트워크 상태를 관리하는 클래스를 만들고 인터넷이 연결된 상태에서만 API 요청을 하게 만듦으로써 다음과 같이 불필요한 API 요청을 사전에 막을 수 있었습니다.
화면 전환 -> 인터넷 연결 끊김 -> 에러처리 -> 인터넷 연결 -> API 호출
위 링크에 첨부된 영상을 보시면 아시겠지만 인터넷이 연결되지 않았을 때는 에러 페이지를 보여주고 인터넷이 연결된 순간 자동으로 API 호출을 하게 만들어 사용자 경험을 개선시킬 수 있었습니다. 결과적으로 사용자 편의성을 위해 개발한 기능이었지만 저희 자체적인 서버 트래픽에도 도움이 되는 기능이 되었습니다.
'PROJECT' 카테고리의 다른 글
| 안드로이드 접근성 개선기 (0) | 2024.08.26 |
|---|---|
| 안드로이드 리사이클러뷰 성능 개선 일지 4편 (0) | 2024.08.11 |
| 안드로이드 리사이클러뷰 성능 개선 일지 3편 (1) | 2024.08.10 |
| 안드로이드 리사이클러뷰 성능 개선 일지 2편(부제 : DiffUtil Deep Dive) (1) | 2024.08.04 |
| 안드로이드 리사이클러뷰 성능 개선 일지 1편(부제: Recyclerview Deep Dive) (0) | 2024.08.03 |
