앱별 파일에 액세스

대부분의 경우 앱은 다른 앱에서 액세스할 필요가 없거나 액세스하면 안 되는 파일을 만듭니다. 시스템에서 제공하는 아래 위치에 이러한 앱별 파일을 저장할 수 있습니다.

  • 내부 저장소 디렉터리: 이 디렉터리에는 영구 파일을 저장하는 전용 위치와 캐시 데이터를 저장하는 위치가 모두 포함되어 있습니다. 시스템은 다른 앱에서 이러한 위치에 액세스하는 것을 방지하고, Android 10(API 수준 29) 이상에서는 이러한 위치가 암호화됩니다. 이와 같은 특성으로 인해 앱 자체에서만 액세스할 수 있는 민감한 정보를 저장하기에 좋습니다.

  • 외부 저장소 디렉터리: 이 디렉터리에는 영구 파일을 저장하는 전용 위치와 캐시 데이터를 저장하는 위치가 모두 포함되어 있습니다. 다른 앱에 적절한 권한이 있는 경우 이러한 디렉터리에 액세스할 수 있지만 디렉터리에 저장된 파일은 개발자의 앱에서만 사용하게 되어 있습니다. 다른 앱에서 액세스할 수 있는 파일을 만들려면 앱에서 이러한 파일을 외부 저장소의 공유 저장공간 부분에 대신 저장해야 합니다.

사용자가 앱을 제거하면 앱별 저장소에 저장된 파일이 삭제됩니다. 이러한 이유로, 사용자가 앱과는 별개로 유지할 것으로 예상하는 항목을 이 저장소에 저장해서는 안 됩니다. 예를 들어 앱에서 사용자의 사진 캡처를 허용하면 사용자는 앱을 제거한 후에도 사진에 액세스할 수 있다고 예상합니다. 따라서 공유 저장소를 대신 사용하여 이러한 유형의 파일을 적절한 미디어 컬렉션에 저장해야 합니다.

다음 섹션에서는 앱별 디렉터리 내에 파일을 저장하고 파일에 액세스하는 방법을 설명합니다.

내부 저장소에서 액세스

앱별로 시스템의 내부 저장소 내에 앱이 파일을 구성할 수 있는 디렉터리가 제공됩니다. 한 디렉터리는 앱의 영구 파일에 사용되고 다른 디렉터리에는 앱의 캐시된 파일이 포함되어 있습니다. 앱에는 이러한 디렉터리의 파일을 읽고 쓰는 데 어떤 시스템 권한도 필요하지 않습니다.

다른 앱은 내부 저장소에 저장된 파일에 액세스할 수 없습니다. 이와 같은 이유로 내부 저장소는 다른 앱이 액세스해서는 안 되는 앱 데이터를 저장하기에 좋습니다.

그러나 이러한 디렉터리는 작은 경향이 있습니다. 앱별 파일을 내부 저장소에 작성하기 전에 앱에서 기기의 여유 공간을 쿼리해야 합니다.

영구 파일에 액세스

앱의 일반적인 영구 파일은 컨텍스트 객체의 filesDir 속성을 사용하여 액세스할 수 있는 디렉터리에 있습니다. 프레임워크에서 제공하는 여러 메서드를 사용하여 이 디렉터리의 파일에 액세스하고 파일을 이 디렉터리에 저장할 수 있습니다.

파일 액세스 및 저장

파일 액세스 및 저장에 File API를 사용할 수 있습니다.

앱 성능을 유지하려면 동일한 파일을 여러 번 열거나 닫지 마세요.

다음 코드 스니펫은 File API 사용 방법을 보여줍니다.

Kotlin

val file = File(context.filesDir, filename)

Java

File file = new File(context.getFilesDir(), filename);

스트림을 사용하여 파일 저장

File API 사용의 대안으로 openFileOutput()을 호출하여 filesDir 디렉터리 내 파일에 쓰는 FileOutputStream을 가져올 수 있습니다.

다음 코드 스니펫은 파일에 텍스트를 쓰는 방법을 보여줍니다.

Kotlin

val filename = "myfile"
val fileContents = "Hello world!"
context.openFileOutput(filename, Context.MODE_PRIVATE).use {
        it.write(fileContents.toByteArray())
}

Java

String filename = "myfile";
String fileContents = "Hello world!";
try (FileOutputStream fos = context.openFileOutput(filename, Context.MODE_PRIVATE)) {
    fos.write(fileContents.toByteArray());
}

내부 저장소 내 이 디렉터리에 저장된 파일에 다른 앱의 액세스를 허용하려면 FLAG_GRANT_READ_URI_PERMISSION 속성이 있는 FileProvider를 사용하세요.

스트림을 사용하여 파일에 액세스

파일을 스트림으로 읽으려면 openFileInput()을 사용하세요.

Kotlin

context.openFileInput(filename).bufferedReader().useLines { lines ->
    lines.fold("") { some, text ->
        "$some\n$text"
    }
}

Java

FileInputStream fis = context.openFileInput(filename);
InputStreamReader inputStreamReader =
        new InputStreamReader(fis, StandardCharsets.UTF_8);
StringBuilder stringBuilder = new StringBuilder();
try (BufferedReader reader = new BufferedReader(inputStreamReader)) {
    String line = reader.readLine();
    while (line != null) {
        stringBuilder.append(line).append('\n');
        line = reader.readLine();
    }
} catch (IOException e) {
    // Error occurred when opening raw file for reading.
} finally {
    String contents = stringBuilder.toString();
}

파일 목록 보기

다음 코드 스니펫과 같이 fileList()를 호출하여 filesDir 디렉터리에 있는 모든 파일의 이름이 포함된 배열을 가져올 수 있습니다.

Kotlin

var files: Array<String> = context.fileList()

Java

Array<String> files = context.fileList();

중첩된 디렉터리 만들기

Kotlin 기반 코드에서 getDir()을 호출하여 또는 Java 기반 코드에서 루트 디렉터리와 새 디렉터리 이름을 File 생성자에 전달하여 중첩된 디렉터리를 만들거나 내부 디렉터리를 열 수도 있습니다.

Kotlin

context.getDir(dirName, Context.MODE_PRIVATE)

Java

File directory = context.getFilesDir();
File file = new File(directory, filename);

캐시 파일 만들기

민감한 정보를 일시적으로만 저장해야 하는 경우 내부 저장소 내 앱의 지정된 캐시 디렉터리를 사용하여 데이터를 저장해야 합니다. 모든 앱별 저장소의 경우와 마찬가지로 이 디렉터리에 저장된 파일은 사용자가 앱을 제거할 때 삭제되지만 이 디렉터리의 파일은 더 빨리 삭제될 수 있습니다.

캐시된 파일을 만들려면 File.createTempFile()을 호출하세요.

Kotlin

File.createTempFile(filename, null, context.cacheDir)

Java

File.createTempFile(filename, null, context.getCacheDir());

앱에서 컨텍스트 객체의 cacheDir 속성과 File API를 사용하여 이 디렉터리의 파일에 액세스합니다.

Kotlin

val cacheFile = File(context.cacheDir, filename)

Java

File cacheFile = new File(context.getCacheDir(), filename);

캐시 파일 삭제

Android에서 캐시 파일을 직접 삭제하는 경우가 있다 하더라도 이러한 파일을 정리하는 작업을 시스템에 의존해서는 안 됩니다. 항상 앱의 캐시 파일을 내부 저장소 내에 유지해야 합니다.

내부 저장소 내 캐시 디렉터리에서 파일을 삭제하려면 다음 메서드 중 하나를 사용하세요.

  • 파일을 표시하는 File 객체의 delete() 메서드

    Kotlin

    cacheFile.delete()
    

    Java

    cacheFile.delete();
    
  • 앱 컨텍스트의 deleteFile() 메서드. 파일 이름을 전달합니다.

    Kotlin

    context.deleteFile(cacheFileName)
    

    Java

    context.deleteFile(cacheFileName);
    

외부 저장소에서 액세스

앱별 파일을 저장하는 데 내부 저장소 공간이 충분하지 않으면 대신 외부 저장소를 사용해보세요. 시스템은 앱 내에서만 사용자에게 가치를 제공하는 파일을 앱에서 정리할 수 있는 디렉터리를 외부 저장소에 제공합니다. 한 디렉터리는 앱의 영구 파일에 사용되고 다른 디렉터리에는 앱의 캐시된 파일이 포함되어 있습니다.

Android 4.4(API 수준 19) 이상에서는 앱이 외부 저장소 내 앱별 디렉터리에 액세스하기 위해 저장소 관련 권한을 요청할 필요가 없습니다. 이러한 디렉터리에 저장된 파일은 앱을 제거할 때 삭제됩니다.

Android 9(API 수준 28) 이하를 실행하는 기기에서는 앱이 적절한 저장소 권한을 보유하고 있다면 다른 앱에 속하는 앱별 파일에 액세스할 수 있습니다. 사용자에게 더 많은 파일 제어 권한을 제공하고 파일이 복잡해지지 않도록 하기 위해, Android 10(API 수준 29) 이상을 타겟팅하는 앱에는 기본적으로 외부 저장소로 범위가 지정된 액세스 권한 또는 범위 지정 저장소가 부여됩니다. 범위 지정 저장소를 사용 설정하면 앱에서는 다른 앱에 속한 앱별 디렉터리에 액세스할 수 없습니다.

저장소를 사용할 수 있는지 확인

외부 저장소는 사용자가 삭제할 수 있는 실제 볼륨에 있으므로 외부 저장소에서 앱별 데이터를 읽거나 외부 저장소에 앱별 데이터를 쓰기 전에 볼륨에 액세스할 수 있는지 확인합니다.

Environment.getExternalStorageState()를 호출하여 볼륨 상태를 쿼리할 수 있습니다. 반환된 상태가 MEDIA_MOUNTED이면 외부 저장소 내에서 앱별 파일을 읽고 쓸 수 있습니다. 반환된 상태가 MEDIA_MOUNTED_READ_ONLY이면 파일을 읽을 수만 있습니다.

예를 들어 다음 메서드는 저장소의 사용 가능 여부를 확인하는 데 유용합니다.

Kotlin

// Checks if a volume containing external storage is available
// for read and write.
fun isExternalStorageWritable(): Boolean {
    return Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED
}

// Checks if a volume containing external storage is available to at least read.
fun isExternalStorageReadable(): Boolean {
     return Environment.getExternalStorageState() in
        setOf(Environment.MEDIA_MOUNTED, Environment.MEDIA_MOUNTED_READ_ONLY)
}

Java

// Checks if a volume containing external storage is available
// for read and write.
private boolean isExternalStorageWritable() {
    return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
}

// Checks if a volume containing external storage is available to at least read.
private boolean isExternalStorageReadable() {
     return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED) ||
            Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED_READ_ONLY);
}

이동식 외부 저장소가 없는 기기에서는 다음 명령어를 사용하여 외부 저장소 가용성 로직을 테스트하기 위한 가상 볼륨을 사용 설정합니다.

adb shell sm set-virtual-disk true

실제 저장소 위치 선택

내부 메모리의 파티션을 외부 저장소로 할당하는 기기에서 SD 카드 슬롯을 제공하는 경우도 있습니다. 즉, 기기에 외부 저장소가 포함될 수 있는 여러 실제 볼륨이 있으므로 앱별 저장소에 사용할 볼륨을 선택해야 합니다.

다른 위치에 액세스하려면 ContextCompat.getExternalFilesDirs()를 호출하세요. 다음 코드 스니펫과 같이 반환된 배열의 첫 번째 요소는 기본 외부 저장소 볼륨으로 간주합니다. 가득 차거나 사용할 수 없는 경우가 아니라면 이 볼륨을 사용하세요.

Kotlin

val externalStorageVolumes: Array<out File> =
        ContextCompat.getExternalFilesDirs(applicationContext, null)
val primaryExternalStorage = externalStorageVolumes[0]

Java

File[] externalStorageVolumes =
        ContextCompat.getExternalFilesDirs(getApplicationContext(), null);
File primaryExternalStorage = externalStorageVolumes[0];

영구 파일에 액세스

외부 저장소에서 앱별 파일에 액세스하려면 getExternalFilesDir()을 호출합니다.

앱 성능을 유지하려면 동일한 파일을 여러 번 열거나 닫지 마세요.

다음 코드 스니펫은 getExternalFilesDir() 호출 방법을 보여줍니다.

Kotlin

val appSpecificExternalDir = File(context.getExternalFilesDir(null), filename)

Java

File appSpecificExternalDir = new File(context.getExternalFilesDir(null), filename);

캐시 파일 만들기

외부 저장소 내 캐시에 앱별 파일을 추가하려면 externalCacheDir 참조를 가져옵니다.

Kotlin

val externalCacheFile = File(context.externalCacheDir, filename)

Java

File externalCacheFile = new File(context.getExternalCacheDir(), filename);

캐시 파일 삭제

외부 캐시 디렉터리에서 파일을 삭제하려면 파일을 표시하는 File 객체에서 delete() 메서드를 사용하세요.

Kotlin

externalCacheFile.delete()

Java

externalCacheFile.delete();

미디어 콘텐츠

앱 내에만 있는 사용자에게 가치를 제공하는 미디어 파일과 앱이 호환되면 다음 코드 스니펫과 같이 외부 저장소 내 앱별 디렉터리에 미디어 파일을 저장하는 것이 가장 좋습니다.

Kotlin

fun getAppSpecificAlbumStorageDir(context: Context, albumName: String): File? {
    // Get the pictures directory that's inside the app-specific directory on
    // external storage.
    val file = File(context.getExternalFilesDir(
            Environment.DIRECTORY_PICTURES), albumName)
    if (!file?.mkdirs()) {
        Log.e(LOG_TAG, "Directory not created")
    }
    return file
}

Java

@Nullable
File getAppSpecificAlbumStorageDir(Context context, String albumName) {
    // Get the pictures directory that's inside the app-specific directory on
    // external storage.
    File file = new File(context.getExternalFilesDir(
            Environment.DIRECTORY_PICTURES), albumName);
    if (file == null || !file.mkdirs()) {
        Log.e(LOG_TAG, "Directory not created");
    }
    return file;
}

DIRECTORY_PICTURES와 같은 API 상수에서 제공하는 디렉터리 이름을 사용해야 합니다. 이러한 디렉터리 이름으로 시스템에서 파일을 올바르게 처리할 수 있습니다. 사전 정의된 하위 디렉터리 이름 중에 파일에 적합한 것이 없다면 nullgetExternalFilesDir()에 대신 전달할 수 있습니다. 이렇게 하면 외부 저장소 내의 루트 앱별 디렉터리가 반환됩니다.

여유 공간 쿼리

사용자는 대부분 기기에 사용 가능한 저장공간이 충분하지 않으므로 앱에서 공간을 신중하게 사용해야 합니다.

저장하는 데이터 양을 미리 알면 getAllocatableBytes()를 호출하여 기기에서 앱에 제공할 수 있는 공간을 확인할 수 있습니다. getAllocatableBytes()의 반환 값이 기기의 현재 여유 공간보다 클 수 있습니다. 시스템이 다른 앱의 캐시 디렉터리에서 삭제할 수 있는 파일을 식별했기 때문입니다.

앱 데이터를 저장할 공간이 충분하다면 allocateBytes()를 호출합니다. 충분하지 않으면 앱이 사용자에게 기기에서 일부 파일을 삭제하거나 기기에서 모든 캐시 파일을 삭제하라고 요청할 수 있습니다.

다음 코드 스니펫은 앱이 기기의 여유 공간을 쿼리할 수 있는 방법의 예를 보여줍니다.

Kotlin

// App needs 10 MB within internal storage.
const val NUM_BYTES_NEEDED_FOR_MY_APP = 1024 * 1024 * 10L;

val storageManager = applicationContext.getSystemService<StorageManager>()!!
val appSpecificInternalDirUuid: UUID = storageManager.getUuidForPath(filesDir)
val availableBytes: Long =
        storageManager.getAllocatableBytes(appSpecificInternalDirUuid)
if (availableBytes >= NUM_BYTES_NEEDED_FOR_MY_APP) {
    storageManager.allocateBytes(
        appSpecificInternalDirUuid, NUM_BYTES_NEEDED_FOR_MY_APP)
} else {
    val storageIntent = Intent().apply {
        // To request that the user remove all app cache files instead, set
        // "action" to ACTION_CLEAR_APP_CACHE.
        action = ACTION_MANAGE_STORAGE
    }
}

Java

// App needs 10 MB within internal storage.
private static final long NUM_BYTES_NEEDED_FOR_MY_APP = 1024 * 1024 * 10L;

StorageManager storageManager =
        getApplicationContext().getSystemService(StorageManager.class);
UUID appSpecificInternalDirUuid = storageManager.getUuidForPath(getFilesDir());
long availableBytes =
        storageManager.getAllocatableBytes(appSpecificInternalDirUuid);
if (availableBytes >= NUM_BYTES_NEEDED_FOR_MY_APP) {
    storageManager.allocateBytes(
            appSpecificInternalDirUuid, NUM_BYTES_NEEDED_FOR_MY_APP);
} else {
    // To request that the user remove all app cache files instead, set
    // "action" to ACTION_CLEAR_APP_CACHE.
    Intent storageIntent = new Intent();
    storageIntent.setAction(ACTION_MANAGE_STORAGE);
}

저장소 관리 활동 만들기

앱은 시작 시 사용자가 앱이 사용자의 기기에 저장한 데이터를 관리할 수 있는 맞춤 활동을 선언하고 만들 수 있습니다. 개발자는 이 맞춤 '공간 관리' 활동을 매니페스트 파일의 android:manageSpaceActivity 속성을 사용하여 선언합니다. 파일 관리자 앱은 앱이 활동을 내보내지 않을 때도 즉, 활동이 android:exportedfalse로 설정할 때도 이 활동을 호출할 수 있습니다.

사용자에게 일부 기기 파일을 삭제하도록 요청

사용자가 기기에서 삭제할 파일을 선택하도록 요청하려면 ACTION_MANAGE_STORAGE 작업이 포함된 인텐트를 호출합니다. 이 인텐트는 사용자에게 메시지를 표시합니다. 원하는 경우 이 메시지에는 기기에서 사용 가능한 여유 공간이 표시될 수 있습니다. 사용자 친화적인 이 정보를 표시하려면 다음 계산 결과를 사용하세요.

StorageStatsManager.getFreeBytes() / StorageStatsManager.getTotalBytes()

사용자에게 모든 캐시 파일을 삭제하도록 요청

또는 사용자에게 기기의 모든 앱에서 캐시 파일을 삭제하도록 요청할 수 있습니다. ACTION_CLEAR_APP_CACHE 인텐트 작업이 포함된 인텐트를 호출하면 됩니다.

추가 리소스

기기의 저장소에 파일을 저장하는 방법에 관한 자세한 내용은 다음 리소스를 참조하세요.

동영상