Android AssetManager ‘FileNotFoundException: file is probably compressed’ — 압축된 asset을 openFd()로 열 때 해결

검증 환경

본 절은 원문 사례·문서에 등장한 버전/도구를 정리한 것이다. 별도 실험실 재현이 명시되지 않은 항목은 일반화하지 않는다.

  • 본문에 명시된 오류 메시지·프레임워크 버전을 기준으로 원인을 추적했다.
  • 문서 정리일: 2026-08-30
  • OS/CI 세부 값은 프로젝트마다 다르므로, 적용 전 로컬에서 동일 오류 메시지를 재확인한다.

1. 문제 정의

앱에서 assets/ 폴더 안에 넣어 둔 사운드나 리소스 파일을 재생하거나 읽으려 하다가, 일부 파일만 실패하고 일부는 정상 동작하는 경험을 한 적이 있는가? 이 문제는 전형적으로 아래와 같은 스택 트레이스와 함께 나타난다.

1
2
3
4
05-31 13:23:04.227 18440 18603 W System.err: java.io.FileNotFoundException: This file can not be opened as a file descriptor; it is probably compressed
05-31 13:23:04.227 18440 18603 W System.err:    at android.content.res.AssetManager.openAssetFd(Native Method)
05-31 13:23:04.227 18440 18603 W System.err:    at android.content.res.AssetManager.openFd(AssetManager.java:331)
05-31 13:23:04.227 18440 18603 W System.err:    at com.phonegap.AudioPlayer.startPlaying(AudioPlayer.java:201)

핵심 문구는 짧지만 결정적이다.

This file can not be opened as a file descriptor; it is probably compressed

출처: StackOverflow 6186866

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가 압축해 버린 확장자는 실패했기 때문이다.

1
2
3
4
// 예: 원래 읽으려던 파일이 압축 확장자였고,
// .mp3 로 변경하니 `openFd()` 로 열린다
val afd: AssetFileDescriptor = assets.openFd("sounds/click.mp3")
val fd: ParcelFileDescriptor = afd.parcelFileDescriptor

다만 확장자 트릭은 파일 포맷이 실제로 그 확장자와 일치해야 의미가 있다. 포맷이 아니라 단순히 확장자만 바꾸는 것은 버그를 유발할 수 있으므로 주의한다.

해결책 B — aaptOptions.noCompress로 확장자를 압축 제외에 추가한다 (권장)

프로젝트 빌드 설정에서 aapt의 압축 제외 목록에 확장자를 추가한다. 이렇게 하면 확장자를 바꾸지 않고도 해당 파일을 압축하지 않게 만들 수 있다.

build.gradle (module 수준):

1
2
3
4
5
6
android {
    aaptOptions {
        // assets/ 에서 이 확장자들은 압축하지 않는다
        noCompress "csv", "txt", "bin"
    }
}

Gradle DSL에서 aaptOptions.noCompress에 확장자 문자열 목록을 지정하면, aapt는 해당 확장자의 파일을 압축하지 않고 APK에 넣는다. 그 결과 openFd()로 열 수 있다.

참고: 일부 Gradle/AGP 버전에서는 android.aaptOptions.noCompress의 문법이 List<String>이 아닌 다른 형식일 수 있다. 환경에 따라 .noCompress += "ext" 또는 전체 목록을 다시 지정하는 방식으로 조정한다. (일반적인 점검 기준이며, 환경에 따라 다를 수 있음)

해결책 C — openFd()를 포기하고 open()으로 전체 읽기

파일이 반드시 압축되어도 상관없다면, FD/메모리 매핑 대신 스트림으로 전체 바이트를 읽는 방식으로 바꾼다.

1
2
3
4
val input: InputStream = assets.open("data/config.bin")
val bytes = input.readBytes()
input.close()
// bytes 를 사용

AssetManager.open()은 압축 파일도 자동으로 풀어서 InputStream을 반환하므로, 이 경로에서는 위 에러가 발생하지 않는다. 단, 메모리 매핑의 장점(지연 로딩, 추가 메모리 절약)을 포기하게 된다.

잘못된 해결책 (하지 말 것)

잘못된 해결책왜 권장하지 않는가
파일을 아무 데나 옮기거나 경로만 바꾼다문제의 원인(압축)과 무관하다. 경로가 맞아도 압축 파일이면 여전히 실패한다. 증상을 잠시 회피할 뿐이다
확장자만 포맷과 무관하게 .mp3로 바꾼다aapt 압축은 피할 수 있지만, 재생/파싱 로직이 확장자나 MIME로 동작하면 오히려 다른 버그를 만든다. 포맷이 실제로 일치할 때만 사용한다
근본 원인(압축)을 해결하지 않고 예외만 무시한다예외를 삼키면 사운드가 조용히 재생되지 않는 등 원인 파악이 어려운 부작용이 남는다. 근본 원인(압축)을 제거해야 한다

6. 검증 명령

수정 후 실제로 압축이 풀렸는지(즉, FD로 열 수 있는지)를 확인한다.

1
2
3
4
5
6
# 1) APK 내부의 해당 파일이 실제로 압축됐는지 확인
unzip -l app-debug.apk | grep 'assets/sounds/click'

# 2) stored(압축 안 됨)인지 deflated(압축됨)인지 method 컬럼 확인
#  - Stored     : 압축 안 됨 → openFd() 가능
#  - Defl:N     : 압축됨   → openFd() 실패

또한 앱 레벨에서 정상 동작을 확인하는 코드 검증:

1
2
3
4
5
6
// 앱 코드에서 재생 전에 openFd()가 성공하는지 검증
@Test
fun openFdShouldNotThrowForNoCompressAsset(context: Context) {
    val afd = context.assets.openFd("sounds/click.mp3") // 예외 없이 통과해야 함
    assertNotNull(afd.parcelFileDescriptor)
}

unzip -l 출력에서 해당 항목의 method가 Defl이 아니라 Stored로 보인다면, openFd()가 동작할 것이다. 에뮬레이터/실기기에서 실제 재생 코드를 한 번 돌려 FD가 정상 반환되는지 한 번 더 확인한다.

7. 향후 예방 조치

같은 문제가 재발하지 않도록 다음을 챙긴다.

  • 빌드 설정을 단일화한다. assets/에 넣는 커스텀 확장자 파일은 처음부터 aaptOptions.noCompress에 미리 등록해 둔다. 확장자를 겨우 쓰는 임시 트릭에 의존하지 않는다.
  • 로컬 / CI에서 APK 산출물을 검증한다. 빌드 후 unzip -l로 커스텀 확장자 파일이 Stored로 들어가는지 CI 단계에서 확인하면 회귀를 조기 발견한다. (일반적인 점검 기준)
  • openFd()를 써야만 하는 파일만 압축을 벤다. 그 외에는 open() 스트림을 사용하는 편이 단순하고 메모리 측면에서도 예측이 쉽다.
  • 메모리 매핑이 필요한 이유를 명확히 둔다. 큰 파일을 여러 번 로딩하지 않도록 지연 매핑을 쓰려는 것이라면, 압축을 끄는 것이 맞는 선택이다.

DevTrace verdict

이 문제의 핵심은 파일이 없어서 실패하는 것이 아니다. 패키징 단계(aapt)에서 그 파일을 압축해 버렸기 때문에 파일 디스크립터로 열 수 없게 된 것이다.


출처: StackOverflow 6186866 — java.io.FileNotFoundException: This file can not be opened as a file descriptor; it is probably compressed

DevTrace 결론

openFd()는 파일 디스크립터를 메모리 매핑으로 반환하므로, aapt가 압축한 asset은 파일 디스크립터로 열 수 없어 이 에러가 발생한다. 핵심은 파일을 압축하지 않게 만드는 것이다.

원문 출처는 문제 발견의 단서이며, 위 판단과 점검 항목은 DevTrace의 독자 분석이다.