반응형
Android 10에서 'Attempt to get length of null array' 오류 원인과 해결 방법 완벽 가이드
개요
Android 10(API 레벨 29)을 사용하다 보면 파일을 조회하거나 접근할 때 'Attempt to get length of null array' 오류를 만나는 경우가 있습니다. 특히 이전 버전에서 잘 작동하던 앱이 Android 10에서 갑자기 이 오류를 표시하며 작동하지 않는다면 매우 당황스러울 것입니다. 이 글에서는 해당 오류의 주요 원인과 실제 개발 현장에서 적용할 수 있는 해결 방법을 상세히 알아보겠습니다.
목차
1. 오류의 기본 원인 이해하기
2. Android 10의 저장소 정책 변화
3. 권한 관련 문제 해결하기
4. 파일 접근 방식 수정 및 최적화
5. 실전 해결 사례 및 FAQ
#1. 오류의 기본 원인 이해하기
'Attempt to get length of null array' 오류는 기본적으로 null 객체의 length 속성에 접근하려고 할 때 발생합니다. 자바에서는 null 참조의 메서드나 속성에 접근하면 NullPointerException이 발생하는데, 이것이 안드로이드에서는 위와 같은 오류 메시지로 나타나는 것입니다.
1) 오류 발생 메커니즘
파일 시스템 접근 시 이 오류가 발생하는 주요 상황들입니다.
(1) 주요 발생 상황
① 파일이나 디렉토리가 존재하지 않을 때
② 파일 목록을 가져오는 API가 null을 반환할 때
③ 권한이 없어 파일 시스템에 접근할 수 없을 때
④ Scoped Storage 정책으로 인한 접근 제한
⑤ 외부 저장소가 마운트되지 않았을 때
② 파일 목록을 가져오는 API가 null을 반환할 때
③ 권한이 없어 파일 시스템에 접근할 수 없을 때
④ Scoped Storage 정책으로 인한 접근 제한
⑤ 외부 저장소가 마운트되지 않았을 때
(2) 오류 발생 코드 예시
문제가 되는 코드: 아래 코드는 Android 10에서 null을 반환할 가능성이 높습니다.
// 위험한 코드 - null 체크 없음
File externalStorage = Environment.getExternalStorageDirectory();
File[] files = externalStorage.listFiles();
// files가 null이면 아래에서 오류 발생
int fileCount = files.length; // Attempt to get length of null array
for (File file : files) { // NullPointerException 발생
Log.d("File", file.getName());
}
File externalStorage = Environment.getExternalStorageDirectory();
File[] files = externalStorage.listFiles();
// files가 null이면 아래에서 오류 발생
int fileCount = files.length; // Attempt to get length of null array
for (File file : files) { // NullPointerException 발생
Log.d("File", file.getName());
}
. . . . .
2) Android 10에서 심화된 이유
특히 Android 10에서는 저장소 접근 정책이 크게 변경되면서 이 문제가 더 빈번하게 발생하게 되었습니다.
| Android 버전 | 저장소 접근 방식 | 오류 발생 빈도 |
|---|---|---|
| Android 9 이하 | 자유로운 외부 저장소 접근 | 낮음 |
| Android 10 | Scoped Storage 도입 (선택) | 중간 |
| Android 11 이상 | Scoped Storage 필수 | 높음 |
#2. Android 10의 저장소 정책 변화
Android 10부터 Google은 사용자 프라이버시 보호를 위해 "Scoped Storage"라는 개념을 도입했습니다.
1) Scoped Storage 주요 변화
이전 버전과 비교했을 때 주요 변화를 살펴보겠습니다.
(1) 핵심 변경 사항
① 외부 저장소 직접 접근 제한 - 애플리케이션이 더 이상 모든 외부 저장소에 자유롭게 접근할 수 없음
② 샌드박스 저장소 도입 - 각 앱은 자신만의 샌드박스 저장소 공간을 가지며, 다른 앱은 이 공간에 접근 불가
③ 미디어 파일 접근 방식 변경 - 사진, 비디오, 음악 등은 MediaStore API 사용 필수
④ 공유 저장소 접근 제한 - Storage Access Framework 사용 또는 특별한 권한 필요
⑤ 파일 경로 기반 접근 제한 - URI 기반 접근으로 전환
② 샌드박스 저장소 도입 - 각 앱은 자신만의 샌드박스 저장소 공간을 가지며, 다른 앱은 이 공간에 접근 불가
③ 미디어 파일 접근 방식 변경 - 사진, 비디오, 음악 등은 MediaStore API 사용 필수
④ 공유 저장소 접근 제한 - Storage Access Framework 사용 또는 특별한 권한 필요
⑤ 파일 경로 기반 접근 제한 - URI 기반 접근으로 전환
(2) 변화 비교표
| 항목 | Android 9 이하 | Android 10 이상 |
|---|---|---|
| 외부 저장소 접근 | READ/WRITE 권한만으로 가능 | 앱 전용 디렉토리만 자유 접근 |
| 미디어 파일 | 직접 경로 접근 | MediaStore API 필수 |
| 다른 앱 파일 | 권한으로 접근 가능 | SAF 사용 필수 |
| 다운로드 폴더 | 직접 접근 가능 | MediaStore 또는 SAF |
. . . . .
2) 정책 변화로 인한 영향
이러한 변화로 인해 기존에 잘 작동하던 파일 접근 코드가 Android 10에서 'Attempt to get length of null array' 오류를 발생시키는 경우가 많아졌습니다.
(1) 영향받는 주요 기능
① 파일 탐색기 기능
② 갤러리 및 미디어 라이브러리
③ 백업 및 복원 기능
④ 파일 다운로드 및 업로드
⑤ 문서 편집 앱
② 갤러리 및 미디어 라이브러리
③ 백업 및 복원 기능
④ 파일 다운로드 및 업로드
⑤ 문서 편집 앱
중요: Android 11부터는 Scoped Storage가 필수이므로, Android 10에서 미리 대응하는 것이 좋습니다.
#3. 권한 관련 문제 해결하기
Android 10에서도 파일 시스템에 접근하려면 적절한 권한이 필요합니다.
1) 필요한 권한 선언하기
AndroidManifest.xml에 다음 권한을 추가합니다.
<!-- 기본 저장소 접근 권한 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- Android 10에서 모든 파일에 접근하려면 필요 -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<!-- Android 10에서 레거시 저장소 모드 사용 (임시 방편) -->
<application
android:requestLegacyExternalStorage="true"
... >
...
</application>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- Android 10에서 모든 파일에 접근하려면 필요 -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<!-- Android 10에서 레거시 저장소 모드 사용 (임시 방편) -->
<application
android:requestLegacyExternalStorage="true"
... >
...
</application>
. . . . .
2) 런타임 권한 요청하기
Android 6.0(API 레벨 23) 이상에서는 매니페스트에 권한을 선언하는 것 외에도 런타임에 사용자에게 직접 권한을 요청해야 합니다.
(1) 기본 저장소 권한 요청
// 권한 요청 코드
private static final int PERMISSION_REQUEST_CODE = 123;
private void requestStoragePermission() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
},
PERMISSION_REQUEST_CODE);
}
}
// 권한 요청 결과 처리
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == PERMISSION_REQUEST_CODE) {
if (grantResults.length > 0 &&
grantResults[0] == PackageManager.PERMISSION_GRANTED &&
grantResults[1] == PackageManager.PERMISSION_GRANTED) {
// 권한이 승인되었을 때 파일 접근 로직 수행
accessFiles();
} else {
// 권한이 거부되었을 때 처리
Toast.makeText(this, "저장소 접근 권한이 필요합니다", Toast.LENGTH_SHORT).show();
}
}
}
private static final int PERMISSION_REQUEST_CODE = 123;
private void requestStoragePermission() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
},
PERMISSION_REQUEST_CODE);
}
}
// 권한 요청 결과 처리
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == PERMISSION_REQUEST_CODE) {
if (grantResults.length > 0 &&
grantResults[0] == PackageManager.PERMISSION_GRANTED &&
grantResults[1] == PackageManager.PERMISSION_GRANTED) {
// 권한이 승인되었을 때 파일 접근 로직 수행
accessFiles();
} else {
// 권한이 거부되었을 때 처리
Toast.makeText(this, "저장소 접근 권한이 필요합니다", Toast.LENGTH_SHORT).show();
}
}
}
(2) Android 10 이상 파일 관리 권한
Android 10에서 MANAGE_EXTERNAL_STORAGE 권한이 필요한 경우 다음과 같이 구현합니다.
private void requestManageStoragePermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (!Environment.isExternalStorageManager()) {
Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
Uri uri = Uri.fromParts("package", getPackageName(), null);
intent.setData(uri);
startActivity(intent);
}
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (!Environment.isExternalStorageManager()) {
Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
Uri uri = Uri.fromParts("package", getPackageName(), null);
intent.setData(uri);
startActivity(intent);
}
}
}
주의: MANAGE_EXTERNAL_STORAGE 권한은 Google Play 스토어에서 엄격하게 검토됩니다. 파일 관리자, 백업 앱 등 명확한 사용 목적이 있어야 승인됩니다.
. . . . .
3) 권한 체크 베스트 프랙티스
(1) 권한 상태 확인 헬퍼 메서드
// 저장소 권한 확인 유틸리티
public class PermissionUtils {
public static boolean hasStoragePermission(Context context) {
return ContextCompat.checkSelfPermission(context,
Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED &&
ContextCompat.checkSelfPermission(context,
Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
}
public static boolean isAndroid10OrHigher() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
}
public static boolean canAccessAllFiles(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
return Environment.isExternalStorageManager();
}
return hasStoragePermission(context);
}
}
public class PermissionUtils {
public static boolean hasStoragePermission(Context context) {
return ContextCompat.checkSelfPermission(context,
Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED &&
ContextCompat.checkSelfPermission(context,
Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
}
public static boolean isAndroid10OrHigher() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
}
public static boolean canAccessAllFiles(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
return Environment.isExternalStorageManager();
}
return hasStoragePermission(context);
}
}
#4. 파일 접근 방식 수정 및 최적화
Android 10에서 안전하게 파일에 접근하는 다양한 방법을 알아보겠습니다.
1) 앱 전용 디렉토리 사용하기
Android 10에서는 외부 저장소의 앱 전용 디렉토리를 사용하는 것이 가장 안전합니다.
(1) 기본 앱 전용 디렉토리
// 앱 전용 외부 저장소 디렉토리 가져오기
File appExternalDir = getExternalFilesDir(null);
if (appExternalDir != null) {
File myFile = new File(appExternalDir, "myfile.txt");
// 파일 작업 수행
}
// 또는 특정 유형의 디렉토리를 가져오기
File appPicturesDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
File appDocumentsDir = getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS);
File appDownloadsDir = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS);
File appExternalDir = getExternalFilesDir(null);
if (appExternalDir != null) {
File myFile = new File(appExternalDir, "myfile.txt");
// 파일 작업 수행
}
// 또는 특정 유형의 디렉토리를 가져오기
File appPicturesDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
File appDocumentsDir = getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS);
File appDownloadsDir = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS);
(2) 안전한 파일 접근 패턴
// null 체크를 포함한 안전한 파일 목록 가져오기
public List<File> safeListFiles(File directory) {
List<File> fileList = new ArrayList<>();
if (directory == null || !directory.exists() || !directory.isDirectory()) {
return fileList; // 빈 목록 반환
}
File[] files = directory.listFiles();
if (files != null) {
fileList.addAll(Arrays.asList(files));
}
return fileList;
}
public List<File> safeListFiles(File directory) {
List<File> fileList = new ArrayList<>();
if (directory == null || !directory.exists() || !directory.isDirectory()) {
return fileList; // 빈 목록 반환
}
File[] files = directory.listFiles();
if (files != null) {
fileList.addAll(Arrays.asList(files));
}
return fileList;
}
. . . . .
2) Storage Access Framework 활용하기
Android 10에서는 Storage Access Framework(SAF)를 사용하는 것이 권장됩니다.
(1) 파일 선택하기
private static final int READ_REQUEST_CODE = 42;
// 파일 선택 창 열기
private void openFileSelector() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*"); // 모든 파일 유형
startActivityForResult(intent, READ_REQUEST_CODE);
}
// 선택 결과 처리
@Override
public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
super.onActivityResult(requestCode, resultCode, resultData);
if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
if (resultData != null) {
Uri uri = resultData.getData();
try {
// 파일 내용 읽기
InputStream inputStream = getContentResolver().openInputStream(uri);
if (inputStream != null) {
// 스트림을 사용하여 파일 처리
inputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
// 파일 선택 창 열기
private void openFileSelector() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*"); // 모든 파일 유형
startActivityForResult(intent, READ_REQUEST_CODE);
}
// 선택 결과 처리
@Override
public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
super.onActivityResult(requestCode, resultCode, resultData);
if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
if (resultData != null) {
Uri uri = resultData.getData();
try {
// 파일 내용 읽기
InputStream inputStream = getContentResolver().openInputStream(uri);
if (inputStream != null) {
// 스트림을 사용하여 파일 처리
inputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
(2) 디렉토리 접근하기
private static final int DIRECTORY_REQUEST_CODE = 43;
// 디렉토리 선택 창 열기
private void openDirectorySelector() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(intent, DIRECTORY_REQUEST_CODE);
}
// 선택 결과 처리
@Override
public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
super.onActivityResult(requestCode, resultCode, resultData);
if (requestCode == DIRECTORY_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
if (resultData != null) {
Uri treeUri = resultData.getData();
// 지속적인 접근 권한 요청
getContentResolver().takePersistableUriPermission(treeUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
// DocumentFile을 사용하여 디렉토리 및 파일 접근
DocumentFile documentFile = DocumentFile.fromTreeUri(this, treeUri);
if (documentFile != null && documentFile.exists()) {
// 모든 하위 파일 나열
DocumentFile[] files = documentFile.listFiles();
for (DocumentFile file : files) {
String name = file.getName();
boolean isDirectory = file.isDirectory();
}
}
}
}
}
// 디렉토리 선택 창 열기
private void openDirectorySelector() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(intent, DIRECTORY_REQUEST_CODE);
}
// 선택 결과 처리
@Override
public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
super.onActivityResult(requestCode, resultCode, resultData);
if (requestCode == DIRECTORY_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
if (resultData != null) {
Uri treeUri = resultData.getData();
// 지속적인 접근 권한 요청
getContentResolver().takePersistableUriPermission(treeUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
// DocumentFile을 사용하여 디렉토리 및 파일 접근
DocumentFile documentFile = DocumentFile.fromTreeUri(this, treeUri);
if (documentFile != null && documentFile.exists()) {
// 모든 하위 파일 나열
DocumentFile[] files = documentFile.listFiles();
for (DocumentFile file : files) {
String name = file.getName();
boolean isDirectory = file.isDirectory();
}
}
}
}
}
. . . . .
3) MediaStore API 활용하기
Android 10에서 미디어 파일에 접근하려면 MediaStore API를 사용해야 합니다.
(1) 이미지 목록 가져오기
// 이미지 가져오기
private void loadImages() {
List<String> imageList = new ArrayList<>();
Uri collection;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL);
} else {
collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
}
String[] projection = new String[] {
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.DATA
};
try (Cursor cursor = getContentResolver().query(
collection,
projection,
null,
null,
MediaStore.Images.Media.DATE_ADDED + " DESC"
)) {
int dataColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
while (cursor.moveToNext()) {
String path = cursor.getString(dataColumn);
imageList.add(path);
}
}
// 이미지 목록 사용
for (String imagePath : imageList) {
// 이미지 처리
}
}
private void loadImages() {
List<String> imageList = new ArrayList<>();
Uri collection;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL);
} else {
collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
}
String[] projection = new String[] {
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.DATA
};
try (Cursor cursor = getContentResolver().query(
collection,
projection,
null,
null,
MediaStore.Images.Media.DATE_ADDED + " DESC"
)) {
int dataColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
while (cursor.moveToNext()) {
String path = cursor.getString(dataColumn);
imageList.add(path);
}
}
// 이미지 목록 사용
for (String imagePath : imageList) {
// 이미지 처리
}
}
(2) 파일 생성하기
// 앱 내부 저장소에 파일 생성
private void createAppFile(String fileName, String content) {
try {
FileOutputStream fos = openFileOutput(fileName, Context.MODE_PRIVATE);
fos.write(content.getBytes());
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
// 앱 전용 외부 저장소에 파일 생성
private void createExternalAppFile(String fileName, String content) {
File file = new File(getExternalFilesDir(null), fileName);
try {
FileOutputStream fos = new FileOutputStream(file);
fos.write(content.getBytes());
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
private void createAppFile(String fileName, String content) {
try {
FileOutputStream fos = openFileOutput(fileName, Context.MODE_PRIVATE);
fos.write(content.getBytes());
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
// 앱 전용 외부 저장소에 파일 생성
private void createExternalAppFile(String fileName, String content) {
File file = new File(getExternalFilesDir(null), fileName);
try {
FileOutputStream fos = new FileOutputStream(file);
fos.write(content.getBytes());
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
. . . . .
4) 파일 존재 여부 확인 베스트 프랙티스
(1) 안전한 파일 체크
File directory = new File(path);
if (directory.exists() && directory.isDirectory()) {
File[] files = directory.listFiles();
if (files != null) {
// 파일 배열 처리
for (File file : files) {
// 파일 작업
}
} else {
// 파일 목록이 null인 경우 처리
Log.e("FileError", "디렉토리가 비어있거나 접근할 수 없습니다.");
}
} else {
// 디렉토리가 존재하지 않는 경우 처리
Log.e("FileError", "디렉토리가 존재하지 않습니다: " + path);
}
if (directory.exists() && directory.isDirectory()) {
File[] files = directory.listFiles();
if (files != null) {
// 파일 배열 처리
for (File file : files) {
// 파일 작업
}
} else {
// 파일 목록이 null인 경우 처리
Log.e("FileError", "디렉토리가 비어있거나 접근할 수 없습니다.");
}
} else {
// 디렉토리가 존재하지 않는 경우 처리
Log.e("FileError", "디렉토리가 존재하지 않습니다: " + path);
}
#5. 실전 해결 사례 및 FAQ
실제 개발 현장에서 자주 발생하는 오류 상황과 해결책을 알아보겠습니다.
1) 자주 발생하는 오류 케이스
(1) 공유 저장소 파일 목록 오류
오류 상황: 공유 저장소의 파일 목록을 가져올 때 null 반환
// 문제가 발생하는 코드
File externalStorage = Environment.getExternalStorageDirectory();
File[] files = externalStorage.listFiles(); // 여기서 null 반환 가능
for (File file : files) { // NullPointerException 발생
// ...
}
File externalStorage = Environment.getExternalStorageDirectory();
File[] files = externalStorage.listFiles(); // 여기서 null 반환 가능
for (File file : files) { // NullPointerException 발생
// ...
}
해결책: null 체크를 추가하고, Android 10에서는 SAF 사용을 권장합니다.
// 안전한 코드
File externalStorage = Environment.getExternalStorageDirectory();
File[] files = externalStorage.listFiles();
if (files != null) {
for (File file : files) {
// 파일 처리
}
} else {
// 권한 문제 또는 다른 이유로 파일 목록을 가져올 수 없음
Log.e("FileError", "파일 목록을 가져올 수 없습니다.");
}
File externalStorage = Environment.getExternalStorageDirectory();
File[] files = externalStorage.listFiles();
if (files != null) {
for (File file : files) {
// 파일 처리
}
} else {
// 권한 문제 또는 다른 이유로 파일 목록을 가져올 수 없음
Log.e("FileError", "파일 목록을 가져올 수 없습니다.");
}
(2) SD 카드 접근 오류
Android 10에서는 SD 카드와 같은 이동식 미디어에 대한 접근이 제한됩니다.
해결책: SAF를 사용하여 사용자에게 SD 카드 접근 권한을 요청합니다.
// SAF를 사용하여 SD 카드 접근
private void accessSDCard() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(intent, DIRECTORY_REQUEST_CODE);
}
private void accessSDCard() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(intent, DIRECTORY_REQUEST_CODE);
}
(3) 다운로드 폴더 접근 오류
Android 10에서는 Download 폴더에 대한 직접 접근이 제한됩니다.
// Android 10 이상에서 다운로드 폴더 접근
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// MediaStore API 사용
ContentValues values = new ContentValues();
values.put(MediaStore.Downloads.DISPLAY_NAME, "myfile.txt");
values.put(MediaStore.Downloads.MIME_TYPE, "text/plain");
Uri uri = getContentResolver().insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values);
if (uri != null) {
try (OutputStream os = getContentResolver().openOutputStream(uri)) {
if (os != null) {
os.write("파일 내용".getBytes());
}
} catch (IOException e) {
e.printStackTrace();
}
}
} else {
// Android 9 이하에서는 기존 방식 사용
File downloadsDir = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS);
File file = new File(downloadsDir, "myfile.txt");
// 파일 작업
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// MediaStore API 사용
ContentValues values = new ContentValues();
values.put(MediaStore.Downloads.DISPLAY_NAME, "myfile.txt");
values.put(MediaStore.Downloads.MIME_TYPE, "text/plain");
Uri uri = getContentResolver().insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values);
if (uri != null) {
try (OutputStream os = getContentResolver().openOutputStream(uri)) {
if (os != null) {
os.write("파일 내용".getBytes());
}
} catch (IOException e) {
e.printStackTrace();
}
}
} else {
// Android 9 이하에서는 기존 방식 사용
File downloadsDir = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS);
File file = new File(downloadsDir, "myfile.txt");
// 파일 작업
}
. . . . .
2) 자주 묻는 질문 (FAQ)
(1) 레거시 저장소 모드는 언제까지 사용 가능한가요?
requestLegacyExternalStorage 속성은 Android 10에서만 작동하며, Android 11부터는 무시됩니다. 따라서 임시 방편으로만 사용하고, 빠른 시일 내에 Scoped Storage로 마이그레이션해야 합니다.
(2) 모든 파일 접근 권한(MANAGE_EXTERNAL_STORAGE)이 필요한 경우는?
다음과 같은 앱 유형에만 이 권한이 필요합니다.
① 파일 관리자 앱
② 백업 및 복원 앱
③ 안티바이러스 앱
④ 문서 관리 앱
⑤ 디바이스 간 파일 전송 앱
① 파일 관리자 앱
② 백업 및 복원 앱
③ 안티바이러스 앱
④ 문서 관리 앱
⑤ 디바이스 간 파일 전송 앱
중요: Google Play에서는 이 권한 사용을 엄격하게 검토하며, 정당한 사유 없이 사용하면 앱이 거부될 수 있습니다.
(3) 파일이 존재하는데도 null이 반환되는 이유는?
가장 흔한 원인은 다음과 같습니다.
① 권한이 부여되지 않음
② Scoped Storage 정책으로 인한 접근 제한
③ 외부 저장소가 마운트되지 않음
④ 파일 경로가 잘못됨
⑤ 앱이 접근 권한이 없는 다른 앱의 샌드박스 영역에 접근 시도
① 권한이 부여되지 않음
② Scoped Storage 정책으로 인한 접근 제한
③ 외부 저장소가 마운트되지 않음
④ 파일 경로가 잘못됨
⑤ 앱이 접근 권한이 없는 다른 앱의 샌드박스 영역에 접근 시도
(4) Android 버전별 대응 전략은?
| Android 버전 | 권장 접근 방법 | 비고 |
|---|---|---|
| Android 9 이하 | 기존 파일 시스템 API | READ/WRITE 권한 필요 |
| Android 10 | Scoped Storage + 레거시 모드 | 점진적 마이그레이션 |
| Android 11 이상 | Scoped Storage 필수 | SAF, MediaStore 사용 |
(5) 성능에 미치는 영향은?
SAF와 MediaStore API 사용 시 약간의 성능 오버헤드가 발생할 수 있습니다. 하지만 사용자 프라이버시 보호와 시스템 안정성 향상을 위한 필수적인 변화입니다.
. . . . .
3) 마이그레이션 체크리스트
기존 앱을 Android 10 Scoped Storage로 마이그레이션할 때 확인해야 할 사항들입니다.
| 항목 | 확인 사항 | 우선순위 |
|---|---|---|
| 권한 선언 | AndroidManifest.xml에 필요한 권한 추가 | 높음 |
| 런타임 권한 | 런타임 권한 요청 코드 구현 | 높음 |
| 파일 경로 | 앱 전용 디렉토리로 변경 | 높음 |
| 미디어 파일 | MediaStore API로 전환 | 중간 |
| 공유 파일 | SAF 구현 | 중간 |
| null 체크 | 모든 파일 접근 코드에 null 체크 추가 | 높음 |
| 에러 핸들링 | 파일 접근 실패 시 처리 로직 구현 | 중간 |
| 테스트 | Android 10, 11 디바이스에서 테스트 | 높음 |
. . . . .
4) 디버깅 팁
(1) 로그 활용하기
// 파일 접근 로그 추가
private void debugFileAccess(File directory) {
Log.d("FileDebug", "경로: " + directory.getAbsolutePath());
Log.d("FileDebug", "존재 여부: " + directory.exists());
Log.d("FileDebug", "디렉토리 여부: " + directory.isDirectory());
Log.d("FileDebug", "읽기 권한: " + directory.canRead());
Log.d("FileDebug", "쓰기 권한: " + directory.canWrite());
File[] files = directory.listFiles();
if (files != null) {
Log.d("FileDebug", "파일 개수: " + files.length);
} else {
Log.e("FileDebug", "파일 목록이 null입니다");
}
}
private void debugFileAccess(File directory) {
Log.d("FileDebug", "경로: " + directory.getAbsolutePath());
Log.d("FileDebug", "존재 여부: " + directory.exists());
Log.d("FileDebug", "디렉토리 여부: " + directory.isDirectory());
Log.d("FileDebug", "읽기 권한: " + directory.canRead());
Log.d("FileDebug", "쓰기 권한: " + directory.canWrite());
File[] files = directory.listFiles();
if (files != null) {
Log.d("FileDebug", "파일 개수: " + files.length);
} else {
Log.e("FileDebug", "파일 목록이 null입니다");
}
}
(2) 권한 상태 확인
// 현재 권한 상태 확인
private void checkPermissionStatus() {
boolean hasRead = ContextCompat.checkSelfPermission(this,
Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
boolean hasWrite = ContextCompat.checkSelfPermission(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
Log.d("Permission", "READ_EXTERNAL_STORAGE: " + hasRead);
Log.d("Permission", "WRITE_EXTERNAL_STORAGE: " + hasWrite);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Log.d("Permission", "isExternalStorageManager: " + Environment.isExternalStorageManager());
}
}
private void checkPermissionStatus() {
boolean hasRead = ContextCompat.checkSelfPermission(this,
Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
boolean hasWrite = ContextCompat.checkSelfPermission(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
Log.d("Permission", "READ_EXTERNAL_STORAGE: " + hasRead);
Log.d("Permission", "WRITE_EXTERNAL_STORAGE: " + hasWrite);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Log.d("Permission", "isExternalStorageManager: " + Environment.isExternalStorageManager());
}
}
마무리
Android 10에서 'Attempt to get length of null array' 오류는 주로 Scoped Storage 정책 변화로 인한 파일 시스템 접근 제한 때문에 발생합니다.
이 문제를 효과적으로 해결하기 위해서는 다음 사항들을 반드시 숙지하고 적용해야 합니다.
① 앱 전용 디렉토리 사용 - getExternalFilesDir() 메서드를 활용하여 앱 전용 저장 공간에 파일을 저장하고 관리합니다.
② MediaStore API 활용 - 미디어 파일 접근을 위해 MediaStore API를 사용하여 사진, 비디오, 오디오 파일을 안전하게 처리합니다.
③ Storage Access Framework 도입 - 다른 앱의 파일이나 공유 저장소 접근을 위해 SAF를 활용하여 사용자에게 명시적으로 권한을 요청합니다.
④ 철저한 null 체크 - 모든 파일 시스템 접근 코드에 null 체크를 추가하여 안전하게 처리합니다.
⑤ 런타임 권한 요청 - 필요한 권한을 AndroidManifest.xml에 선언하고 런타임에 사용자에게 요청합니다.
② MediaStore API 활용 - 미디어 파일 접근을 위해 MediaStore API를 사용하여 사진, 비디오, 오디오 파일을 안전하게 처리합니다.
③ Storage Access Framework 도입 - 다른 앱의 파일이나 공유 저장소 접근을 위해 SAF를 활용하여 사용자에게 명시적으로 권한을 요청합니다.
④ 철저한 null 체크 - 모든 파일 시스템 접근 코드에 null 체크를 추가하여 안전하게 처리합니다.
⑤ 런타임 권한 요청 - 필요한 권한을 AndroidManifest.xml에 선언하고 런타임에 사용자에게 요청합니다.
이러한 방법들을 적용하면 Android 10 및 그 이상 버전에서도 안정적으로 파일 시스템에 접근할 수 있으며, 'Attempt to get length of null array' 오류를 효과적으로 방지할 수 있습니다. 더불어 사용자 프라이버시도 보호하고 앱 스토어 정책도 준수할 수 있습니다.
기억하세요: requestLegacyExternalStorage는 임시 방편일 뿐이며, Android 11부터는 작동하지 않습니다. 가능한 빠른 시일 내에 Scoped Storage로 마이그레이션하는 것이 중요합니다.
긴 글 읽어주셔서 감사합니다.
끝.
끝.
반응형
'Development > Error' 카테고리의 다른 글
| [Error] Invalid file name: must contain only [a-z0-9_.] 원인과 해겳 방법 (0) | 2020.04.08 |
|---|---|
| [Error] ORACLE 계정이 Lock 걸렸을 때 원인과 해결 방법 (0) | 2020.04.08 |
| [Error] Android Data-Scheme 설정 후 앱 아이콘 사라지는 이유와 해결 방법 (0) | 2020.03.22 |
| [Error] 큰 용량의 PDF 읽기 (0) | 2019.10.29 |
| [Error] ListView를 드래그 하면 검게 보이는 현상 (0) | 2019.10.01 |