WRITE_EXTERNAL_STORAGE 권한 요청없이 파일을 쓰고, 다운로드 받는 방법

galcyurio
PRND
Published in
7 min readJan 20, 2022

API 29 이상의 기기에서 별도의 쓰기 권한이 없어도 다운로드 디렉토리에 파일을 다운로드받을 수 있다는 것을 알고 계셨나요?
모르고 계셨다면 이 글을 통해 API 28 이하와 API 29 이상의 기기별로 어떻게 처리해야하는지 알아보고 앱에 적용해보세요.

안녕하세요.
헤이딜러 안드로이드팀의 김태현입니다.

Android 10에 Scoped Storage가 나온 이후로 저장소와 관련된 접근 방식이 굉장히 많이 변경되었습니다. 그 중 하나가 Android 10 (API 29) 부터는 WRITE_EXTERNAL_STORAGE 권한이 없어도 다운로드, 사진 디렉토리에 파일과 이미지를 다운로드할 수 있다는 것입니다.

https://google.github.io/modernstorage/permissions/

혹시 여러분들의 앱에서 불필요하게 API 29 이상까지 권한을 요청하고 있었다면 이번 기회에 불필요한 권한 요청을 제거하여 유저 경험을 향상시켜보는걸 추천드립니다.

이 글에서는 Scoped Storage에 대한 대응 방법에 대해서만 설명하기 때문에 Scoped Storage가 무엇인지는 설명하지 않습니다. Scoped Storage에 대해서 궁금하신 분들은 공식 문서를 참고해주세요.

본격적으로 설명하기 전에 전제조건으로 앱이 targetSdkVersion을 API 30 이상으로 설정한 것으로 하겠습니다. 그 이유는 targetSdkVersion을 29 이하로 설정하는 경우 requestLegacyExternalStorage과 같은 속성을 설정하면 아래에서 설명하는 것과 다르게 동작하기 때문입니다.

저장소 권한 처리

많은 분들이 WRITE_EXTERNAL_STORAGE 권한을 사용하고 계실겁니다. 헤이딜러에서도 이미지 다운로드 기능을 위해서 이 권한을 사용하고 있습니다.
이 권한은 위험 권한으로 분리되기 때문에 AndroidManifest.xml 파일에 선언하고 런타임에 권한을 부여할지 말지를 유저에게 물어보아야 합니다.

API 29 이상부터는 굳이 WRITE_EXTERNAL_STORAGE 권한을 사용할 필요가 없습니다.
따라서 런타임에 권한을 체크할 필요가 없고 AndroidManifest.xml 에도 아래와 같이 설정해주시면 됩니다.

<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />

여기서 주의하셔야 할 점은 기존에는 READ_EXTERNAL_STORAGE 권한을 별도로 선언하지 않고 WRITE_EXTERNAL_STORAGE만 사용하여 암묵적으로 허가를 받으신 분들이 많으셨을겁니다.
하지만 이제부터는 WRITE와 READ 권한을 명시적으로 분리해서 선언하고 런타임에 각각 권한을 체크해야 합니다.

WRITE_EXTERNAL_STORAGE와 다르게 API 29 이상에서도 READ_EXTERNAL_STORAGE 권한은 계속해서 필요하고 런타임에도 체크해야 합니다.

본문 상단의 표를 참고하여 본인이 구현하려는 기능에 READ_EXTERNAL_STORAGE 권한이 필요한지 확인하세요.
필요하다면 아래와 같이 AndroidManifest.xml에 선언하세요.

<uses-permission 
android:name="android.permission.READ_EXTERNAL_STORAGE" />

그리고 런타임에 권한도 체크해야 합니다. 여기서는 가독성을 위해서 TedPermission 라이브러리를 사용했습니다.

TedPermission.create()
.setPermissions(Manifest.permission.READ_EXTERNAL_STORAGE)
.checkGranted()

API 28 이하의 기기에서는 WRITE_EXTERNAL_STORAGE 권한을 요청하고 허가받으면 READ_EXTERNAL_STORAGE 권한까지 암묵적으로 허가됩니다. 이 방식은 계속해서 유지할 수 있습니다.

지금까지 WRITE_EXTERNAL_STORAGE 권한이 필요한 경우 각 API별로 어떻게 처리해야하는지 살펴보았습니다.

조금 더 상세한 예시로 헤이딜러에서 이미지 저장기능과 다운로드 기능을 API 28 이하에서 그리고 API 29 이상에서 어떻게 구현했는지 소개해드리겠습니다.

이미지 저장하기

먼저, 이미지를 사진(Pictures) 디렉토리로 다운로드하는 기능을 구현해보겠습니다.

API 28 이하

Environment.getExternalStorageDirectory()를 활용하여 사진 디렉토리를 가져올 수 있습니다. 여기서는 Pictures/heydealer/ 디렉토리를 사용합니다.

이제 우리가 원하는 이미지의 Bitmap을 FileOutputStream을 통해 이미지 디렉토리에 넣어주기만 하면 됩니다.

API 29 이상

MediaStore를 활용하여 이미지를 저장할 수 있습니다. API 29 이상부터 RELATIVE_PATH를 넘겨서 사진 디렉토리의 하위에서 우리가 원하는 디렉토리에 저장할 수 있습니다.

전체 구현 코드는 아래와 같습니다.

위 HeyDealerBitmapSaver를 이용하여 이미지 URL을 통해 이미지로 저장하려면 아래와 같이 처리하면 됩니다. 여기서는 Glide를 활용하였습니다.

HeyDealerBitmapSaver가 동작하는 방식을 도식화해보면 아래와 같습니다.

파일 다운로드

마지막으로, 다운로드하고 싶은 파일의 URL을 통해 다운로드 디렉토리로 다운로드하는 방법에 대해 소개드립니다.

API에 상관없이 다음과 같이 DownloadManager를 통해 다운로드할 수 있습니다.

그리고 API 28 이하인 경우에만 위 코드를 실행하기 전에 런타임에 WRITE_EXTERNAL_STORAGE 권한을 허가받았는지 체크해주면 됩니다.

TedPermission.create()
.setPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.checkGranted()

전체 코드는 다음과 같습니다.

HeyDealerDownloader가 동작하는 방식도 BitmapSaver와 유사한 것을 볼 수 있습니다. 이는 우연의 일치가 아니라 WRITE_EXTERNAL_STORAGE가 API 29 이상부터 쓰이지 않기 때문입니다.

지금까지 저장소 권한에 대해 처리하는 방법과 예시로 이미지와 파일을 다운로드하는 방법에 대해서 알아보았습니다.
모든 경우에 대해서 소개드리진 못해서 아쉽지만 대부분의 경우 위와 같은 패턴에서 크게 벗어나지 않을겁니다.

예시 코드는 모두 GitHub Repository에서 확인하실 수 있습니다.

저희와 함께 헤이딜러 서비스를 발전 시켜나가실 분들을 기다리고 있습니다.

http://bit.ly/prnd-hiring

위의 채용링크로 많은 지원부탁드립니다.

감사합니다.

--

--