본문 바로가기
Development/Error

[Error] Android OS 10 Target 시 파일 조회 원인과 해결 방법

by 은스타 2020. 3. 24.
반응형
Android 10에서 'Attempt to get length of null array' 오류 원인과 해결 방법 완벽 가이드

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 정책으로 인한 접근 제한
⑤ 외부 저장소가 마운트되지 않았을 때
(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());
}
. . . . .
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 기반 접근으로 전환
(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>
. . . . .
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();
        }
    }
}
(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);
        }
    }
}
주의: 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);
    }
}

#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);
(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;
}
. . . . .
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();
            }
        }
    }
}
(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();
                }
            }
        }
    }
}
. . . . .
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) {
        // 이미지 처리
    }
}
(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();
    }
}
. . . . .
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);
}

#5. 실전 해결 사례 및 FAQ
실제 개발 현장에서 자주 발생하는 오류 상황과 해결책을 알아보겠습니다.
1) 자주 발생하는 오류 케이스
(1) 공유 저장소 파일 목록 오류
오류 상황: 공유 저장소의 파일 목록을 가져올 때 null 반환
// 문제가 발생하는 코드
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", "파일 목록을 가져올 수 없습니다.");
}
(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);
}
(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");
    // 파일 작업
}
. . . . .
2) 자주 묻는 질문 (FAQ)
(1) 레거시 저장소 모드는 언제까지 사용 가능한가요?
requestLegacyExternalStorage 속성은 Android 10에서만 작동하며, Android 11부터는 무시됩니다. 따라서 임시 방편으로만 사용하고, 빠른 시일 내에 Scoped Storage로 마이그레이션해야 합니다.
(2) 모든 파일 접근 권한(MANAGE_EXTERNAL_STORAGE)이 필요한 경우는?
다음과 같은 앱 유형에만 이 권한이 필요합니다.

① 파일 관리자 앱
② 백업 및 복원 앱
③ 안티바이러스 앱
④ 문서 관리 앱
⑤ 디바이스 간 파일 전송 앱
중요: Google Play에서는 이 권한 사용을 엄격하게 검토하며, 정당한 사유 없이 사용하면 앱이 거부될 수 있습니다.
(3) 파일이 존재하는데도 null이 반환되는 이유는?
가장 흔한 원인은 다음과 같습니다.

① 권한이 부여되지 않음
② 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입니다");
    }
}
(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());
    }
}

마무리
Android 10에서 'Attempt to get length of null array' 오류는 주로 Scoped Storage 정책 변화로 인한 파일 시스템 접근 제한 때문에 발생합니다.
이 문제를 효과적으로 해결하기 위해서는 다음 사항들을 반드시 숙지하고 적용해야 합니다.
앱 전용 디렉토리 사용 - getExternalFilesDir() 메서드를 활용하여 앱 전용 저장 공간에 파일을 저장하고 관리합니다.
② 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로 마이그레이션하는 것이 중요합니다.
긴 글 읽어주셔서 감사합니다.

끝.
반응형