연결 손실 — 모바일 앱에서 가장 흔하고 짜증나는 현상 중 하나입니다. 사용자는 데이터에 대한 액세스를 잃고, 작업이 중단되며, 앱이 멈추거나 충돌합니다. Google Android Developer Blog에 따르면, 사용자의 70%가 앱이 두 번 충돌하거나 멈추면 앱을 삭제합니다. 연결 손실의 원인과 내결함성 통신을 구축하는 방법을 알아보겠습니다.
핵심 포인트
연결 끊김 — 앱이 서버와의 연결을 잃거나, 작업에 응답하지 않거나, 오류로 종료되는 상황을 설명하는 사용자 용어입니다. 기술적으로는 네트워크 오류(타임아웃, DNS 실패), ANR(UI 스레드 멈춤), 충돌(처리되지 않은 예외) 또는 경쟁 조건(race condition)일 수 있습니다.
사용자의 관점에서 이러한 모든 시나리오는 동일하게 보입니다: 앱이 작동을 멈춥니다. 개발자에게 차이점은 진단 및 수정 접근 방식에 있습니다. 네트워크 오류는 재시도 메커니즘으로 해결되고, ANR은 UI 스레드에서 작업을 분리하여, 충돌은 예외 처리로 해결됩니다.
Crittercism(현재 Apteligent)에 따르면, 모바일 앱은 충돌할 때마다 평균 1~2%의 사용자를 잃습니다. 100만 사용자 앱의 경우, 단일 버그당 1만~2만 건의 설치 손실을 의미합니다. 이는 금융 및 의료 분야 앱에서 특히 중요합니다.
불안정한 네트워크 — 모바일 기기는 지속적으로 Wi-Fi와 모바일 네트워크 간을 전환하며, 커버리지가 없는 영역(지하철, 엘리베이터, 지하실)에 진입합니다. 각 전환은 일시적인 연결 손실을 일으키며 앱이 올바르게 처리해야 합니다.
타임아웃 — 서버가 설정된 타임아웃(보통 10~30초) 내에 응답하지 않으면 클라이언트가 SocketTimeoutException을 던집니다. 피드백 없는 긴 타임아웃은 사용자에게 멈춤으로 인식됩니다. 타임아웃은 15초를 초과하지 않도록 설정하는 것이 좋습니다.
val client = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.build()
경쟁 조건(race condition) — 여러 스레드가 동기화 없이 동시에 동일한 데이터를 읽고 쓸 때 발생합니다. 예를 들어, 네트워크에서 캐시를 업데이트하는 것과 동시에 UI 스레드에서 캐시에서 데이터를 로드하면 오래되었거나 잘못된 데이터가 표시될 수 있습니다.
Offline-first — 로컬 스토리지(Room, CoreData)를 유일한 신뢰할 수 있는 정보 소스로 사용하는 아키텍처 패턴입니다. 네트워크는 백그라운드 데이터 동기화에 사용됩니다. 사용자는 네트워크 연결 없이도 로컬 캐시에서 항상 최신 데이터를 볼 수 있습니다.
Repository 패턴 — 네트워크에서 데이터를 가져올지 캐시에서 가져올지 결정하는 데이터의 단일 진입점입니다. Repository는 ViewModel 및 UI에서 데이터 소스를 추상화합니다. 네트워크 오류 시 Repository는 자동으로 로컬 소스로 전환합니다.
class UserRepository(
private val api: UserApi,
private val dao: UserDao
) {
suspend fun getUsers(): Result<List<User>> {
return try {
val remote = api.fetchUsers()
dao.insertAll(remote)
Result.success(remote)
} catch (e: IOException) {
val cached = dao.getAll()
if (cached.isNotEmpty()) {
Result.success(cached) // return cache on network error
} else {
Result.failure(e)
}
}
}
}
서킷 브레이커 — 서버를 사용할 수 없을 때 요청 폭주로부터 보호하는 패턴입니다. N번 연속 오류 후 서킷 브레이커가 열리고 모든 요청은 연결을 시도하지 않고 즉시 오류를 반환합니다. 지정된 타임아웃 후 서킷 브레이커는 테스트 요청을 위해 반열림 상태로 전환됩니다.
지수 백오프(Exponential backoff) — 표준 재시도 메커니즘입니다. 첫 번째 실패 후 1초, 두 번째 후 2초, 그 다음 4, 8, 16초를 기다립니다. 서버와 배터리에 과부하를 주지 않도록 최대 재시도 횟수(보통 3~5회)를 제한하세요.
사용자 피드백 — 네트워크 오류 시 명확한 메시지를 표시합니다: “연결 없음”, “서버를 일시적으로 사용할 수 없음”, “인터넷 연결을 확인하세요”. Snackbar 또는 Inline State View를 사용하세요. 절대 기술적 오류(HTTP 500, SocketException)를 사용자에게 표시하지 마세요.
ConnectivityManager — 네트워크 모니터링을 위한 Android API입니다. 앱이 변경 사항에 반응하도록 합니다: 연결 손실 시 플레이스홀더를 표시하고, 복원 시 자동으로 데이터를 업데이트합니다. iOS에서는 Network 프레임워크의 NWPathMonitor를 사용하세요.
class NetworkMonitor(private val context: Context) {
private val manager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
fun isOnline(): Boolean {
val network = manager.activeNetwork ?: return false
val caps = manager.getNetworkCapabilities(network) ?: return false
return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
}
}
Crashlytics(Firebase) — 모바일 앱을 위한 표준 충돌 보고 도구입니다. 모든 처리되지 않은 예외의 스택 추적, OS 버전, 기기 모델 및 충돌 시간을 수집합니다. 오류를 그룹화하고 수정 책임자를 지정할 수 있습니다.
Sentry — 성능 모니터링을 지원하는 Crashlytics의 대안입니다. 특정 트랜잭션(예: “사용자 인증”)을 추적하고 어떤 단계에서 오류가 발생했는지 확인할 수 있습니다. 성능 추적은 네트워크 타임아웃과 앱 로직 버그를 구별하는 데 도움이 됩니다.
Timber — 클래스별로 자동으로 태그를 추가하는 Android용 로깅 라이브러리입니다. 디버그 빌드에서는 모든 네트워크 요청과 응답을 기록합니다. 릴리스 빌드에서는 Crashlytics.setCustomLog를 통해 오류와 경고만 기록합니다.
| 도구 | 유형 | 사용 시기 |
|---|---|---|
| Crashlytics | 충돌 보고 | 항상 릴리스에서 — 자동 충돌 수집 |
| Sentry | 충돌 + 성능 | 특정 사용자 시나리오를 프로파일링해야 할 때 |
| Timber | 로깅 | 디버그: 전체 로깅; 릴리스: 오류만 |
| HTTP Toolkit | 네트워크 디버그 | 로컬 HTTP 트래픽 가로채기 및 분석 |
Firebase Summit 2023에 따르면, Crashlytics + Performance Monitoring을 구현한 앱은 중요한 버그를 감지하고 수정하는 평균 시간을 3일에서 4시간으로 단축했습니다. 활성 사용자의 0.1%를 초과하는 빈도의 충돌마다 알림을 설정하는 것이 좋습니다.
자주 묻는 질문
충돌이 Crashlytics에서 포착되지 않으면 네이티브 충돌(SIGSEGV, SIGABRT)을 확인하세요 — Java/Kotlin 예외 핸들러에서 처리되지 않습니다. Android에서는 JNI의 네이티브 메모리 누수일 수 있고, iOS에서는 EXC_BAD_ACCESS일 수 있습니다. 네이티브 충돌 스택 추적을 수집하려면 Breakpad(Android) 또는 PLCrashReporter(iOS)를 사용하세요.
Network Link Conditioner(iOS에 내장, Android에서는 Facebook Network Connection Class 또는 Developer Options > Network > Select network type 사용)를 사용하세요. 지연 시간 500~3000ms, 패킷 손실 5~30%로 설정합니다. Charles Proxy 또는 mitmproxy를 사용하여 네트워크 지연 및 연결 끊김을 시뮬레이션할 수도 있습니다.
ANR은 UI 스레드가 5초 이상 차단될 때 발생합니다. 네트워크 요청은 백그라운드 스레드에서 실행해야 합니다: 코루틴(viewModelScope.launch(Dispatchers.IO)), RxJava(subscribeOn(Schedulers.io)), 또는 동기화를 위한 WorkManager를 사용하세요. HTTP 클라이언트에 항상 타임아웃을 설정하세요 — 타임아웃이 없으면 영구 차단이 발생할 수 있습니다.
경쟁 조건 — 작업 결과가 스레드 실행 순서에 따라 달라지는 상황입니다. 예를 들어, 사용자가 “보내기” 버튼을 빠르게 두 번 누르면 요청이 두 번 전송됩니다. 해결책: Mutex, 단일 스레드 실행기 또는 상태 머신(첫 번째 클릭 후 버튼 비활성화)을 사용하세요. Kotlin에서는 코루틴의 Mutex 또는 @Synchronized 어노테이션을 사용하세요.
모바일 앱에 카오스 엔지니어링을 적용하세요: 작업 중 네트워크를 끊고, 높은 지연 시간을 시뮬레이션하고, Wi-Fi와 모바일 네트워크 간 전환, 시스템을 통해 프로세스 종료. 도구: Facebook Network Connection Class, Charles Proxy, iOS Network Link Conditioner. CI/CD에서 AndroidTest Orchestrator를 통해 다양한 네트워크 조건에서 UI 테스트를 추가하세요.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.