문제 상황
Android 앱 개발 중 Camera2 API를 사용하여 이미지를 캡처한 후 외부 저장소에 저장하려고 했을 때, 다음과 같은 예외가 발생할 수 있습니다:
FATAL EXCEPTION: videoThread
Process: com.example.myapplication, PID: 17049
java.io.FileNotFoundException: /storage/emulated/0/thisimage.jpg: open failed: EPERM (Operation not permitted)
이 에러는 Android의 저장소 접근 정책 변화로 인해 발생하는데, 특히 Android 10(API 29) 이상에서는 Scoped Storage가 도입되면서 앱이 외부 저장소에 접근하는 방식이 제한되었습니다.
원인 분석
- Scoped Storage 도입
- Android 10부터는 외부 저장소 접근이 제한됩니다.
- Environment.getExternalStorageDirectory()와 같은 경로를 사용할 경우, 특별한 설정 없이 파일을 저장하거나 읽을 수 없습니다.
- 권한 문제
- WRITE_EXTERNAL_STORAGE 권한이 제대로 요청되지 않았거나, 사용자가 권한 요청을 거부했을 수 있습니다.
- 잘못된 경로 사용
- /storage/emulated/0/ 경로는 앱 전용 디렉터리가 아니므로, Scoped Storage 정책에 의해 접근이 제한될 수 있습니다.
해결 방법
1. 앱 전용 디렉터리 사용
앱 전용 디렉터리는 특별한 권한 없이 사용할 수 있습니다. 다음과 같이 앱의 외부 저장소 전용 디렉터리를 사용하면 문제를 해결할 수 있습니다:
val file = File(getExternalFilesDir(null), "thisimage.jpg")
- getExternalFilesDir(null)은 앱 전용 디렉터리를 반환합니다.
- 이 디렉터리에서 저장된 파일은 앱이 삭제되면 함께 삭제됩니다.
2. Scoped Storage 설정 (Android 10 이상)
Android 10 이상에서 외부 저장소를 사용하려면 MediaStore를 사용하는 것이 권장됩니다. 다음은 예시 코드입니다:
val contentValues = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, "thisimage.jpg")
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
}
val resolver = contentResolver
val uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
uri?.let {
val outputStream = resolver.openOutputStream(it)
outputStream?.use { stream ->
stream.write(bytes)
}
Toast.makeText(this, "Image saved to Pictures", Toast.LENGTH_SHORT).show()
}
3. Legacy External Storage 설정 (임시 방편)
Android 10(API 29)에서는 requestLegacyExternalStorage를 AndroidManifest.xml에 추가하여 기존 방식으로 외부 저장소를 사용할 수 있습니다:
<application
android:requestLegacyExternalStorage="true"
... >
</application>
하지만, 이 설정은 Android 11(API 30)부터는 무시됩니다. 따라서 장기적으로 MediaStore를 사용하는 방식으로 전환하는 것이 좋습니다.
4. 권한 요청 확인
외부 저장소에 파일을 저장하려면 WRITE_EXTERNAL_STORAGE와 READ_EXTERNAL_STORAGE 권한이 필요합니다. 다음 코드는 권한 요청을 수행합니다:
fun get_permissions() {
val permissionsList = mutableListOf(
android.Manifest.permission.CAMERA,
android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
android.Manifest.permission.READ_EXTERNAL_STORAGE
)
if (permissionsList.any { ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED }) {
ActivityCompat.requestPermissions(this, permissionsList.toTypedArray(), 101)
}
}
최종 코드 예제
수정된 onImageAvailable 메서드는 다음과 같습니다:
override fun onImageAvailable(reader: ImageReader) {
val image = reader.acquireLatestImage()
val buffer = image.planes[0].buffer
val bytes = ByteArray(buffer.remaining())
buffer.get(bytes)
try {
val file = File(getExternalFilesDir(null), "thisimage.jpg")
FileOutputStream(file).use { output ->
output.write(bytes)
}
Toast.makeText(this@MainActivity, "Image captured and saved: ${file.absolutePath}", Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
e.printStackTrace()
Toast.makeText(this@MainActivity, "Failed to save image", Toast.LENGTH_SHORT).show()
} finally {
image.close()
}
}
결론
Android의 저장소 정책 변화로 인해 기존의 방식으로 파일을 저장하는 데 어려움이 생겼지만, 위에서 설명한 방법들을 사용하면 문제를 해결할 수 있습니다. 특히, 장기적으로는 Scoped Storage에 맞는 방식을 사용하는 것이 권장됩니다. 저는 일단 앱 전용 디렉토리를 사용했더니 해결되었습니다.