Android AssetManager ‘FileNotFoundException: file is probably compressed’ — 압축된 asset을 openFd()로 열 때 해결
검증 환경
본 절은 원문 사례·문서에 등장한 버전/도구를 정리한 것이다. 별도 실험실 재현이 명시되지 않은 항목은 일반화하지 않는다.
- 본문에 명시된 오류 메시지·프레임워크 버전을 기준으로 원인을 추적했다.
- 문서 정리일: 2026-08-30
- OS/CI 세부 값은 프로젝트마다 다르므로, 적용 전 로컬에서 동일 오류 메시지를 재확인한다.
1. 문제 정의
앱에서 assets/ 폴더 안에 넣어 둔 사운드나 리소스 파일을 재생하거나 읽으려 하다가, 일부 파일만 실패하고 일부는 정상 동작하는 경험을 한 적이 있는가? 이 문제는 전형적으로 아래와 같은 스택 트레이스와 함께 나타난다.
| |
핵심 문구는 짧지만 결정적이다.
This file can not be opened as a file descriptor; it is probably compressed
2. 표면 증상
겉으로 보이는 증상은 두 가지다.
AssetManager.openFd()호출 시java.io.FileNotFoundException이 던져진다.- 같은
assets/폴더인데도 일부 파일은 잘 열리고, 일부 파일만 실패한다. (사운드보드처럼 확장자가 다양한 파일을 함께 넣은 경우 특히 두드러진다.)
이 “일부만 실패” 패턴이 첫 번째 단서다. 파일이 누락된 문제도 아니고, 권한 문제도 아니며, 파일 자체가 손상된 것도 아니다. 파일의 성격(압축 여부)에 따라 실패 여부가 갈린다.
증상 fingerprint
| 항목 | 값 |
|---|---|
| 에러 메시지 | This file can not be opened as a file descriptor; it is probably compressed |
| 발생 지점 | AssetManager.openAssetFd(Native Method) → AssetManager.openFd() |
| 발생 단계 | 런타임 (assets에서 파일을 FD/메모리 매핑으로 열 때) |
| 관련 도구 | Android AssetManager, aapt, Gradle (android.aaptOptions) |
| 흔한 오해 | “파일이 없거나 잘못 배치됐다"고 생각해 경로/권한부터 뒤지는 것 |
| 빠른 판단 기준 | 같은 폴더에서 확장자만 다른데 선별적으로 실패한다면 압축 여부가 원인일 가능성이 높다 |
3. 원인 추적
이 에러를 이해하려면 프로세스 안에서 실제로 무슨 일이 일어나는지 따라가 봐야 한다.
openFd()는 무엇을 반환하나
AssetManager.openFd()는 실제 파일 디스크립터(AssetFileDescriptor)를 반환한다. 이 디스크립터는 리소스를 **프로세스의 가상 주소 공간에 직접 메모리 매핑(memory-map)**해서 사용하기 위한 것이다.
메모리 매핑이 가능하려면 디스크에 있는 원본 바이트가 곧 렌더링/사용할 바이트여야 한다. 그런데 만약 파일이 압축되어 있다면, 디스크에는 압축된 바이트가 있고, 실제 사용할 때는 풀어야(decompress) 한다. 디스크 원본 바이트를 그대로 매핑해 쓸 수가 없다.
즉, “파일 디스크립터로 열 수 없는(= 메모리 매핑할 수 없는) 파일"이 바로 압축된 파일이다. 그래서 오류 자체가 it is probably compressed라고 정확히 알려주는 것이다.
왜 압축되는가 — aapt의 기본 동작
APK 빌드 과정에서 aapt(AAPT2)는 assets/의 파일을 처리할 때 **일부 확장자를 제외하고 기본적으로 파일을 압축(compress)**한다. 이미 압축된 포맷(예: .png, .jpg, .mp3 같은 미디어 확장자)은 다시 압축해도 이득이 없으므로 압축 제외 목록(noCompress)에 기본 포함되어 있다.
반면, 텍스트나 덜 알려진 확장자(e.g. .json, .txt, 사용자 정의 확장자 등)는 aapt가 기본적으로 압축한다. 이렇게 압축된 파일은 openFd()로 열 수 없고, AssetManager.open()을 통해 전체 바이트를 읽어 풀어서 써야 한다.
4. 근본 원인
표면 증상과 근본 원인을 정리하면 다음과 같다.
| 구분 | 내용 |
|---|---|
| 표면 증상 | openFd() 호출 시 FileNotFoundException 발생, 일부 파일만 실패 |
| 실질 원인 | 해당 파일이 aapt에 의해 압축되어 있어 메모리 매핑이 불가능 |
| 실제 메커니즘 | openFd()는 압축되지 않은(disk 바이트 그대로) 파일만 네이티브 AssetManager.openAssetFd로 매핑할 수 있다. 압축된 파일은 open()으로 스트림 전체를 읽어야 한다 |
| 핵심 판단 | 파일이 “없어서” 실패하는 것이 아니라, aapt가 압축했기 때문에 파일 디스크립터로 열 수 없는 것이 핵심이다 |
즉 이 문제의 핵심은 “파일이 없다는 것"이 아니다. 패키징 단계(aapt)에서 파일을 압축했는지가 관건이다.
5. 코드 해결책
원인이 명확하므로 해결책은 “파일을 압축하지 않게 하기” 한 가지로 귀착된다. 상황에 따라 아래 중 하나를 선택한다.
해결책 A — 압축되지 않는 확장자를 사용한다 (임시)
aapt가 압축하지 않는 확장자(예: .mp3)로 파일을 쓰거나 파일명을 바꾸면 동작한다. 사운드보드에서 일부 사운드만 동작한 이유가 여기에 있다 — 압축되지 않은 확장자(실제로 압축이 안 된)는 열렸고, aapt가 압축해 버린 확장자는 실패했기 때문이다.
| |
다만 확장자 트릭은 파일 포맷이 실제로 그 확장자와 일치해야 의미가 있다. 포맷이 아니라 단순히 확장자만 바꾸는 것은 버그를 유발할 수 있으므로 주의한다.
해결책 B — aaptOptions.noCompress로 확장자를 압축 제외에 추가한다 (권장)
프로젝트 빌드 설정에서 aapt의 압축 제외 목록에 확장자를 추가한다. 이렇게 하면 확장자를 바꾸지 않고도 해당 파일을 압축하지 않게 만들 수 있다.
build.gradle (module 수준):
| |
Gradle DSL에서 aaptOptions.noCompress에 확장자 문자열 목록을 지정하면, aapt는 해당 확장자의 파일을 압축하지 않고 APK에 넣는다. 그 결과 openFd()로 열 수 있다.
참고: 일부 Gradle/AGP 버전에서는
android.aaptOptions.noCompress의 문법이List<String>이 아닌 다른 형식일 수 있다. 환경에 따라.noCompress += "ext"또는 전체 목록을 다시 지정하는 방식으로 조정한다. (일반적인 점검 기준이며, 환경에 따라 다를 수 있음)
해결책 C — openFd()를 포기하고 open()으로 전체 읽기
파일이 반드시 압축되어도 상관없다면, FD/메모리 매핑 대신 스트림으로 전체 바이트를 읽는 방식으로 바꾼다.
| |
AssetManager.open()은 압축 파일도 자동으로 풀어서 InputStream을 반환하므로, 이 경로에서는 위 에러가 발생하지 않는다. 단, 메모리 매핑의 장점(지연 로딩, 추가 메모리 절약)을 포기하게 된다.
잘못된 해결책 (하지 말 것)
| 잘못된 해결책 | 왜 권장하지 않는가 |
|---|---|
| 파일을 아무 데나 옮기거나 경로만 바꾼다 | 문제의 원인(압축)과 무관하다. 경로가 맞아도 압축 파일이면 여전히 실패한다. 증상을 잠시 회피할 뿐이다 |
확장자만 포맷과 무관하게 .mp3로 바꾼다 | aapt 압축은 피할 수 있지만, 재생/파싱 로직이 확장자나 MIME로 동작하면 오히려 다른 버그를 만든다. 포맷이 실제로 일치할 때만 사용한다 |
| 근본 원인(압축)을 해결하지 않고 예외만 무시한다 | 예외를 삼키면 사운드가 조용히 재생되지 않는 등 원인 파악이 어려운 부작용이 남는다. 근본 원인(압축)을 제거해야 한다 |
6. 검증 명령
수정 후 실제로 압축이 풀렸는지(즉, FD로 열 수 있는지)를 확인한다.
| |
또한 앱 레벨에서 정상 동작을 확인하는 코드 검증:
| |
unzip -l 출력에서 해당 항목의 method가 Defl이 아니라 Stored로 보인다면, openFd()가 동작할 것이다. 에뮬레이터/실기기에서 실제 재생 코드를 한 번 돌려 FD가 정상 반환되는지 한 번 더 확인한다.
7. 향후 예방 조치
같은 문제가 재발하지 않도록 다음을 챙긴다.
- 빌드 설정을 단일화한다.
assets/에 넣는 커스텀 확장자 파일은 처음부터aaptOptions.noCompress에 미리 등록해 둔다. 확장자를 겨우 쓰는 임시 트릭에 의존하지 않는다. - 로컬 / CI에서 APK 산출물을 검증한다. 빌드 후
unzip -l로 커스텀 확장자 파일이Stored로 들어가는지 CI 단계에서 확인하면 회귀를 조기 발견한다. (일반적인 점검 기준) - openFd()를 써야만 하는 파일만 압축을 벤다. 그 외에는
open()스트림을 사용하는 편이 단순하고 메모리 측면에서도 예측이 쉽다. - 메모리 매핑이 필요한 이유를 명확히 둔다. 큰 파일을 여러 번 로딩하지 않도록 지연 매핑을 쓰려는 것이라면, 압축을 끄는 것이 맞는 선택이다.
DevTrace verdict
이 문제의 핵심은 파일이 없어서 실패하는 것이 아니다. 패키징 단계(aapt)에서 그 파일을 압축해 버렸기 때문에 파일 디스크립터로 열 수 없게 된 것이다.
DevTrace 결론
openFd()는 파일 디스크립터를 메모리 매핑으로 반환하므로, aapt가 압축한 asset은 파일 디스크립터로 열 수 없어 이 에러가 발생한다. 핵심은 파일을 압축하지 않게 만드는 것이다.
원문 출처는 문제 발견의 단서이며, 위 판단과 점검 항목은 DevTrace의 독자 분석이다.