Android 앱 내부 디렉토리에 대한 고찰
Android Internal Directory 에 대한 심층 분석
Introduction
가끔 사진 편집으로 사용한 앱을 삭제하면 편집한 사진이 저장되었던 폴더가 사라져서 느낌표 5억 5천개가 생겼을 때가 있다.
그런데 또 다른 앱 같은 경우 앱을 삭제해도 사진 파일이 그대로 있을 때가 있다. 무엇이 다른걸까
단순히 버그인걸까 아니면 내부가 다른걸까
해당 글에서는 안드로이드에서 적용되는 디렉토리 수준에 대하여 여러 글을 참고하여 정리하며 타 앱 관점에서 접근하는 부분만 다루고자 한다.
What is that
안드로이드에서 디렉토리에 접근할 수 있는데 보안을 위하여 샌드박스가 적용되어 다른 앱에서 접근이 불가한 디렉토리들이 있다.
리눅스와 비슷한 모습을 보이지만 보안과 정책으로 인하여 강화된 룰로 내부를 보호하고 있다.
안드로이드에서 접근하는 디렉토리는 크게 내부 저장소와 외부 저장소로 나뉠 수 있다.
내부 저장소는 앱에서 생성하는 캐시 파일 다운로드한 파일 앱을 구성하고 사용자가 다시 앱을 로드할 때 필요한 파일들을 주로 저장하고 외부 저장소에는 앱의 임시 파일이나 일시적으로 생성되는 파일들이 주로 배치된다.
그런데 특이한 점이 있는데 앱 내부 디렉토리는 앱을 삭제하면 같이 삭제된다는 점이 있으며 외부 디렉토리는 앱을 삭제해도 잔류한다는 점이 있다.
Android ApplicationInfo 에 따라 들어가면 설치된 앱에서 확인할 수 있는 디렉토리들이 몇개 있다.
해당 API 들을 통해 앱 접근을 확인해보면 아래와 같다.
1
2
3
4
5
Access Fail: /data/user/0/com.sample.application # ApplicationInfo.dataDir
Access Done: /data/app/~~qwerty==/com.sample.application-qwerty==/base.apk # ApplicationInfo.publicSourceDir
Access Done: /data/app/~~qwerty==/com.sample.application-qwerty==/lib/arm64 # ApplicationInfo.nativeLibraryDir
Access Done: /data/app/~~qwerty==/com.sample.application-qwerty==/base.apk # ApplicationInfo.sourceDir
Access Fail: /data/user_de/0/com.sample.application # ApplicationInfo.deviceProtectedDataDir
사용했던 API 들은 아래와 같다.
1
2
3
public String nativeLibraryDir
// Full path to the directory where native JNI libraries are stored.
1
2
3
public String publicSourceDir
// Full path to the publicly available parts of sourceDir, including resources and manifest. This may be different from sourceDir if an application is forward locked.
1
2
3
public String sourceDir
// Full path to the base APK for this application.
위 3개 API 는 /data/app/~~qwerty==/com.sample.application-qwerty==/
에 연결된 경로이다.
publicSourceDir
과sourceDir
의 차이는 Forward Locked 여부인데 일반적으로sourceDir
를 사용하는 걸 권장한다.
1
2
3
public String dataDir
// Full path to the default directory assigned to the package for its persistent data.
다음 API 는 본인이 속한 디렉토리일 경우에만 접근하는 것으로 보인다.
Android 개인 디렉토리 관하여
안드로이드 개발에 있어서 파일 저장은 매우 중요한 부분이다. 각 Android 애플리케이션에는 애플리케이션 데이터, 구성, 캐시 및 기타 파일을 저장하기 위한 고유한 개인 디렉토리가 있다. 이러한 파일은 다른 애플리케이션에서는 볼 수 없으므로 데이터 보안과 격리가 보장된다. 다음에서는 Android 의 개인 디렉토리에 대해 심도 있게 이해하는 데 도움이 될 것이다.
개인 디렉토리의 정의
안드로이드의 개인 디렉토리는 애플리케이션만 관리하고 접근할 수 있는 파일 디렉토리를 말한다. 즉, 디렉토리가 애플리케이션에 속한다는 뜻이다. 각 Android 애플리케이션이 설치될 때, 시스템은 내부 저장 공간에 개인 디렉토리를 할당하여 애플리케이션의 영구 파일을 저장한다. 또한, 애플리케이션 캐시 파일을 저장하기 위한 디렉토리도 있다. 이러한 디렉토리는 해당 애플리케이션에서만 완전히 공개되며 다른 애플리케이션에서 직접 액세스할 수 없다.
개인 디렉토리의 위치
안드로이드의 개인 디렉토리는 주로 내부 저장소와 외부 저장소의 두 곳에 분산되어 있다.
내부 저장소 개인 디렉토리
내부 저장소 개인 디렉토리는 /data/data/packagename/
에 있으며, 여기서 packagename 은 애플리케이션의 패키지 이름이다. 이 디렉토리는 애플리케이션의 주 저장 영역이며, 설정 파일, 데이터베이스 , 이미지 등과 같은 애플리케이션의 영구 파일을 저장하는 데 사용된다. 앱은 시스템 권한 없이도 이러한 파일을 읽고 쓸 수 있지만, 다른 앱은 이러한 파일에 직접 액세스할 수 없다.
외부 저장소 개인 디렉토리
외부 저장소 개인 디렉토리는 /sdcard/Android/data/packagename/
에 있다 . 이 디렉토리는 임시 파일, 다운로드한 파일 등과 같은 애플리케이션 캐시 파일을 저장하는 데 주로 사용된다. 이 디렉토리는 외부 저장소에 위치하므로 해당 애플리케이션에서만 볼 수 있지만, 다른 애플리케이션이 어떤 방법을 통해 이 디렉토리에 접근할 수도 있다. 따라서 민감한 데이터를 외부 저장소 개인 디렉토리에 저장하는 것은 권장하지 않는다.
개인 디렉토리를 획득하고 운영하는 방법
Android 에서는 Context
클래스의 getFilesDir()
메서드를 호출하여 애플리케이션의 개인 디렉토리 경로를 가져올 수 있다. 예를 들어:
1
File privateDir = context.getFilesDir();
개인 디렉토리의 경로를 얻은 후에는 디렉토리에서 파일을 생성하고, 파일을 읽고 쓰고, 다른 작업을 수행할 수 있다. 예를 들어, 개인 디렉토리에 my_file.txt
라는 파일을 만들고 여기에 일부 데이터를 쓰려면 다음과 같이 할 수 있다.
1
2
3
4
5
6
7
File file = new File(privateDir, "my_file.txt");
boolean created = file.createNewFile();
if (created) {
FileOutputStream outputStream = new FileOutputStream(file);
outputStream.write("Hello, world!".getBytes());
outputStream.close();
}
위의 코드는 먼저 File
클래스의 createNewFile()
메서드를 통해 개인 디렉토리에 my_file.txt
라는 파일을 만들고, 파일이 성공적으로 만들어졌는지 여부를 나타내는 부울 값을 반환한다. 그런 다음, FileOutputStream
클래스를 사용하여 문자열 Hello, world!
가 파일에 기록된다.
대부분 getDir() 로 파일 및 폴더를 생성하거나 getDataDir() 로 내부 디렉토리에 접근하여 파일을 생성한다.
Android Directory Structure
그러면 더 넓게 보았을 때 내부 저장소, 외부 저장소(확장된 외부 저장소, SD 카드 저장소 포함), 시스템 저장소 디렉토리를 포함한 Android 저장소 디렉토리 구조를 자세히 소개해보려고 한다. 또한 서로 다른 획득 경로 간의 차이, 데이터 지우기와 캐시 지우기의 차이 등 관련 개념 간의 차이도 설명한다.
Android 에서 볼 수 있는 디렉토리들은 위와 같다.
빠진 부분도 있을 수 있으나 앱단 입장에서 볼 때 위와 같이 볼 수 있다.
내부 저장소
내부 저장소는 시스템의 매우 특별한 위치에 있다. 장치에 설치된 각 앱에 대해 시스템은 자동으로 data/data/packagename/xxx
에 해당 폴더를 만든다 . 내부 저장소에 파일을 저장하려면 기본적으로 해당 애플리케이션에서만 파일에 액세스할 수 있으며, 애플리케이션에서 생성된 모든 파일은 애플리케이션 패키지 이름과 동일한 디렉토리에 저장된다. 즉, 내부 저장소에 있는 애플리케이션에서 생성된 파일은 이 애플리케이션과 연결된다. 앱을 제거하면 내부 저장소에 있는 이러한 파일도 삭제된다. 사용자는 루트 권한을 얻지 않는 한 이 내부 디렉토리에 접근할 수 없다.
1
2
String fileDir = this.getFilesDir().getAbsolutePath();
String cacheDir = this.getCacheDir().getAbsolutePath();
일반적으로 얻는 경로는 data/data/packagename/xxx
이다.
1
2
fileDir: /data/user/0/packagename/files
cacheDir: /data/user/0/packagename/cache
내부 저장소 경로의 경우 일반적으로 다음 두 가지 방법으로 얻는다. 내부 저장소 공간을 확보하려면 Context
를 사용해야 한다.
1
Context.getFileDir()
해당 내부 저장 경로는 data/data/packagename/files
이지만 일부 모바일 폰의 경우 얻은 경로는 data/user/0/packagename/files
이다.
1
Context.getCacheDir()
해당 내부 저장 경로는 data/data/packagename/cache
이다. 하지만 일부 휴대폰의 경우, 얻은 경로는 data/user/0/packagename/cache
이다. 애플리케이션의 캐시 디렉토리, 장치 메모리가 부족할 때 이 디렉토리에 있는 파일이 먼저 삭제되므로 여기에 저장된 파일은 보장되지 않으며 언제든지 손실될 수 있다.
외부 저장소
외부 저장소에 대해 혼동하기 쉬운데, 안드로이드 4.4 이전에는 휴대폰의 내장 저장소를 내부 저장소라고 불렀고, 삽입된 SD 카드는 외부 저장소였다. 그러나 안드로이드 4.4 이후 현재 휴대폰의 내장 저장소는 매우 크다. 이제 안드로이드 10.0에서는 일부 휴대폰이 256G의 저장소에 도달하였으며 이 경우 휴대폰의 내장 저장소도 외부 저장소이다. SD 카드를 삽입하면 외부 저장소라고도 한다. 따라서 외부 저장소는 SD 카드와 확장 카드 메모리의 두 부분으로 나뉜다.
1
2
3
4
5
6
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
File[] files = getExternalFilesDirs(Environment.MEDIA_MOUNTED);
for (File file : files) {
Log.e("file_dir", file.getAbsolutePath());
}
}
위 코드에 대해 인쇄된 결과는 다음과 같다.
1
2
/storage/emulated/o/~
/storage/extSdCard/~
두 개의 디렉토리가 인쇄된다.
첫 번째 디렉토리는 장치의 외부 저장소 디렉토리이다. 디렉토리 구조는 다음과 같다: /storage/emulated/0/Android/data/packagename/files
.
두 번째 디렉토리는 저장소 카드의 디렉토리 구조이다. 경로는 다음과 같다: /storage/extSdCard/Android/data/packagename/files
.
외부 저장소 확장
이 디렉토리 경로는 Context
통해 얻어야 하며, 이러한 파일도 앱이 제거되면 삭제된다. 내부 저장소와 유사한다.
1
getExternalCacheDir()
해당 외부 저장 경로: /storage/emulated/0/Android/data/packagename/cache
1
getExternalFilesDir(String type)
해당 외부 저장 경로: /storage/emulated/0/Android/data/packagename/files
SD 카드 저장
SD 카드에 있는 파일은 자유롭게 접근할 수 있다. 즉, 파일의 데이터는 다른 애플리케이션이나 사용자가 접근할 수 있다. 애플리케이션을 제거하면 제거 전에 생성된 파일은 여전히 유지된다. SD 카드의 파일 경로는 환경을 통해 얻어야 하며, 이를 얻기 전에 SD의 상태를 확인해야 한다.
1
2
3
4
5
6
7
8
9
10
MEDIA_UNKNOWN SD 카드를 알 수 없음
MEDIA_REMOVED SD 카드 제거.
MEDIA_UNMOUNTED SD 카드가 마운트되지 않음.
MEDIA_CHECKING SD카드 검사, SD카드가 방금 설치된 경우
MEDIA_NOFS SD 카드가 비어 있거나 지원되지 않는 파일 시스템을 사용.
MEDIA_MOUNTED SD 카드가 정상 마운트.
MEDIA_MOUNTED_READ_ONLY SD 카드가 마운트되었지만 읽기 전용.
MEDIA_SHARED SD 카드 공유
MEDIA_BAD_REMOVAL SD 카드 제거 오류
MEDIA_UNMOUNTABLE SD 카드가 존재하지만 미디어가 손상된 경우와 같이 마운트할 수 없음.
아래 코드와 같이 상태를 검증할 수 있다.
1
2
3
4
String externalStorageState = Environment.getExternalStorageState();
if (externalStorageState.equals(Environment.MEDIA_MOUNTED)){
// some code
}
1
getExternalStorageDirectory()
해당 외부 저장 경로: /storage/emulated/0
1
getExternalStoragePublicDirectory(String type)
다음과 같은 외부 저장소의 공유 폴더 경로를 가져온다.
1
2
3
4
5
6
DIRECTORY_MUSIC 음악 디렉토리
DIRECTORY_PICTURES 사진 디렉토리
DIRECTORY_MOVIES 영화 디렉토리
DIRECTORY_DOWNLOADS 디렉토리 다운로드
DIRECTORY_DCIM 카메라 사진이나 비디오 파일의 저장 디렉토리
DIRECTORY_DOCUMENTS 문서 디렉토리
검증하는 코드는 아래와 같다.
1
String externalStoragePublicDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getPath();
위의 내용은 카메라 DCIM 디렉토리를 얻는 것이며, 해당 경로는 /storage/emulated/0/DCIM
이다.
시스템 저장 디렉토리
1
getRootDirectory()
시스템 파티션의 루트 경로에 해당: /system
1
getDataDirectory()
사용자 데이터 디렉토리 경로에 해당: /data
1
getDownloadCacheDirectory()
사용자 캐시 디렉토리 경로에 해당: /cache
관련 개념 간의 차이점
getFileDir()
와 getCacheDir()
의 차이점
둘 다 내부 저장 디렉토리 /data/data/packagename/
의 동일 레벨에 위치하는데, 전자는 파일 디렉토리에 있고 후자는 캐시 디렉토리에 있다.
getFileDir()
와 getExternalFilesDir(String type)
의 차이점
전자는 내부 저장소 디렉토리 /data/data/packagename/file
에 있고, 후자는 외부 저장소 디렉토리 /storage/emulated/0/Android/data/packagename/files
에 있다. 둘 다 애플리케이션 패키지 이름으로 존재하며, 즉 앱에 속하므로 앱을 제거하면 삭제된다. 위에 언급된 앱 다운로드 및 업그레이드 기능을 위해, 서버에서 다운로드한 앱은 내부 저장소 디렉토리가 아닌 외부 저장소 디렉토리에 저장해야 한다. 왜냐하면 내부 저장소 디렉토리의 공간은 매우 작기 때문이다. 또한 관련 테스트도 수행했다. apk 가 내부 저장소 디렉토리 파일 아래에 배치되면 설치 중에 문제가 발생하고 패키지를 구문 분석하는 동안 오류가 발생했다는 메시지가 표시된다.
데이터 지우기와 캐시 지우기의 차이점
앱에는 데이터 지우기와 캐시 지우기라는 두 가지 개념이 있다. 그렇다면 이 두 개념은 각각 어떤 디렉토리를 지우는 건가
데이터 지우기
데이터를 지우면 앱에 저장된 모든 데이터, 즉 위에서 언급한 패키지 이름 아래의 모든 파일이 지워진다. 여기에는 내부 저장소(/data/data/packagename/
)와 외부 저장소(/storage/emulated/0/Android/data/packagename/
)가 포함된다. 물론, 앱을 제거한 후에도 SD 카드의 데이터 외에도 데이터는 계속 존재한다.
캐시를 지우기
캐시는 프로그램이 실행 중일 때의 임시 저장 공간이다. 인터넷에서 다운로드한 임시 이미지를 저장할 수 있다. 사용자 관점에서 볼 때 캐시를 지우는 것은 사용자에게 큰 영향을 미치지 않는다. 그러나 사용자가 캐시를 지운 후 다시 APP 을 사용하면 로컬 캐시가 지워졌기 때문에 모든 데이터를 인터넷에서 다시 가져와야 한다. 캐시를 지울 때 애플리케이션과 연관된 캐시가 제대로 지워지도록 getCacheDir()
또는 getExternalCacheDir()
경로에 캐시 파일을 저장하세요.
파일 시스템 접근 방식
내부 저장소 (Internal Storage)
애플리케이션의 전용 저장소로, getFilesDir()
및 getCacheDir()
을 이용해 접근 가능하다.
보안이 보장되며, 권한 요청 없이 접근할 수 있으나, 다른 애플리케이션에서는 격리되어 있다.
저장된 데이터는 애플리케이션이 제거될 때 자동 삭제된다.
내부 저장소의 파일은 MODE_PRIVATE
모드로 생성된다.
1
2
3
4
5
6
File file = new File(getFilesDir(), "example.txt");
try (FileOutputStream fos = new FileOutputStream(file)) {
fos.write("Hello, World!".getBytes());
} catch (IOException e) {
e.printStackTrace();
}
외부 저장소 (External Storage)
getExternalFilesDir(null)
을 사용하여 접근 가능하며, 앱이 제거되면 해당 디렉토리도 삭제된다.
공용 외부 저장소에 접근하려면 MediaStore 또는 Storage Access Framework (SAF)를 사용해야 한다.
MANAGE_EXTERNAL_STORAGE
권한이 없으면 특정 디렉토리 외에는 직접 접근이 제한된다.
1
2
3
4
5
6
File externalDir = new File(getExternalFilesDir(null), "example.txt");
try (FileOutputStream fos = new FileOutputStream(externalDir)) {
fos.write("Hello, External Storage!".getBytes());
} catch (IOException e) {
e.printStackTrace();
}
MANAGE_EXTERNAL_STORAGE 권한에 관하여
Scoped Storage가 도입되면서 /sdcard/Android/data/
및 /sdcard/Android/obb/
경로의 직접 접근이 차단되었다.
adb shell
로는 여전히 접근 가능하지만 일반 앱에서는 접근이 불가능하다.
MANAGE_EXTERNAL_STORAGE
권한을 부여받은 애플리케이션만 전체 파일 시스템 접근이 가능하다.
MANAGE_EXTERNAL_STORAGE
권한 요청 및 파일 접근 예제는 아래와 같다.
메니페스트에 추가되는 권한은 아래와 같다.
1
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
코드는 아래와 같다.
1
2
3
4
5
6
if (Environment.isExternalStorageManager()) { // 권한이 부여되었는지 확인한다.
// some code
} else {
Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION); // special permission 창을 띄운다.
startActivity(intent);
}
절대경로 접근 (직접 경로)
파일에 접근할 때 직접 경로로 접근하는 그러니까 절대경로로 접근하는 방법들이 있다.
fopen() 같은 경우 아래와 같은 인자를 받는다.
1
fopen(const char * restrict path, const char * restrict mode)
이때
1
fopen("/data/user/0/com.sample.application/some/directory/file.txt", "r")
위와 같이 완전한 문자열의 절대경로로 접근하는 것을 의미한다.
Conclusion
이렇게 안드로이드 내부 디렉토리를 살펴보았다. 사실 시작은 /data/user/0
그러니까 getFileDir()
쪽 접근할 수 있는 방안을 찾아보다 이렇게 심층적으로 살펴보게 되었다. 결과적으로 앱 내부 디렉토리는 완전 막혀있어서 접근이 불가하다, 다른 방법이 있나 모색해봤는데 여러가지 방법은 있지만 대부분이 일반 디바이스에서 실행할 수 있는 방법은 아니어서 좋은 결과를 찾지는 못했다.
지금까지 앱 내부 디렉토리 접근에 대한 내용을 살펴보았다. 안드로이드에서 제공하는 권한과 히든 스페이스를 적절히 활용하면 접근할 수 있는 경로와 파일들이 다소 확장된다. 보안 측면에서 내부 디렉토리를 강화하여 필요에 따라 다른 앱의 내부 디렉토리에 접근할 수 있는 방법이 있는지 연구해볼 필요가 있을 것 같다.
실제 개발에서는 개인 디렉토리를 사용하여 애플리케이션 구성 정보, 사용자 데이터, 캐시 파일 등을 저장하는 경우가 많다. 예를 들어, 음악 플레이어 앱은 사용자의 재생 목록, 노래 정보 등을 개인 디렉토리에 저장해 두어 사용자가 다음에 앱을 열 때 이전 재생 상태로 복원할 수 있다. 또한, 개인 디렉토리는 애플리케이션의 임시 파일, 다운로드 파일 등을 저장하는 데 사용할 수도 있으며, 이를 통해 애플리케이션의 파일 관리를 용이하게 할 수 있다.
이 글에서는 안드로이드의 개인 디렉토리의 개념과 위치, 개인 디렉토리를 얻고 운영하는 방법, 그리고 실제 개발에서의 적용 시나리오를 자세히 소개해보았다. 개인 디렉토리의 사용을 마스터함으로써 애플리케이션 데이터와 파일을 보다 효과적으로 관리하고 데이터 보안과 격리를 보장할 수 있다.
If you find any errors, please let me know by comment or email. Thank you.