Android 네트워크 끊김 감지 - ConnectivityManager

Ryan·2026년 4월 30일

Android

목록 보기
6/7
post-thumbnail

이 글은 Now In Android 프로젝트의 ConnectivityManagerNetworkMonitor 구현을 기반으로, 네트워크 모니터링의 필요성부터 실제 구현까지를 다룹니다.

만약 앱에서 네트워크가 끊기게 된다면 어떻게 될까요? 기본적으로 서버와 통신을 주고 받는 앱이라면 사용자는 빈 화면을 보거나, 로딩이 끝나지 않거나, 알 수 없는 에러를 마주하게 됩니다.

그래서 네트워크 연결이 불안정한 경우에는 그 사실을 사용자에게 알려주는 UI가 필요합니다. 문제는, 그걸 어떤 방식으로 감지하고 보여줄 것이냐예요.

네트워크 끊김 처리 방식

1. 서버 요청 실패 시 에러 UI 띄우기

가장 직관적인 방법이에요. API 요청이 실패하면 에러 다이얼로그나 스낵바를 띄웁니다. 구현이 단순하고, 네트워크 문제뿐 아니라 서버 장애, 타임아웃 등 모든 통신 실패를 한 번에 처리할 수 있다는 장점이 있어요.

단순 조회성 API 위주의 앱이라면 이것만으로도 충분해요.

2. 네트워크 연결 상태를 실시간으로 감지하기

ConnectivityManager를 사용하면 OS 수준에서 네트워크 연결 상태를 실시간으로 감지할 수 있어요. 네트워크가 연결되었는지, 끊겼는지를 Boolean 값으로 받아 앱 전반의 동작을 제어할 수 있습니다.

그럼 언제 ConnectivityManager가 더 적절할까?

에러 다이얼로그 방식은 간단하지만, 다음과 같은 상황에서는 한계가 있어요.

실시간 통신이 필요한 경우: 채팅, 라이브 스트리밍처럼 연결이 지속되어야 하는 앱에서는 네트워크가 끊겼는데도 불필요한 요청을 계속 보낼 수 있어요. 연결 상태를 미리 파악하면 이런 낭비를 막을 수 있습니다.

오프라인 우선 아키텍처를 적용한 경우: 기본적으로 로컬 데이터를 기반으로 동작하되, 네트워크가 연결된 시점에만 서버 요청을 보내서 데이터를 동기화하는 구조예요. 연결 상태를 알 수 없으면 오프라인인데도 불필요한 요청을 시도하게 됩니다.

정리하면, ConnectivityManager는 네트워크 상태 변화에 따라 앱의 연결, 요청, UI 상태를 사전에 제어하기 위한 도구예요. 불필요한 네트워크 요청을 방지해 리소스 낭비와 배터리 소모를 줄이고, 오프라인 우선 아키텍처에서 적절한 시점에만 서버 요청을 보낼 수 있게 해줍니다.

다만, 만능은 아니에요

ConnectivityManager가 "네트워크에 연결되어 있다"고 알려줘도, 실제 서버 통신이 가능하다는 보장은 없어요. 서버 장애, DNS 문제, 인증 만료, 타임아웃 등 네트워크 연결과는 별개로 요청이 실패할 수 있는 이유는 얼마든지 있습니다.

ConnectivityManager는 서버 요청 에러 처리를 대체하는 게 아니라, 네트워크 상태에 따라 불필요한 요청을 줄이고 UI를 사전에 제어하기 위한 보조 수단이에요. 결국 두 방식은 역할이 다른 것이고, 앱의 요구사항에 따라 함께 사용하는 것이 가장 적절합니다.

ConnectivityManager

ConnectivityManager를 제대로 사용하려면 함께 동작하는 4개의 클래스를 이해해야 해요.

ConnectivityManager: 시스템의 연결 상태를 앱에 알리는 컨트롤 타워예요. 네트워크 콜백을 등록하고, 현재 네트워크 정보를 조회하는 진입점 역할을 합니다.

Network: 기기가 연결된 특정 네트워크를 나타내는 객체예요. Wi-Fi 하나, 모바일 데이터 하나가 각각 별도의 Network 객체가 됩니다. 네트워크가 끊기면 이 객체도 함께 소멸해요.

LinkProperties: DNS 서버, 로컬 IP 주소, 네트워크 경로 등 해당 네트워크의 상세 연결 정보를 담고 있어요. 다만 VPN 앱이나 네트워크 진단 도구 같은 특수한 경우가 아니라면 직접 다룰 일은 거의 없습니다.

NetworkCapabilities: 해당 네트워크가 "뭘 할 수 있는지"를 알려주는 객체예요. 크게 두 가지를 확인할 수 있습니다. 하나는 전송 방식(Wi-Fi인지, 셀룰러인지, 이더넷인지)이고, 다른 하나는 네트워크 속성(인터넷 접근이 가능한지, 실제로 검증되었는지, 데이터 무제한인지)이에요.

네트워크 상태를 읽는 두 가지 방식: 스냅샷 vs 콜백

순간적인 상태 가져오기 (Snapshot)

activeNetwork를 통해 현재 기본 네트워크 정보를 즉시 가져올 수 있어요. 디버깅이나 일시적인 확인에는 유용하지만, 호출 이후의 변화는 알 수 없다는 단점이 있어요. 네트워크 상태는 언제든 바뀔 수 있기 때문에, 스냅샷만으로는 실시간 대응이 불가능해요.

네트워크 이벤트 수신 대기 (Callback)

시스템에 콜백을 등록하여, 네트워크 상태가 변할 때마다 즉시 알림을 받는 방식이에요. 대부분의 앱에서 권장되는 방식이며, NetworkCallback을 구현하면 다음과 같은 이벤트를 받을 수 있습니다.

  • onAvailable() : 새로운 네트워크가 연결되어 사용 준비가 되었을 때
  • onCapabilitiesChanged() : 네트워크 속성이 변했을 때 (예: 인터넷 연결 검증 완료, 무제한 데이터로 변경)
  • onLinkPropertiesChanged() : DNS나 IP 주소 등 네트워크 설정이 변경되었을 때
  • onLost() : 네트워크 연결이 완전히 끊어졌을 때

NET_CAPABILITY_INTERNET vs NET_CAPABILITY_VALIDATED

NetworkCapabilities를 확인할 때 반드시 알아야 할 구분이 있어요.

NET_CAPABILITY_INTERNET: 네트워크가 인터넷에 연결되도록 설정되어 있음을 의미해요. 문이 열려 있는 상태와 같습니다. 실제로 밖에 나갈 수 있는지는 별개의 문제예요.

NET_CAPABILITY_VALIDATED: 실제로 공개 인터넷에 액세스할 수 있음이 검증된 상태예요. 문 밖으로 나갈 수 있는 상태입니다.

이 구분이 실제로 체감되는 대표적인 사례가 캡티브 포탈(Captive Portal), 즉 로그인이 필요한 공용 Wi-Fi예요. 카페나 공항 Wi-Fi에 연결하면 처음에는 INTERNET은 있지만 VALIDATED는 아닌 상태입니다. 브라우저에서 로그인을 완료해야 비로소 VALIDATED가 돼요.

따라서 실제 인터넷 사용 가능 여부를 판단하려면 NET_CAPABILITY_VALIDATED를 확인해야 합니다.

구현시 지켜야 할 주의사항

Android 공식문서에서는 아래사항을 주의하라고 이야기해요.

경합 상태(Race Condition)에 주의하세요

콜백 메서드 내부에서 getNetworkCapabilities() 같은 동기 메서드를 직접 호출하면 안 돼요. 네트워크 상태는 콜백이 호출된 시점과 동기 메서드가 실행되는 시점 사이에 바뀔 수 있기 때문입니다. 반환값이 최신 상태임을 보장할 수 없으므로, 반드시 콜백의 인자로 전달된 객체를 그대로 사용해야 해요.

콜백 리소스 누수를 방지하세요

네트워크 콜백은 앱 내부 객체만 사용하는 게 아니라, OS의 네트워크 서비스가 상태 추적과 이벤트 전달을 계속 유지해야 하는 시스템 리소스예요.

무제한으로 등록을 허용하면 다음과 같은 문제가 생깁니다.

메모리/핸들 누수: 콜백을 해제하지 않으면 앱과 시스템 양쪽에 참조가 남아요.

이벤트 폭증: 네트워크 변화마다 등록된 모든 콜백에 브로드캐스트해야 하므로, 콜백이 많을수록 비용이 커져요.

시스템 안정성 저하: 악성 앱이나 버그 있는 앱이 과도하게 등록하면 전체 디바이스 성능에 영향을 줍니다.

그래서 안드로이드는 앱(UID) 단위로 등록 가능한 네트워크 요청/콜백 수를 100개로 제한하고 있으며, 이를 초과하면 TooManyRequestsException이 발생해요. 더 이상 필요하지 않은 콜백은 반드시 unregisterNetworkCallback()으로 해제해야 합니다.

Now In Android의 구현에서도 아래와 같은 패턴을 쓴건 이 때문이에요.

callbackFlow { ... awaitClose { unregisterNetworkCallback(callback) } }

Flow 수집이 끝나면 자동으로 콜백이 해제되어 리소스 누수를 방지합니다.

WorkManager와의 역할을 구분하세요

"인터넷이 연결되면 데이터를 동기화해라" 같은 백그라운드 작업이 목적이라면, ConnectivityManager보다 WorkManager가 더 적절해요. WorkManager는 네트워크 제약 조건을 지정할 수 있고, 배터리 효율과 작업 보장 측면에서 훨씬 유리합니다.

ConnectivityManager는 UI 상태를 실시간으로 반영하는 데 집중하고, 백그라운드 동기화는 WorkManager에 맡기는 것이 역할 분담의 원칙이에요.

실제 구현(NetworkMonitorImpl 코드 분석)

이론은 여기까지고, 이제 실제로 어떻게 구현했는지 코드를 살펴볼게요. 핵심은 ConnectivityManager의 콜백을 Kotlin Flow로 브릿지하는 것입니다.

전체 코드

@Singleton
class NetworkMonitorImpl @Inject constructor(
    @param:ApplicationContext private val context: Context,
) : NetworkMonitor {

    override val isOnline: Flow<Boolean> = callbackFlow {
        val connectivityManager = context.getSystemService(ConnectivityManager::class.java)

        val callback = object : NetworkCallback() {
            private val networks = mutableSetOf<Network>()

            override fun onAvailable(network: Network) {
                networks += network
                trySend(true)
            }

            override fun onLost(network: Network) {
                networks -= network
                trySend(networks.isNotEmpty())
            }
        }

        val request = NetworkRequest.Builder()
            .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
            .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
            .build()

        trySend(connectivityManager.isCurrentlyConnected())
        connectivityManager.registerNetworkCallback(request, callback)

        awaitClose {
            connectivityManager.unregisterNetworkCallback(callback)
        }
    }
        .distinctUntilChanged()
        .conflate()

    private fun ConnectivityManager.isCurrentlyConnected(): Boolean {
        val networkCapabilities = getNetworkCapabilities(activeNetwork) ?: return false
        return networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
                networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
    }
}

왜 callbackFlow인가?

ConnectivityManager의 네트워크 콜백은 전형적인 콜백 기반 API예요. onAvailable(), onLost() 같은 메서드가 시스템에 의해 호출되는 구조입니다. 그런데 우리가 원하는 건 이 이벤트를 Compose UI나 ViewModel에서 Flow로 수집하는 거예요.

callbackFlow는 바로 이 간극을 메워주는 코루틴 빌더예요. 콜백 기반 API를 Flow로 변환할 때 쓰는 정석 패턴입니다. 내부에서 trySend()를 통해 콜백 이벤트를 Flow의 값으로 전달할 수 있어요.

override val isOnline: Flow<Boolean> = callbackFlow {
    // 여기서 콜백을 등록하고, trySend()로 값을 방출
    awaitClose {
        // Flow 수집이 끝나면 여기서 정리(cleanup)
    }
}

그리고 awaitClose 블록이 중요해요. Flow 수집이 끝나는 시점에 unregisterNetworkCallback()을 호출해서 콜백을 해제합니다. 앞서 이야기한 콜백 100개 제한과 리소스 누수 문제를 이 한 줄로 해결하는 거예요.

Network 집합으로 온라인 여부 판단하기

아래는 now in android 구현 방식을 많이 참고했는데요.

val callback = object : NetworkCallback() {
    private val networks = mutableSetOf<Network>()

    override fun onAvailable(network: Network) {
        networks += network
        trySend(true)
    }

    override fun onLost(network: Network) {
        networks -= network
        trySend(networks.isNotEmpty())
    }
}

이 부분이 구현의 핵심이에요. 왜 단순히 onAvailable이면 true, onLostfalse로 보내지 않았을까요?

안드로이드 기기는 여러 네트워크에 동시에 연결될 수 있기 때문이에요. 예를 들어 Wi-Fi와 모바일 데이터가 동시에 활성화된 상태에서 Wi-Fi가 끊기면 onLost가 호출됩니다. 이때 무조건 false를 보내면 실제로는 모바일 데이터로 인터넷이 가능한데도 오프라인으로 판단해 버려요.

그래서 mutableSetOf<Network>()로 현재 사용 가능한 네트워크를 추적하고, onLost에서는 해당 네트워크를 제거한 뒤 집합이 비어 있는지로 최종 온라인 여부를 결정합니다. 하나라도 남아 있으면 여전히 온라인이에요.

초기 상태 처리: isCurrentlyConnected()

trySend(connectivityManager.isCurrentlyConnected())
connectivityManager.registerNetworkCallback(request, callback)

콜백을 등록하기 전에 현재 연결 상태를 먼저 방출하고 있어요. 왜 이 순서가 중요할까요?

registerNetworkCallback()은 등록 이후 발생하는 변화만 알려줘요. 이미 연결되어 있는 상태에서 Flow 수집을 시작하면, 변화가 없으니 콜백이 호출되지 않습니다. 그러면 수집자는 아무 값도 받지 못한 채 대기하게 돼요.

private fun ConnectivityManager.isCurrentlyConnected(): Boolean {
    val networkCapabilities = getNetworkCapabilities(activeNetwork) ?: return false
    return networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
            networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
}

그래서 콜백 등록 전에 isCurrentlyConnected()로 현재 스냅샷을 먼저 보내는 거예요. 수집자는 즉시 현재 상태를 받고, 이후 변화는 콜백을 통해 받게 됩니다.

distinctUntilChanged()와 conflate()

.distinctUntilChanged()
.conflate()

Flow 체인 끝에 붙은 이 두 연산자는 각각 역할이 달라요.

distinctUntilChanged() : 연속으로 같은 값이 방출되면 무시합니다. 예를 들어 Wi-Fi가 연결된 상태에서 모바일 데이터까지 연결되면 onAvailable이 두 번 호출되고, trySend(true)도 두 번 실행돼요. 하지만 이미 true인 상태에서 또 true를 받을 필요는 없으니, 중복을 걸러냅니다.

conflate() : 수집자가 이전 값을 아직 처리하지 못했을 때 새 값이 들어오면, 중간 값을 건너뛰고 최신 값만 전달해요. 네트워크 상태가 빠르게 변할 때(예: 터널을 지나면서 연결/해제가 반복될 때) 수집자가 모든 중간 상태를 하나씩 처리할 필요가 없습니다. 우리가 필요한 건 지금 현재 온라인인지 아닌지, 그 최신 상태뿐이에요.

이 두 연산자의 조합으로 수집자는 의미 있는 변화가 있을 때만, 항상 최신 값을 받게 됩니다.

NetworkRequest: 어떤 네트워크를 감시할 것인가

val request = NetworkRequest.Builder()
    .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
    .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
    .build()

registerNetworkCallback에 전달하는 NetworkRequest는 "어떤 조건의 네트워크만 수집할지”를 정하는 필터 역할을 해요. 여기서는 NET_CAPABILITY_INTERNET으로 인터넷 연결 설정이 있는 네트워크만, 그리고 NET_CAPABILITY_VALIDATED로 실제 인터넷 접속이 검증된 네트워크만 감시해요.

인터넷과 무관한 네트워크와, 카페 Wi-Fi처럼 연결은 됐지만 로그인 전이라 실제로 인터넷이 안 되는 상태도 걸러냅니다.

구현 요약

  1. callbackFlow로 콜백 기반 API를 Flow로 변환해요
  2. 콜백 등록 전에 isCurrentlyConnected()로 초기 상태를 먼저 방출해요
  3. mutableSetOf<Network>()로 활성 네트워크를 추적해서, 하나라도 남아 있으면 온라인으로 판단해요
  4. awaitClose에서 콜백을 해제해 리소스 누수를 방지해요
  5. distinctUntilChanged()로 중복 값을 제거하고, conflate()로 항상 최신 값만 전달해요

실제 구현 영상

마무리

단순 조회성 API 위주의 앱이라면, 요청 실패 시 에러 UI를 보여주는 것만으로 충분할 수 있어요. 하지만 실시간 통신이나 오프라인 우선 구조처럼 네트워크 상태가 앱의 동작 흐름에 직접적인 영향을 주는 경우에는 ConnectivityManager를 함께 사용하는 편이 더 적절합니다.

ConnectivityManager는 모든 네트워크 에러를 해결하기 위한 도구가 아니에요. 네트워크 상태 변화에 따라 앱의 동작을 더 능동적으로 제어하기 위한 도구입니다. 서버 에러 처리까지 함께 갖춰야, 네트워크에 대한 대응이 완성돼요.

참고

profile
Seungjun Gong

0개의 댓글