권한·보안 — 런타임 권한·저장소·암호화·네트워크
왜 권한이라는 게 있는가
휴대폰에는 사람의 가장 민감한 것이 다 들어 있다 — 위치, 연락처, 카메라, 마이크, 사진, 메시지. 앱이 이걸 마음대로 가져갈 수 있다면 아무도 앱을 믿고 설치하지 못한다. 그래서 안드로이드는 기기 기능과 사용자 데이터를 권한(permission)이라는 문으로 잠그고, 그 문을 여는 열쇠를 사용자에게 준다 — 앱이 카메라를 쓰려면 사용자가 직접 "허용"을 눌러야 한다. 권한은 앱과 사용자 사이의 신뢰 계약이고, 보안은 그 계약을 지키는 기술적 뒷받침이다. 이 장은 권한 요청·저장소 접근·민감 데이터 암호화·네트워크 보안을 다룬다. (출처: Android — Permissions / Security best practices.)
권한 — normal vs dangerous
권한은 위험도에 따라 둘로 나뉜다. 일반(normal) 권한(인터넷 접근 등)은 위험이 낮아 — 매니페스트에 선언만 하면 설치 시 자동 부여된다(사용자에게 묻지 않음). 반면 위험(dangerous) 권한(카메라·위치·연락처·마이크)은 사용자 프라이버시에 직결되므로 — 앱이 실제로 쓰는 순간 런타임에 사용자에게 명시적으로 물어야 한다.
런타임 권한 요청은 세 갈래 분기로 처리한다 — 이 흐름을 정확히 짜는 게 핵심이다.
// 권한 결과를 받을 launcher 등록 (Activity Result API)
val launcher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
if (granted) openCamera() else showDeniedUi() // 거부돼도 크래시 없이 기능 저하
}
when {
// ① 이미 허용 — 바로 사용
checkSelfPermission(CAMERA) == PERMISSION_GRANTED -> openCamera()
// ② 전에 거부한 적 있음 — "왜 필요한지" 먼저 설명하고 다시 요청
shouldShowRequestPermissionRationale(CAMERA) -> showRationale { launcher.launch(CAMERA) }
// ③ 처음 요청 — 시스템 다이얼로그
else -> launcher.launch(Manifest.permission.CAMERA)
}
shouldShowRequestPermissionRationale가 *true면 "전에 한 번 거부했다"*는 신호다 — 이때 왜 이 권한이 필요한지 사용자에게 설명한 뒤 다시 요청해야 동의를 얻기 쉽다. 사용자가 "다시 묻지 않음"으로 영구 거부하면 시스템 다이얼로그가 더는 안 뜨므로 설정 화면으로 안내한다.
관통하는 원칙은 최소 권한(least privilege) — 꼭 필요한 권한만, 필요한 순간에 요청한다. 그리고 거부를 우아하게 다룬다(권한이 없으면 그 기능만 빠지고 앱은 정상 — 크래시 금지). 더 나아가 권한이 아예 필요 없는 대안을 우선한다 — 예컨대 사진을 고를 때 저장소 권한 대신 권한 없는 Photo Picker를 쓰는 식이다.
저장소 — Scoped Storage
예전 안드로이드는 앱이 기기의 파일 시스템을 거의 자유롭게 돌아다녔다 — 그래서 악성 앱이 다른 앱의 데이터를 훔치거나, 앱을 지워도 쓰레기 파일이 남는 문제가 컸다. 현대 안드로이드는 **범위 지정 저장소(scoped storage)**로 이를 막는다 — 각 앱은 자기 전용 디렉터리만 자유롭게 쓰고, 그 밖은 통제된 통로로만 접근한다. 공유 미디어는 MediaStore나 권한 없는 Photo Picker로, 문서는 *Storage Access Framework(SAF)*로 — 결과적으로 광범위한 저장소 권한이 거의 불필요해졌다. "내 앱 데이터는 내 전용 공간에, 공유할 것만 통제된 통로로"가 사고법이다.
민감 데이터 저장 — 왜 평문은 위험한가
로그인 토큰·개인정보를 평문 SharedPreferences나 파일에 두면 — 루팅된 기기·백업·물리적 탈취로 그대로 노출된다. 핵심 도구는 Android Keystore다 — 암호화 키를 하드웨어 보안 영역(TEE/StrongBox)에 보관해, 앱조차 키 원문을 못 보게 한다. 그래서 디스크를 통째로 훔쳐도 키가 없어 복호화 불가다.
- 민감 값은 Keystore 기반 암호화로 저장한다(Jetpack Security의 EncryptedSharedPreferences 등 — 일부 API는 변천이 있으니 현재 권장 라이브러리를 확인).
- 결제·로그인 같은 민감 작업은 **생체 인증(BiometricPrompt)**으로 한 겹 더 보호한다.
네트워크 보안
데이터는 저장될 때만이 아니라 오갈 때도 노출될 수 있다(공용 와이파이의 도청).
- HTTPS 강제 — 현대 안드로이드는 평문 HTTP를 기본 차단(cleartext 비허용)한다. 도메인별 예외·정책은 Network Security Config로 선언한다.
- 인증서 고정(certificate pinning) — 신뢰할 인증서를 앱에 박아 두어, 중간자가 가짜 인증서로 가로채는 MITM 공격을 막는다. 단 핀이 만료되면 앱이 서버에 못 붙으니 만료 관리가 중요하다.
- 클라이언트 검증만 믿지 않는다 — 보안 검증의 최종 권위는 서버다(클라이언트 코드는 역공학으로 우회될 수 있다).
코드·비밀 보호
- 비밀(secret)을 코드에 하드코딩 금지 — API 키·토큰을 코드에 박으면 APK를 역공학해 추출된다. 서버 측에 두거나 안전한 채널로 받는다.
- R8 난독화([11])로 코드 분석을 어렵게 한다(완전한 보호는 아니지만 분석 비용을 올린다).
- 컴포넌트 노출 최소화 — 매니페스트의
exported=false로 불필요한 외부 접근을 차단하고, ContentProvider·딥링크로 들어오는 입력은 반드시 검증한다(외부가 보내는 값은 신뢰하지 않는다).
정리 — 최소 권한·암호화·검증
보안의 핵심 — 위험 권한은 런타임에 최소·근거와 함께 요청(거부돼도 기능 저하지 크래시 금지), scoped storage로 저장소 접근 최소화(Photo Picker/SAF), 민감 데이터는 Keystore 기반 암호화·전송은 HTTPS(+ 필요 시 인증서 고정), 비밀 하드코딩 금지·exported=false·외부 입력 검증. 모든 결정의 기준은 *"꼭 필요한 만큼만, 안전하게"*이고, 최종 신뢰는 서버에 둔다.
한 줄 요약 — 권한=사용자와의 신뢰 계약. normal(자동) vs dangerous(런타임 요청·
registerForActivityResult·shouldShowRationale 분기)·최소 권한·거부 우아하게. 저장소: scoped storage(전용 공간+Photo Picker/SAF). 민감 데이터: Keystore(하드웨어 키)·EncryptedSharedPreferences·BiometricPrompt. 네트워크: HTTPS 강제·Network Security Config·인증서 고정. 비밀 하드코딩 금지·exported=false·외부 입력 검증·최종 검증은 서버.
(출처: Android — Permissions / App security best practices / Data and file storage / Photo Picker / Security guidelines.)