안드로이드 파일·APK 다운로드가 멈출 때 점검 순서

Android에서 “다운로드가 멈췄다”는 말은 서로 다른 세 단계를 가리킬 수 있습니다. 브라우저가 파일을 끝까지 받지 못한 것, APK 파일은 다 받았지만 설치 단계에서 거절된 것, Play 스토어가 앱 설치를 대기 중인 것은 원인과 확인 위치가 다릅니다. 우선 알림에 보이는 문구보다 멈춘 바이트 수와 최종 HTTP 응답을 남기면 문제 층을 빠르게 나눌 수 있습니다.

이 글에서는 실제 Android 기기 화면을 재현했다고 주장하지 않습니다. 대신 2026년 9월 21일 이 컴퓨터에 격리 HTTP 서버를 만들고 정상 재개, Range 무시, redirect, 응답 중간 절단을 같은 1MiB 파일로 실행했습니다. 그 결과를 바탕으로 사용자와 앱·서버 개발자가 각각 어디까지 확인할지 정리합니다.

먼저 멈춘 단계를 네 가지로 나누기

관찰한 증상 지금 확인할 층 남겨야 할 증거
누른 직후 0B이거나 로그인 화면으로 돌아감 링크 만료, 인증 쿠키, redirect, 접근 권한 최종 URL과 HTTP 상태
항상 비슷한 용량에서 멈추거나 처음부터 다시 받음 연결 중단, 저장 공간, Range 재개 지원 수신 바이트, 200/206, Content-Range
파일 크기는 채워졌지만 열기·설치 실패 파일 손상, APK 서명·호환성, 설치 출처 정책 예상 크기, SHA-256, 설치 오류 문구
Play 스토어에서 ‘대기 중’만 표시 Google Play 앱·계정·네트워크의 별도 흐름 다른 앱도 같은지, 스토어 오류 문구

예를 들어 APK가 100%까지 받아진 뒤 “앱이 설치되지 않았습니다”가 뜬다면 다운로드 재개 설정부터 바꿀 이유가 없습니다. 반대로 파일 크기가 계속 달라진다면 서명 문제를 조사하기 전에 전송 완료 여부부터 확인합니다.

격리 서버에서 실패 네 가지를 직접 재현했다

테스트는 Windows 10.0.19045 x64, Node.js v24.19.0에서 수행했습니다. 외부 서버나 이동통신망 변수를 제거하기 위해 127.0.0.1에만 HTTP 서버를 열었습니다. 1,048,576 byte의 결정적 바이트 배열을 만들고 원본 SHA-256 042d84d3772b8cc9080a8f388c428c11d88f8566e5a38fda322fc8ea33492cfc를 기준으로 완성 파일을 비교했습니다.

테스트 경로 서버 동작 클라이언트 판정
/file Range를 해석해 206과 Content-Range 반환 부분 두 개를 결합한 SHA-256 일치
/ignore-range Range가 있어도 전체 파일을 200으로 반환 기존 조각에 추가하면 길이·SHA-256 불일치
/redirect 302 뒤 실제 파일 경로로 이동 최종 URL·200·SHA-256 확인
/truncated 1MiB를 선언하고 256KiB만 보낸 뒤 연결 종료 응답 중단 오류 감지
HTTP 206 재개 성공과 Range 요청에 200 응답을 이어 붙였을 때 파일 손상을 비교한 로컬 다운로드 테스트
2026년 9월 21일 Node.js v24.19.0과 127.0.0.1 격리 서버에서 1MiB 파일의 정상 재개, Range 무시, redirect, 중간 절단을 실행한 결과입니다. Android 기기 화면이 아니라 HTTP 전송 계층 테스트입니다.

관찰 결과: 206은 결합됐고, 200을 추가하면 손상됐다

첫 요청은 앞의 262,144 byte만 받고, 두 번째 요청은 나머지를 요청했습니다. 서버가 두 요청 모두 206으로 답하면서 범위를 명시했을 때 결합 파일은 정확히 1,048,576 byte였고 원본 SHA-256과 일치했습니다.

GET /file  Range: bytes=0-262143
→ 206  Content-Range: bytes 0-262143/1048576

GET /file  Range: bytes=262144-
→ 206  Content-Range: bytes 262144-1048575/1048576

결합 크기: 1048576 byte
SHA-256 일치: true

다음에는 같은 이어받기 요청을 Range를 무시하는 경로에 보냈습니다. 응답은 206이 아니라 200이었고 Content-Range도 없었습니다. 이를 기존 262,144 byte 뒤에 그대로 붙이면 1,310,720 byte가 되어 체크섬이 달라졌습니다. 기존 조각을 버리고 200 응답 전체로 교체했을 때만 원본과 일치했습니다.

GET /ignore-range  Range: bytes=262144-
→ 200  Content-Range: 없음

잘못 이어 붙인 크기: 1310720 byte
잘못 이어 붙인 SHA-256 일치: false
부분 파일 폐기 후 전체 저장: 1048576 byte
다시 저장한 SHA-256 일치: true

1MiB 선언 후 256KiB만 전송
→ socket hang up 감지: true

따라서 “서버가 Accept-Ranges: bytes를 한 번 보냈다”는 정보만으로 이어 붙이면 안 됩니다. RFC 9110도 서버가 Range를 무시할 수 있다고 설명합니다. 클라이언트는 매번 206 상태와 유효한 Content-Range가 요청한 시작점과 전체 길이에 맞는지 검사해야 합니다. Range 요청에 200이 오면 그 응답은 전체 표현이므로 부분 파일에 추가하지 않고 처음부터 저장합니다.

사용자라면 파일 이름보다 바이트 수부터 기록하기

  1. 브라우저 다운로드 목록에서 파일 이름, 받은 용량, 총용량, 표시된 오류 문구를 적습니다.
  2. 같은 링크를 계속 누르기 전에 로그인 세션이 필요한 주소인지 확인합니다. 공유 링크나 서명 URL은 만료되면 HTML 로그인 페이지를 APK 이름으로 저장하기도 합니다.
  3. 다른 네트워크에서 한 번 비교하되 VPN·광고 차단·보안 앱을 무작정 모두 끄지는 않습니다. 어느 조건을 바꿨는지 한 번에 하나씩 남깁니다.
  4. 저장 공간에는 표시된 파일 크기보다 여유가 있어야 합니다. 다운로드 뒤 설치·압축 해제 과정은 추가 공간을 쓸 수 있습니다.
  5. 제공자가 SHA-256을 공개했다면 받은 파일의 값과 비교합니다. 크기만 같아도 내용이 같다는 보장은 없습니다.

브라우저 메뉴 이름과 저장 위치는 제조사·Android 버전에 따라 다릅니다. 이 글에서 특정 기기의 메뉴를 실제로 눌러 검증하지 않았으므로 “설정의 몇 번째 항목”보다 다운로드 목록에서 관찰할 값을 기준으로 제시합니다.

서버 운영자는 최종 GET과 Range 응답을 따로 확인한다

HEAD만으로 판단하면 실제 GET·CDN 경로와 다를 수 있습니다. 테스트 파일이나 권한이 허용된 자신의 URL에서 최종 redirect를 따라간 GET 응답과 Range GET을 각각 저장합니다. 아래 명령의 example.com은 실제 관리 URL로 바꿉니다.

# 전체 응답: redirect를 따라가며 헤더와 파일을 각각 저장
curl -L -D full-headers.txt -o downloaded.apk 
  https://example.com/app.apk

# 262144 byte부터 재개 요청
curl -L -H "Range: bytes=262144-" 
  -D range-headers.txt -o resumed-part.bin 
  https://example.com/app.apk

Range 응답에서 볼 것은 206 여부, Content-Range: bytes 262144-.../전체길이, 실제 본문 길이입니다. CDN이나 reverse proxy가 Range를 제거하거나, 인증 만료로 redirect된 HTML이 200으로 돌아오는 경우도 있으므로 최종 URL과 Content-Type도 함께 남깁니다. ETag나 Last-Modified가 다운로드 도중 바뀌었다면 이전 조각과 새 표현을 합치지 말고 새로 받는 편이 안전합니다.

Range GET 결과 의미 안전한 처리
206 + 요청 시작점과 맞는 Content-Range 부분 표현을 받음 길이·표현 식별자 확인 후 결합
200 + 전체 파일 서버가 Range를 무시함 기존 부분 파일 폐기 후 교체
416 + bytes */전체길이 요청 범위가 현재 표현에 맞지 않음 로컬 길이와 원격 길이를 다시 비교
302/303 뒤 HTML 200 인증·동의·만료 페이지일 수 있음 최종 URL·Content-Type·세션 확인
Content-Length보다 일찍 종료 전송이 완성되지 않음 완료로 표시하지 말고 재시도·로그 수집

앱에서 DownloadManager를 쓴다면 status와 reason을 함께 저장하기

Android DownloadManager는 현재 상태를 COLUMN_STATUS로, 대기·실패 이유를 COLUMN_REASON으로 조회할 수 있습니다. “멈춤”이라는 자체 문구만 보여주지 말고 시스템이 반환한 두 값을 진단 로그에 함께 남깁니다. 공식 API에는 대기, 실행 중, 일시중지, 성공, 실패 상태와 네트워크 대기·재시도 대기 같은 reason이 구분되어 있습니다.

val query = DownloadManager.Query().setFilterById(downloadId)
downloadManager.query(query).use { cursor ->
    if (cursor.moveToFirst()) {
        val status = cursor.getInt(
            cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS)
        )
        val reason = cursor.getInt(
            cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_REASON)
        )
        val received = cursor.getLong(
            cursor.getColumnIndexOrThrow(
                DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR
            )
        )
        val total = cursor.getLong(
            cursor.getColumnIndexOrThrow(
                DownloadManager.COLUMN_TOTAL_SIZE_BYTES
            )
        )
        // 운영 로그에는 URL 토큰을 넣지 말고 status/reason/byte 수만 기록한다.
    }
}

앱이 요청을 만들 때 metered network, roaming, Wi-Fi 제한을 걸었다면 사용자가 보는 “대기”가 코드의 의도일 수 있습니다. DownloadManager.Request의 네트워크 제한과 실제 제품 요구가 맞는지 확인하고, 제한 때문에 기다리는 경우 UI에 그 이유를 설명합니다. 토큰·쿠키·서명 URL 전체는 로그에 남기지 않습니다.

APK를 다 받은 뒤 설치가 안 되면 다운로드 문제에서 빠져나오기

파일 크기와 체크섬이 맞고 다운로드가 끝났다면 다음 단계는 설치 진단입니다. 출처를 알 수 없는 APK를 받거나 Play Protect를 끄는 것을 해결책으로 삼지 않습니다. 배포자가 제공한 공식 다운로드 위치와 체크섬인지 먼저 확인하고, 기기가 표시하는 차단·서명·호환성 문구를 그대로 기록합니다.

  • 기존 앱 업데이트라면 새 APK가 기존 앱과 같은 signing key로 서명됐는지 확인합니다.
  • 지원 Android 버전, ABI, split APK 구성과 기기 조건이 맞는지 확인합니다.
  • 네이티브 라이브러리가 있는 앱이 특정 최신 기기에서만 실패한다면 Android 16KB 페이지와 APK·ELF 정렬 점검으로 분리합니다.
  • 화면의 설치 오류별 분기는 Android·iPhone 설치 오류 진단표에서 이어서 확인합니다.

Google Play Protect는 설치된 앱과 기기에서 유해 동작을 검사하며, Google은 이를 켜 둔 상태를 권장합니다. 국가·기기·계정 정책에 따라 출처 외 설치 흐름이 달라질 수 있으므로 보안 경고를 우회하기보다 배포 경로와 개발자 확인 상태를 점검합니다.

Play 스토어 ‘대기 중’은 직접 파일 다운로드와 분리한다

Play 스토어에서만 대기 중이고 브라우저 파일은 정상이라면 HTTP Range 테스트를 먼저 할 이유가 없습니다. 다른 앱도 같은지, 네트워크와 저장 공간, Google Play 앱·Play 서비스 상태, 계정의 다운로드 조건을 확인합니다. 상세 순서는 Play 스토어 다운로드 대기 중 점검에 따로 정리했습니다.

APK 링크가 앱으로 열려야 하는데 브라우저로 반복해서 돌아오거나 redirect 과정에서 인증이 풀린다면 전송 자체보다 링크 연결 문제일 수 있습니다. 이 경우 Android App Links 검증 순서에서 도메인 연결과 fallback을 확인합니다.

이번 재현으로 확인하지 못한 범위

  • Android 실기기와 제조사별 브라우저, 시스템 DownloadManager UI를 실행하지 않았습니다.
  • 실제 CDN, proxy, 로그인 쿠키, 이동통신망의 끊김을 재현하지 않았습니다.
  • APK 서명 검증과 설치 성공 여부, Play 스토어 계정 상태를 측정하지 않았습니다.
  • 로컬 HTTP라 TLS 인증서·HTTPS redirect 문제는 포함하지 않았습니다.

그러므로 이 글의 수치를 특정 Android 기기의 결과로 일반화하지 않습니다. 대신 응답 상태, 범위, 바이트 수, 체크섬이라는 관찰값으로 전송 완료 여부를 판정하고, 그 뒤에만 설치·스토어 단계로 이동하는 절차를 제공합니다.

판정에 사용한 1차 자료