Android/Utility

[Android] Network 연결 여부를 화면과 API에서 체크하는 방법.

Heeg's 2024. 6. 3. 23:36
728x90

업무를 진행하다 보면, 네트워크가 끊겼을 때 혹은 네트워크가 불안정한 경우 어떻게 처리할 것인가? 에 대한 고민을 하게 된다.

 

네트워크에 연결이 되어 있지 않으면 화면을 변경한다던지, 네트워크에 관련된 메시지를 보여준다던지 등 다양한 방법으로 처리가 가능한 부분인데, 이번에는 이 네트워크 연결에 대한 체크를 할 수 있도록 간단한 코드를 안내하고자 한다.

 

필자는 Koin을 사용하고, Compose 환경에서 예제를 만들었지만 DI를 통한 주입 방식이 다른 경우 것을 제외하고는 대부분의 환경에서 동일하게 사용이 가능할 것이다.


우선,

Koin을 사용하여 네트워크에 관련된 Util을 주입해주어야 하고, Compose를 사용하기 때문에 다음과 같은 Koin 라이브러리를 추가해 주고 시작하도록 하겠다.

implementation "io.insert-koin:koin-core:3.2.2"
implementation "io.insert-koin:koin-android:3.2.2"
implementation "io.insert-koin:koin-androidx-compose:3.2.2"

 

첫 번째로는, 

아주 간단하게 네트워크 연결 여부에 따른 Exception을 API 통신에서 발생시키는 방법이다.

 

Retrofit을 사용하는 경우, 아주 쉽게 OkHttpClient에 커스텀한 Interceptor를 추가할 수 있는데 이것을 Koin을 사용하여 주입해 사용하는 방식이다.

Interceptor부터 선언해 보도록 하자.

class NetworkInterceptor() : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {
        return chain.proceed(chain.request())
    }
}

 

가장 기본적인 구조이다.

 

해당 함수에 대해서 간단하게 설명하자면 다음과 같다.

  • intercept함수는 이름 그대로 네트워크 요청을 중간에 가로채고 원하는 로직을 수행한 후에 다음 요청을 진행시키거나 요청을 중단하는 등의 역할을 할 수 있다.
  • chain의 경우, 현재 네트워크 요청에 대한 데이터가 들어간 객체이다
  • chain.request()는 현재 요청을 가져오기 위한 함수이다.
  • chain.proceed(request)는 요청을 진행하여 서버로부터 응답을 받아오는 함수이다.

즉, 위의 함수에서 별다른 로직을 처리하지 않고 바로 return을 수행한다면, 추가적인 작업 없이 다음 작업을 요청하게 되므로 있으나 없으나 차이가 없게 된다.

 

여기서 우리는 네트워크 연결 유무를 확인하여 exception을 발생시킬 것이다.

 

그러기 위해서, 다음으로는 네트워크 연결을 확인할 수 있는 코드를 구현해 보자.

class NetworkUtil(private val context: Context) {
    fun isNetworkConnected(): Boolean {
        val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            val activeNetwork = connectivityManager.activeNetwork ?: return false
            val isActiveNetwork = connectivityManager.getNetworkCapabilities(activeNetwork) ?: return false
            return when {
                isActiveNetwork.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
                isActiveNetwork.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
                else -> false
            }
        } else {
            val networkInfo = connectivityManager.activeNetworkInfo ?: return false
            return networkInfo.isConnected
        }
    }
}

 

안드로이드 환경에서 네트워크를 체크할 수 있는 함수를 찾아보면 쉽게 찾을 수 있는 내용이다.

systemService를 사용하여 현제 네트워크가 연결되어 있는지 체크하여 연결되어 있으면 true, 아니면 false를 반환해 주는 함수이다.

 

이와 같이 Util 클래스와 함수를 선언해 주었으면, 처음 작성했던 인터셉터 함수를 변경해 주자.

class NetworkInterceptor(private val networkUtil: NetworkUtil) : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {
        if (!networkUtil.isNetworkConnected()) {
            throw IOException("Network connection is lost")
        }
        return chain.proceed(chain.request())
    }
}

 

이와 같이 변경해 주면 네트워크 통신 도중에, 다음 요청을 수행하기 전 네트워크가 연결되어 있는지 확인 후 연결 되어있으면 다음 요청을 수행하게 되고 네트워크 연결이 안 되어 있으면 IOExcpetion이 발생하게 된다.

 

이렇게 함수를 구현했으면, Retrofit을 설정해 주는 부분에 해당 Interceptor를 추가해 주도록 하자.

 

필자는 Koin을 사용하였기 때문에 해당 API관련 module에서 OkHttpClient를 빌드하는 부분에 추가해 주도록 하였다.

single {
    OkHttpClient.Builder()
        .run {
            connectTimeout(60, TimeUnit.SECONDS)
            readTimeout(60, TimeUnit.SECONDS)
            writeTimeout(60, TimeUnit.SECONDS)
            addInterceptor(get<Interceptor>())
            addInterceptor(get<HttpLoggingInterceptor>())
            addInterceptor(get<NetworkInterceptor>())
            build()
        }
}

 

마지막 부분에 추가 한 NetworkInterceptor 클래스가 위에 선언한 Interceptor 클래스이므로, 네트워크 통신 중에 해당 인터셉터를 통하게 된다.

 

여기서 잊지 말아야 할 부분은, NetworkInterceptor를 DI를 통해 사용하였으므로, NetworkInterceptor 클래스도 DI로 미리 주입해주어야 한다.

val networkModule: Module = module {
    single { NetworkUtil(get()) }
    single { NetworkInterceptor(get()) }
        ...
}

 

따라서, 별도로 관리하기 위해 다른 모듈에 해당 인터셉터를 추가해 주고, 인터셉터에 매개변수로 들어가는 NetworkUtil 또한 주입할 수 있도록 추가해 주었다.

 

이와 같이 선언하고 API 호출을 할 때 네트워크를 차단해 보면, 정상적으로 Exception이 발생하는 것을 볼 수 있다.

해당 예제는 Github에서 직접 빌드해 보면 쉽게 확인이 가능할 것이다.


다음으로는, UI단에서 확인하는 방법이다.

 

별다른 코드 추가를 하지 않고, 위의 interceptor를 사용하기 위해 선언했던 클래스인 NetworkUtil를 사용하여 간단하게 체크가 가능하다.

val networkUtil: NetworkUtil = get()
    ...
if (networkUtil.isNetworkConnected()) {
    Log.d("NetworkLog", "Connected")
} else {
    Log.d("NetworkLog", "disConnected")
}

 

Koin Module에서 주입을 했던  클래스이기 때문에,  get() 함수를 사용하여 간단하게 NetworkUtil 클래스를 생성할 수 있다.

해당 객체를 생성한 후, 클래스에 선언해 둔 함수인 isNetworkConnected를 호출하여 반환되는 Boolean 타입의 데이터를 기반으로 현재 네트워크가 연결되어 있는지 아닌지 확인이 가능하다.

 

위와 같이 사용하는 경우 해당 함수가 호출되는 그 순간의 Network 연결 여부를 파악하여 데이터를 처리할 수 있다.

하지만, 우리가 실무에서 사용하는 경우엔 해당 함수가 호출되는 순간의 연결 여부도 중요하지만 실시간으로 네트워크 연결 여부를 판단하고 그에 따른 처리를 하길 원한다.

따라서 위에서 선언한 networkUtil 클래스만으로는 원하는 결과 값을 구할 수 없다.

 

그렇기 때문에 실시간으로 네트워크 연결을 확인하기 위하여 라이브 데이터를 사용하여 네트워크 연결 여부를 옵저빙 해보도록 하자.

 

우선 네트워크 연결 여부에 따라서 데이터를 반환하는 라이브 데이터 클래스를 만들어보자.

class NetworkStatusLiveData(context: Context) : LiveData<Boolean>() {
    private val connectivityManager =
        context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
    private val networkCallback = object : ConnectivityManager.NetworkCallback() {
        override fun onAvailable(network: android.net.Network) {
            postValue(true)
        }

        override fun onLost(network: android.net.Network) {
            postValue(false)
        }
    }

    override fun onActive() {
        super.onActive()
        val networkRequest = NetworkRequest.Builder()
            .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
            .build()
        connectivityManager.registerNetworkCallback(networkRequest, networkCallback)
        postValue(isNetworkAvailable())
    }

    override fun onInactive() {
        super.onInactive()
        connectivityManager.unregisterNetworkCallback(networkCallback)
    }

    private fun isNetworkAvailable(): Boolean {
        val activeNetwork = connectivityManager.activeNetworkInfo
        return activeNetwork?.isConnectedOrConnecting == true
    }
}

 

해당 클래스에 대해서 하나씩 확인해 보자.

 

우선,  ConnectivityManager는 기존에 NetworkUtil에서 사용했던 것과 동일하므로 넘어가도록 하겠다.

networkCallback은 생성되는 객체에서 알 수 있듯이, ConnectivityManager에서 제공해 주는 NetworkCallback을 사용하여 데이터를 반환한다.

반환되는 값은 함수 이름에서 알 수 있듯이 사용 가능하면 true, 연결을 잃어버린 경우 false의 값을 가진 LiveData를 반환하게 된다.

 

onActive와 onInactive는 각각 해당 LiveData가 활성화, 비활성화될 때 호출되는 함수이다.

onActive에서는 NetworkRequest.Builder를 통해 네트워크 요청을 빌드하고, addCapability를 통해 연결이 가능한 네트워크를 요청하게 된다.

요청한 request와 처음 선언한 callback을 매개변수로 네트워크 콜백을 등록하여 변화를 감지하며,

postValue()를 최초 호출 시켜줌으로써 현재 네트워크 상태를 확인하여 LiveData를 업데이트시켜준다.

 

onInactive에서는 LiveData가 비활성화될 때 이므로, 등록했던 콜백을 해제하여 준다.

 

isNetworkAvailable 함수는 말 그대로 현재 네트워크가 연결되어 있으면 true, 아니면 false를 반환해 주는 함수이다.

 

간단하게 설명하긴 했지만, 해당 클래스의 네이밍과 흐름이 워낙 직관적이어서 천천히 읽어보면 전부 이해가 가능할 것이다.

 

필자는 해당 데이터를 viewModel에서 관리하기를 원했다.

따라서 이렇게 선언한 LiveData 또한 Koin Module에 등록해주고 

single { NetworkStatusLiveData(get()) }

 

viewModel에 해당 LiveData 클래스를 선언해서 데이터를 가져올 수 있도록 구현해 준다.

private val networkStatusLiveData = NetworkStatusLiveData(application)
fun getNetworkStatus(): LiveData<Boolean> = networkStatusLiveData

 

이렇게 하면 getNetworkStatus라는 함수를 통해 네트워크 연결 여부를 실시간으로 받아올 수 있게 된다.

 

실제 사용되는 Compose 함수에서는 다음과 같이 사용이 가능하다.

apiExampleViewModel.getNetworkStatus()
    .observe(LocalLifecycleOwner.current) { isConnected ->
        if (isConnected) {
            Log.d("NetworkLog", "isConnected")
        } else {
            Log.d("NetworkLog", "Network is lost")
        }
    }

 

viewModel의 getNetworkStatus 함수를 호출하는데, 해당 데이터는 LiveData이므로 observe를 통해 옵저빙 하여 실시간으로 변경되는 데이터를 받아오도록 한다.


이제는 위에 선언한 네트워크 연결을 확인하는 함수들을 사용하여 간단하게 예외처리를 해보도록 하자.

val viewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current)
val apiExampleViewModel =
    koinViewModel<ApiExampleViewModel>(owner = viewModelStoreOwner)
val posts by apiExampleViewModel.posts.observeAsState(initial = emptyList())

val networkUtil: NetworkUtil = get()
val isConnectNetwork = remember { mutableStateOf(networkUtil.isNetworkConnected()) }

apiExampleViewModel.getNetworkStatus()
    .observe(LocalLifecycleOwner.current) { isConnected ->
        if (isConnected) {
            isConnectNetwork.value = true
        } else {
            isConnectNetwork.value = false
        }
    }

LaunchedEffect(key1 = isConnectNetwork.value, block = {
    if (isConnectNetwork.value) {
        apiExampleViewModel.fetchPosts()
    }
})

 

간단하게, viewModel을 선언 후 보여줄 데이터 posts를 가져온다.

mutableStateOf 내부에 isNetworkConnected를 호출하여 초기 값 설정을 위해 NetworkUtil을 사용한다.

그 후, 네트워크 상태를 옵저빙 하며 네트워크 연결 상태에 따라 isConnectNetwork라는 mutableState <Boolean> 타입의 변수에 저장한다.

 

LaunchedEffect의 key 값으로 해당 데이터를 사용하여 네트워크가 변경되어 true로 설정될 때마다 api를 재 호출하여 최신 값으로 갱신할 수 있도록 side effect 함수를 구현해 주었다.

LazyColumn(
    modifier = Modifier
        .fillMaxSize()
        .background(color = Color.White)
) {
    stickyHeader {
        Box(
            modifier = Modifier
                .background(color = Color.White)
                .padding(top = 10.dp, bottom = 10.dp)
        ) {
            Column {
                ...

                if (!isConnectNetwork.value) {
                    Column(
                        modifier = Modifier
                            .fillMaxWidth()
                            .background(color = Color.Black),
                        horizontalAlignment = Alignment.CenterHorizontally,
                        verticalArrangement = Arrangement.Center
                    ) {
                        Text(
                            text = "The network is disconnected.\nPlease check the network status.",
                            color = Color.White,
                            textAlign = TextAlign.Center
                        )
                    }
                }
            }
        }
    }

    itemsIndexed(posts) { index, item ->
        Text(text = "[$index] : ${item.title}")
    }
}

 

그다음에는 UI를 보여주는 부분으로 아주 간단하게 네트워크 연결이 되어있지 않으면 stickyHeader에 Text Compose 함수를 하나 추가하여 보여주도록 간단하게 구현하였다.

 

이 예제의 실행되는 화면을 보면 다음과 같다.

 


이것으로 아주 간단하게 Network의 연결 여부를 확인하여 처리하는 방법에 대하여 알아보았다.

 

네트워크 처리에 대해서 디테일하게 진행한다고 하면 할 수 있는 것들이 정말 많겠지만,

정말 간단하게 네트워크가 끊기면 어떻게 처리해야 하는 거지?라는 의문이 있거나, 간단한 처리만 하면 되는 경우 해당 게시글 하나만으로 충분히 해결이 가능할 것이다.

 

조금 더 확장해서, 네트워크 연결 여부에 따라서 다른 UI를 보여준다던지, reconnect 혹은 네트워크 연결을 확인하는 system UI로 이동시킨다던지 등의 작업을 추가하면 조금 더 사용자 경험이 좋은 UI UX를 구현할 수 있을 것 같다.

 

해당 게시글에 사용한 예제는 Github에 올려두었다.

https://github.com/HeeGyeong/ComposeSample

 

GitHub - HeeGyeong/ComposeSample: This project provides various examples needed to actually use Jetpack Compose.

This project provides various examples needed to actually use Jetpack Compose. - HeeGyeong/ComposeSample

github.com

 

 

 

728x90