티스토리 뷰

안녕하세요 간만에 포스팅입니다. 오늘은 현재 내 위치를 받아오는 예제 앱을 통해 Permission에 대해 알아보겠습니다.

 

#1> Android Permission 누구니 너?


2007년 스티브잡스가 아이폰을 발표한지 어느덧 13년이 지났습니다. 스마트폰 시장의 태동기 때에는 하드웨어와 소프트웨어 기술 발전에 모든 포커스가 쏠려있었지만 혁신이라 불릴만한 기술들이 대부분 나온 현재에는 수많은 정보들이 저장되있는 스마트폰의 보안에 눈길에 쏠려있습니다. 이런 보안에 대해 안드로이드는 어떤 정책을 가지고 있을까요?

 

초창기 Android OS(Api 22 이하)에서는 앱 개발 시 manifest.xml 파일에 앱에서 필요한 권한을 명시하도록 했고, 여기에 리스트업된 권한들은 사용자가 앱 설치 시 한번만 보여주었습니다.

 

하지만 실 사용자들은 그 권한이 무엇을 의미하는지도 잘 모르는 경우가 많아서 권한에 대한 설명을 읽어보지도 않고 그냥 설치하는 경우가 많았습니다. 이로인해 악의적으로 배포된 앱이 사용자 통화기록부터 각종 민감한 개인정보들까지 모두 자신의 서버로 전송해 유출된 개인정보로 인해 피해를 보는 사람들이 하나 둘 생겨났습니다.

 

출처: https://funnyfrog.tistory.com/47

위 사진은 출처를 밝힌 곳에서 악성 앱으로 추정되는 앱이 요구하는 권한을 올린 사진입니다. 관련 권한이 앱 기능을 실행하는데 필요하면 모르겠지만, 단순히 플래시를 껐다 켰다 하는 앱에 저런 권한이 있다면 조금 의심스럽죠? 당시에는 그런 앱들이 많았습니다.

 

이런 맹점을 해결하고자 구글은 2015년 Android API 23(마시멜로우) 출시와 함께 api23 이상의 안드로이드 스마트폰의 보안 정책을 강화하여 manifest.xml 파일에 앱에서 접근할 권한들을 명시해놓았어도, 앱이 실행된 뒤 사용자에게 직접 승인을 받아야 해당 권한과 관련된 리소스에 접근할 수 있도록 보안 정책을 변경하였습니다.

 

매번 출시되는 버전마다 강력해진 보안 을 언급하고 있으며, 조만간 또 커다란 변화가 있을 것이라 생각됩니다. 이런 시점에서 현재 안드로이드의 권한관련 정책과 활용 방법을 잘 모르시는 분들을 위해 이번 포스팅을 준비했습니다.

 

그럼 저와함께 permission에 대해 알아볼까요?

 

 

#2> Android Permission 파해치기


권한(Permission)의 정의

구글 개발자 레퍼런스 문서에는 아래와 같이 permission에 대해 설명하고 있습니다.

The purpose of a permission is to protect the privacy of an Android user. Android apps must request permission to access sensitive user data (such as contacts and SMS), as well as certain system features (such as camera and internet). Depending on the feature, the system might grant the permission automatically or might prompt the user to approve the request.

요약해보면, "권한이란 사용자의 개인정보 보호를 위해 사용되며 그 권한에 따라 시스템이 자동으로 부여하기도 하고 사용자에게 요청을 승인받아야 한다." 고 정리할 수 있겠습니다.

 

권한에 따라 자동으로 분류하거나 승인을 받아야한다는데, 그 기준은 무엇일까요? 바로 Protection level. 즉, 보호수준 입니다.

 

권한의 보호수준(Protection level)

앱 개발을 한번이라도 해보셨다면 manifest 파일에서 이런 코드를 많이 작성하셨을 겁니다.

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

위 코드 스니펫에서는 앱에서 인터넷 권한과 사용자 위치정보 권한을 사용하겠다고 명시하고 있는데, 이런 코드가 application의 전반적인 정보를 담고있는 manifest.xml 파일에 명시되었다면 앱 실행 시 어떤 권한을 사용자에게 승인받아야 할까요?

 

결론부터 말씀드리면 INTERNET 권한은 사용자에게 따로 승인받을 필요가 없습니다. 왜냐하면 Protection level이 normal이기 때문입니다. 보호니 수준이니 무슨말인지 이해가 안되시죠? 아래 내용을 보시면 이해하실 수 있습니다.

 

protection level은 permission의 속성 중 하나로서 아래와 같이 정의되어 있습니다.

권한에 내포된 잠재적 위험을 특성화하고 권한을 요청하는 애플리케이션에 권한을 부여할지 여부를 결정할 때 시스템에서 따라야 하는 절차를 나타냅니다.

출처: https://developer.android.com/guide/topics/manifest/permission-element

여기서 signature는 이미 권한을 승인받은 앱이 업데이트 되는 경우를 생각하시면 좋을 것 같습니다.

위 표를 참고해서 다시 인터넷 권한과 사용자 위치정보 권한에 대해 생각해보겠습니다. 인터넷 권한은 시스템이 자동으로 부여해준다고 말씀드렸습니다. 사용자 위치정보 권한은 사용자에게 직접 승인을 얻어야 한다고 했으니 그녀석의 protection level은? 당연히 dangerous입니다.

 

출처: https://developer.android.com/reference/android/Manifest.permission?hl=ko#ACCESS_FINE_LOCATION

 

권한별 보호수준을 확인하고 싶으시면 아래 링크를 참고하세요.

https://developer.android.com/reference/android/Manifest.permission?hl=ko

 

지금까지 권한이 무엇이고, 보호 수준에 따라 시스템이 권한을 주기도 하고 사용자에게 직접 승인받아야 한다는 사실까지 알아봤습니다.

 

그럼 protectoin level이 dangerous인 permission은 어떻게 사용자에게 승인받을 수 있을까요? 같이 살펴보시죠.

 

 

#3> 사용자에게 권한을 승인받는 방법 (api 23 이상)


api23 이상이라고 소제목에 명시한 이유는 #1 섹션에서 말씀드린 것 같이 실시간 권한 승인 정책은 Marshmallow 버전부터 적용되었기 때문입니다. release date가 벌써 5년 전이니.. 최신 폰에서 실행되는 앱을 개발할 예정이라면 반드시 runtime 시 사용자에게 dangerous 보호수준을 가진 권한을 승인받아야 합니다.

 

어떻게 앱 실행 시 사용자에게 권한을 요청할 수 있을까요? 위치 권한을 받아 현재 위도와 경도를 TextView에 표시하는 예제 앱을 통해 같이 살펴보겠습니다.

 

권한과 관련된 3가지 시나리오

https://www.youtube.com/watch?v=C8lUdPVSzDk&feature=youtu.be

위 영상에서 조에나가 설명해주는 것과 같이 개발자는 아래 3가지 상황에 대해 고려해야 합니다.

  1. 최초로 권한을 요청하는 경우
  2. 거절당한 권한을 다시 요청하는 경우
  3. 거절과 동시에 해당 권한요청을 다시 표시하지 않음 옵션을 선택한 경우

각 상황 별 올바른 처리 방법에 대해 살펴보도록 하겠습니다.

 

최초로 권한을 요청하는 경우

우선 주의해야 할 점은 모든 권한을 한번에 다 받는 상황은 피하는게 좋다는 것입니다. 사용자 입장에서 아직 아무것도 하지 않았는데 여러 권한을 요청하면 의심스러워서 deny 하는 경우가 발생할 수 있으니 해당 권한이 필요한 경우에 그 권한을 요청하는게 좋겠죠?

 

최초로 권한을 요청하는 시점은 사용자가 앱을 처음 설치하고 권한과 관련된 기능을 사용하려고 할 때입니다. 예제 앱 같은 경우는 설치 후 앱 위치를 가져오려고 할 때 권한을 묻습니다.

 

재미있는 점은 사용자가 언제든 설정에 가서 권한을 해제할 수 있기 때문에 늘 체크를 해줘야 한다는 것입니다. 아래는 사용자에게 권한을 요청하는 코드입니다.

const val REQUEST_CODE = 1
...
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), REQUEST_CODE)

위 코드가 실행되면 위와같이 권한을 묻는 dialog가 생성되며 사용자의 action에 따른 분기 처리가 가능합니다.

override fun onRequestPermissionsResult(
            requestCode: Int,
            permissions: Array<out String>,
            grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == REQUEST_CODE) {
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                getCurrentLocation()
            } else {
                Toast.makeText(this, R.string.no_permission_msg, Toast.LENGTH_SHORT).show()
            }
        }
    }

예제에서는 사용자의 선택에 따라 아래와 같은 parameter가 onRequestPermissionsResult 함수로 전달됩니다.

  1. 권한을 승인한 경우

    • requestCode = 1
    • permission = ACCESS_FINE_LOCATION
    • grantResults = PERMISSION_GRANTED
    • action: 위치를 가져오는 코드를 실행
  2. 권한을 거절한 경우

    • requestCode = 1
    • permission = ACCESS_FINE_LOCATION
    • grantResults = PERMISSION_DENIED
    • action: 권한이 없어 기능을 사용할 수 없음을 알림

권한을 승인한 경우
권한을 거절한 경우

 

거절당한 권한을 다시 요청하는 경우

아래 코드를 통해 현재 앱에 특정 권한이 승인되었는지 확인할 수 있습니다.

if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
	// 권한이 거절된 상태
} else {
	// 권한이 승인된 상태
}

조금 더 세분화 해보면 아래와 같이 4가지로 나눌 수 있습니다.

if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
	// 권한이 거절된 상태
	if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.ACCESS_FINE_LOCATION)) {
		// 1. 사용자가 승인 거절을 누른경우
	} else {
		// 2. 사용자가 승인 거절과 동시에 다시 표시하지 않기 옵션을 선택한 경우
		// 3. 혹은 아직 승인요청을 한적이 없는 경우
	}
} else {
	// 4. 권한이 승인된 상태
}

먼저 살펴봤던 최초로 승인요청을 하는 경우는 3번 case에 해당합니다. 2번 case는 조금 뒤에 설명하기로 하고 1번 케이스부터 살펴보시죠. 권한을 이미 거절당한 상태에서 다시 권한을 요청하는 경우에는 왜 이 권한이 필요한지 사용자에게 설명을 해주는게 좋습니다. 아니면 거절하지 못할 제안을 하거나

 

사용자(좌)를 설득하는 개발자(우)의 모습.jpg
권한을 획득한 개발자의 모습.jpg

이번 예제에서는 SnackBar를 사용해 사용자에게 추가 정보를 전달했습니다. 아래처럼요.

 

Toast를 통해 인스턴스 메세지를 띄워줘도 되지만 SnackBar를 활용하는게 보다 직관적입니다. 혹시 처음 권한을 요청할 때와 다른 점을 발견하셨나요? 네 맞습니다. 3번째 선택 항목에 Deny & don't ask again 이라는 선택지가 추가되었습니다. 친절한 안드로이드 OS는 한번 거절한 권한에 대해 다시 권한을 요청하는 경우 사용자 편의를 위해 다시 표시하지 않기라는 옵션을 추가로 제공합니다. 기능과 관련된 권한이라면 저걸 누르지 않겠지만 수상한 권한을 계속 요청하는 경우 사용자 입장에서는 좋은 옵션이 될 수 있습니다.

 

하지만 사용자가 실수로 저 옵션을 선택하면 사용자에게 메세지 혹은 전화로 앱 정보 화면에 들어가서 권한을 수동으로 승인하는 방법에 대해 구구절절 설명해야 합니다. 그런 일을 방지하기위해 우리는 아래와 같은 코드를 구현할 수 있습니다.

 

권한 요청 거절과 해당 권한 요청을 다시 표시하지 않음을 선택한 경우

이런 상태라면 권한요청 메서드를 아무리 호출해도 승인을 요청하는 다이어로그가 나타나지 않습니다. 결국 사용자가 앱 정보 화면으로 이동해서 수동으로 권한을 승인해줘야 합니다. 여기서 개발자가 어느정도 개입을 할 수 있는데, 바로 내 앱의 설정화면을 화면에 띄워주는 것입니다.

// 사용자가 권한을 거부하면서 다시 묻지않음 옵션을 선택한 경우
// requestPermission을 요청해도 창이 나타나지 않기 때문에 설정창으로 이동한다.
val snackBar = Snackbar.make(layout, R.string.suggest_permissison_grant_in_setting, Snackbar.LENGTH_INDEFINITE)
snackBar.setAction("확인") {
    val intent = Intent()
    intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
    val uri = Uri.fromParts("package", packageName, null)
    intent.data = uri
    startActivity(intent)
}
snackBar.show()

위 코드 스니펫은 스낵바 생성과 action 버튼을 클릭 시 동작하는 로직을 나타내고 있습니다. 위 코드가 실행되면 아래와 같이 동작합니다.

ScankBar의 확인 (action) 버튼을 클릭하면 앱의 설정 화면으로 이동하는 모습을 확인할 수 있습니다. 여기서 수동으로 권한을 설정해주고 back 버튼을 누르면 다시 앱으로 돌아와 원하는 기능을 사용할 수 있습니다. 아래는 위에서 설명드린 3가지 상황에 대한 처리가 모두 구현된 권한체크 메서드의 코드입니다.

private fun isLocationPermissionGranted(): Boolean {
        val preference = getPreferences(Context.MODE_PRIVATE)
        val isFirstCheck = preference.getBoolean("isFirstPermissionCheck", true)
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
            if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.ACCESS_FINE_LOCATION)) {
                // 거부만 한 경우 사용자에게 왜 필요한지 이유를 설명해주는게 좋다
                val snackBar = Snackbar.make(layout, R.string.suggest_permissison_grant, Snackbar.LENGTH_INDEFINITE)
                snackBar.setAction("권한승인") {
                    ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), REQUEST_CODE)
                }
                snackBar.show()
            } else {
                if (isFirstCheck) {
                    // 처음 물었는지 여부를 저장
                    preference.edit().putBoolean("isFirstPermissionCheck", false).apply()
                    // 권한요청
                    ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), REQUEST_CODE)
                } else {
                    // 사용자가 권한을 거부하면서 다시 묻지않음 옵션을 선택한 경우
                    // requestPermission을 요청해도 창이 나타나지 않기 때문에 설정창으로 이동한다.
                    val snackBar = Snackbar.make(layout, R.string.suggest_permissison_grant_in_setting, Snackbar.LENGTH_INDEFINITE)
                    snackBar.setAction("확인") {
                        val intent = Intent()
                        intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
                        val uri = Uri.fromParts("package", packageName, null)
                        intent.data = uri
                        startActivity(intent)
                    }
                    snackBar.show()
                }
            }
            return false
        } else {
            return true
        }
    }

 

 

#4> 마치며


오늘은 쉬우면서도 복잡한 권한에 관련해서 파해쳐 보았습니다. 권한과 관련해 편리한 라이브러리들이 많이 나와있지만 이런 기본적인 기능들은 스스로 구현하는 편이 유지보수 측면에서도 좋다는 생각입니다.

 

긴 글 끝까지 읽어주셔서 감사합니다. 좋은하루 되세요.

 

광고 클릭, 하트, 댓글은 필자에게 큰 힘이 됩니다!

 


예제 소스코드 주소 : https://github.com/manorgass/tistory/tree/master/android/PermissionTest

댓글