Android 앱 보안 강화 실전 가이드 — API Key 보호, Android Keystore, HTTPS, R8 난독화와 WebView 보안
Android 앱을 개발하다 보면 기능 구현과 화면 구성에 집중하기 쉽습니다.
하지만 Google Play에 앱을 배포한 이후에는 개발자가 예상하지 못한 보안 문제가 발생할 수 있습니다.
예를 들어 다음과 같은 상황입니다.
APK를 분석했더니 서버 API Key가 그대로 노출됨
SharedPreferences에 로그인 토큰이 평문으로 저장됨
WebView의 JavaScript 인터페이스를 외부 웹페이지에서 호출할 수 있음
HTTPS 인증서 검증을 비활성화해 중간자 공격에 취약해짐
R8 난독화 이후 Retrofit과 Gson 데이터 파싱이 실패함
앱에 포함된 SQLite 데이터베이스에서 민감한 정보가 추출됨
디버그 로그에 사용자 인증 토큰이나 개인정보가 기록됨
앱 백업 과정에서 보안 정보가 복원되어 오류가 발생함
이러한 문제는 대규모 서비스에서만 발생하는 것이 아닙니다.
광고를 사용하는 개인 개발 앱이나 단순한 정보 조회 앱에서도 API Key, 서버 통신, 사용자 데이터 저장 기능이 있다면 기본적인 보안 설계가 필요합니다.
이번 글에서는 Kotlin 기반 Android 프로젝트에서 우선적으로 적용할 수 있는 보안 방법을 정리하겠습니다.
작성 기준: 2026년 10월. AndroidX Security, Google Play 정책 및 Android 플랫폼 API는 변경될 수 있으므로 실제 배포 전 사용 중인 버전의 공식 문서를 확인해야 합니다.
1. Android 앱에서 보호해야 하는 정보
먼저 앱에서 어떤 정보를 보호해야 하는지 구분해야 합니다.
| 정보 | 예시 | 보호 방법 |
|---|---|---|
| 공개 가능한 설정 | API 기본 URL, 앱 버전 | 일반 설정으로 관리 가능 |
| 공개 API 식별자 | 일부 지도·서비스 API Key | 서비스 제공자의 앱 제한 설정 |
| 비밀 API Key | 서버 관리자 키, 결제 비밀 키 | 서버에 보관 |
| 인증 토큰 | Access Token, Refresh Token | 안전한 저장 및 수명 관리 |
| 사용자 데이터 | 메모, 개인정보 | 접근 통제 및 필요 시 암호화 |
| 암호화 키 | AES 키 | Android Keystore 활용 |
| 디버그 정보 | HTTP 로그, 오류 메시지 | 민감 정보 마스킹 |
가장 중요한 원칙은 다음과 같습니다.
앱에 포함된 정보는 공격자가 추출할 수 있다고 가정해야 합니다.
APK 파일은 사용자 기기에 설치되는 프로그램이기 때문입니다.
2. APK 내부에 API Key를 저장하면 안전할까?
Android 프로젝트에서는 API Key를 다음과 같이 사용하는 경우가 있습니다.
private const val API_KEY = "MY_SECRET_API_KEY"또는 strings.xml에 저장하기도 합니다.
<string name="api_key">MY_SECRET_API_KEY</string>하지만 이러한 값은 APK를 분석하면 추출될 수 있습니다.
BuildConfig에 저장해도 마찬가지입니다.
buildConfigField(
"String",
"API_KEY",
"\"MY_SECRET_API_KEY\""
)이 방법은 소스 코드에서 키를 분리하는 데 도움이 되지만, 배포된 APK에서 키를 숨기는 보안 기능은 아닙니다.
local.properties에 키를 보관한 뒤 빌드 과정에서 주입하더라도 최종 APK에 포함된다면 추출 가능성을 없앨 수 없습니다.
3. 비밀 API Key는 서버에서 관리하기
서버에서만 사용해야 하는 비밀 키는 Android 앱에 포함하지 않는 것이 가장 안전합니다.
예를 들어 다음과 같은 구조를 생각해 보겠습니다.
잘못된 구조
Android App
│
├── Secret API Key
│
▼
External API Server이 경우 앱을 분석하면 비밀 키가 노출될 수 있습니다.
대신 다음과 같이 구성할 수 있습니다.
권장 구조
Android App
│
│ HTTPS + 사용자 인증
▼
Spring Boot Server
│
├── Secret API Key
│
▼
External API ServerSpring Boot 서버에서 비밀 키를 보관하고 외부 API를 호출하는 방식입니다.
서버에서는 다음 사항을 추가로 관리해야 합니다.
사용자 인증 및 권한 확인
API 호출 횟수 제한
외부 API 응답 검증
요청 데이터 검증
비밀 키의 안전한 저장 및 교체
오류 및 감사 로그 관리
단순히 API 호출을 서버로 중계한다고 모든 보안 문제가 해결되는 것은 아닙니다.
서버가 인증 없이 모든 요청을 받아 외부 API를 호출한다면 악용될 수 있습니다.
4. 공개 API Key는 제한 설정을 활용하기
일부 Google API 등은 모바일 앱에 포함되는 API Key를 전제로 설계되어 있습니다.
이 경우에는 해당 서비스가 제공하는 제한 기능을 활용할 수 있습니다.
예를 들어 서비스가 지원한다면 다음 정보를 기준으로 제한할 수 있습니다.
Android 애플리케이션 패키지 이름
앱 서명 인증서의 SHA-1 지문
사용 가능한 API 종류
호출량 및 할당량
다만 모든 API가 이러한 제한 기능을 제공하는 것은 아닙니다.
또한 패키지 이름과 서명 제한은 비밀 키를 서버에 보관하는 것과 동일한 수준의 보호 수단이 아닙니다.
서비스 제공자가 Android 앱용 키를 공식적으로 지원하는지 먼저 확인해야 합니다.
5. SharedPreferences에 토큰을 저장할 때의 문제
Android에서는 간단한 설정을 저장하기 위해 SharedPreferences를 자주 사용합니다.
val preferences = getSharedPreferences(
"user_prefs",
Context.MODE_PRIVATE
)
preferences.edit()
.putString("access_token", token)
.apply()이 코드는 앱 전용 저장소에 데이터를 기록합니다.
일반적인 다른 앱이 자유롭게 읽을 수 있는 저장소는 아니지만, 토큰 자체가 암호화되는 것은 아닙니다.
기기가 손상되었거나 백업·디버깅·취약점을 통해 앱 데이터에 접근할 수 있는 상황에서는 민감한 정보가 노출될 위험이 있습니다.
따라서 민감한 인증 정보를 저장할 때는 별도의 보호 방법을 검토해야 합니다.
6. EncryptedSharedPreferences는 왜 주의해야 할까?
과거 Android 보안 저장소 예제에서는 EncryptedSharedPreferences를 많이 사용했습니다.
이 라이브러리는 AndroidX Security Crypto를 기반으로 SharedPreferences 값을 암호화하는 기능을 제공했습니다.
하지만 AndroidX Security Crypto의 관련 API는 deprecated 상태입니다.
따라서 신규 프로젝트에서 과거 예제를 그대로 복사하기보다 다음 사항을 검토하는 것이 좋습니다.
정말 토큰을 디스크에 저장해야 하는가?
Access Token을 메모리에서만 유지할 수 있는가?
Refresh Token을 안전하게 관리할 필요가 있는가?
Android Keystore를 이용한 암호화가 필요한가?
앱 백업과 복원 과정에서 암호화 키 문제가 발생하지 않는가?
특히 Android Keystore에 저장된 키는 일반적인 앱 데이터 백업으로 다른 기기에 그대로 이전되지 않습니다.
따라서 암호문만 복원되면 복호화에 실패할 수 있습니다.
7. Android Keystore란?
Android Keystore는 앱이 암호화 키를 생성하고 사용할 수 있도록 제공하는 Android 플랫폼 기능입니다.
대표적으로 AES 대칭키와 RSA·EC 비대칭키를 관리할 수 있습니다.
일부 기기에서는 하드웨어 기반 키 보호 기능을 사용할 수도 있습니다.
하지만 모든 기기의 모든 키가 하드웨어에 저장된다고 보장할 수는 없습니다.
하드웨어 기반 보호가 필요한 경우에는 키의 보안 수준을 별도로 확인해야 합니다.
Android Keystore의 중요한 특징은 암호화 키를 일반적인 앱 파일처럼 직접 읽어 가져오지 않고 암호화 연산에 사용할 수 있다는 것입니다.
8. Android Keystore에 AES 키 생성하기
다음은 AES-GCM 암호화에 사용할 키를 생성하는 예시입니다.
private const val KEY_ALIAS = "app_secure_key"
fun getOrCreateSecretKey(): SecretKey {
val keyStore = KeyStore.getInstance(
"AndroidKeyStore"
).apply {
load(null)
}
val existingKey = keyStore.getKey(
KEY_ALIAS,
null
) as? SecretKey
if (existingKey != null) {
return existingKey
}
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
"AndroidKeyStore"
)
val keyGenParameterSpec =
KeyGenParameterSpec.Builder(
KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT or
KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(
KeyProperties.BLOCK_MODE_GCM
)
.setEncryptionPaddings(
KeyProperties.ENCRYPTION_PADDING_NONE
)
.setKeySize(256)
.build()
keyGenerator.init(keyGenParameterSpec)
return keyGenerator.generateKey()
}필요한 주요 import는 다음과 같습니다.
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import java.security.KeyStore
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey이 코드는 Android Keystore에 AES 키가 없으면 생성하고, 이미 존재하면 재사용합니다.
실제 서비스에서는 키 생성 실패, 키 무효화, 저장소 오류도 처리해야 합니다.
9. AES-GCM으로 문자열 암호화하기
AES-GCM은 데이터의 기밀성과 무결성을 함께 보호하는 인증 암호화 방식입니다.
다음은 문자열을 암호화하는 예시입니다.
fun encrypt(
plainText: String,
secretKey: SecretKey
): String {
val cipher = Cipher.getInstance(
"AES/GCM/NoPadding"
)
cipher.init(
Cipher.ENCRYPT_MODE,
secretKey
)
val iv = cipher.iv
val encryptedBytes = cipher.doFinal(
plainText.toByteArray(Charsets.UTF_8)
)
val result = ByteArray(
iv.size + encryptedBytes.size
)
System.arraycopy(
iv,
0,
result,
0,
iv.size
)
System.arraycopy(
encryptedBytes,
0,
result,
iv.size,
encryptedBytes.size
)
return Base64.encodeToString(
result,
Base64.NO_WRAP
)
}필요한 import는 다음과 같습니다.
import android.util.Base64
import javax.crypto.Cipher이 예제에서는 IV와 암호문을 하나의 ByteArray로 결합한 뒤 Base64로 인코딩합니다.
Base64는 암호화 방식이 아닙니다. 암호화된 바이너리 데이터를 문자열로 저장하기 위한 인코딩입니다.
AES-GCM에서는 동일한 키로 암호화할 때 IV를 재사용하면 안 됩니다.
위 예제는 암호화 초기화 과정에서 생성된 IV를 사용합니다.
10. AES-GCM으로 문자열 복호화하기
암호화한 데이터를 복호화하려면 저장된 IV와 암호문을 분리해야 합니다.
fun decrypt(
encodedText: String,
secretKey: SecretKey
): String {
val combined = Base64.decode(
encodedText,
Base64.NO_WRAP
)
require(combined.size > 12) {
"Invalid encrypted data"
}
val iv = combined.copyOfRange(
0,
12
)
val encryptedBytes = combined.copyOfRange(
12,
combined.size
)
val cipher = Cipher.getInstance(
"AES/GCM/NoPadding"
)
val spec = GCMParameterSpec(
128,
iv
)
cipher.init(
Cipher.DECRYPT_MODE,
secretKey,
spec
)
val decryptedBytes = cipher.doFinal(
encryptedBytes
)
return String(
decryptedBytes,
Charsets.UTF_8
)
}추가 import는 다음과 같습니다.
import javax.crypto.spec.GCMParameterSpec이 예제는 암호화 함수에서 생성되는 일반적인 12바이트 IV를 전제로 합니다.
실제 프로젝트에서는 IV 길이와 데이터 포맷을 명시적으로 관리하고, 암호문 길이 및 버전 정보를 검증하는 것이 좋습니다.
암호문이 손상되거나 인증 태그 검증에 실패하면 복호화 예외가 발생할 수 있습니다.
이때 오류를 무시하고 임의의 데이터를 사용해서는 안 됩니다.
11. 암호화된 토큰 저장하기
앞에서 만든 암호화 함수를 이용하면 인증 토큰을 암호화한 뒤 저장할 수 있습니다.
fun saveToken(
context: Context,
token: String
) {
val secretKey = getOrCreateSecretKey()
val encryptedToken = encrypt(
token,
secretKey
)
context.getSharedPreferences(
"secure_prefs",
Context.MODE_PRIVATE
)
.edit()
.putString(
"access_token",
encryptedToken
)
.apply()
}읽을 때는 복호화합니다.
fun readToken(
context: Context
): String? {
val encryptedToken =
context.getSharedPreferences(
"secure_prefs",
Context.MODE_PRIVATE
)
.getString(
"access_token",
null
) ?: return null
return decrypt(
encryptedToken,
getOrCreateSecretKey()
)
}이 코드는 암호화 저장의 기본 구조를 설명하기 위한 것입니다.
실제 서비스에서는 다음 사항을 추가로 구현해야 합니다.
암호문 손상 처리
키 무효화 및 재생성 정책
토큰 만료 처리
로그아웃 시 저장 데이터 삭제
앱 백업 제외 설정
암호화 포맷 버전 관리
메인 스레드에서 무거운 저장소 작업 수행 방지
또한 로컬 저장소 암호화만으로 탈취된 인증 토큰의 악용을 완전히 막을 수는 없습니다.
서버에서 토큰 만료 시간, 폐기, 재발급 정책도 관리해야 합니다.
12. HTTPS를 반드시 사용해야 하는 이유
Android 앱과 서버 사이에서 인증 토큰이나 사용자 데이터를 주고받는다면 HTTPS를 사용하는 것이 기본입니다.
HTTPS는 TLS를 이용해 통신 내용을 보호하고 서버 인증서를 검증합니다.
Retrofit의 기본 URL도 HTTPS로 구성하는 것이 좋습니다.
Retrofit.Builder()
.baseUrl("https://api.example.com/")
.build()반대로 다음과 같은 HTTP 주소는 네트워크 구간에서 데이터가 노출되거나 변조될 위험이 있습니다.
http://api.example.com/단, HTTPS를 사용한다고 모든 공격을 막을 수 있는 것은 아닙니다.
서버의 인증 및 권한 검증, 입력 데이터 검증, 토큰 관리도 함께 필요합니다.
13. Network Security Config 설정하기
Android에서는 Network Security Config를 이용해 네트워크 보안 정책을 설정할 수 있습니다.
예를 들어 평문 HTTP 통신을 허용하지 않도록 구성할 수 있습니다.
res/xml/network_security_config.xml
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config
cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
</network-security-config>AndroidManifest.xml에서 설정합니다.
<application
android:networkSecurityConfig=
"@xml/network_security_config">
</application>이렇게 하면 Android의 Network Security Config를 따르는 네트워크 스택에서 평문 통신을 제한할 수 있습니다.
단, 모든 네이티브 라이브러리와 모든 네트워크 구현이 동일한 방식으로 이 설정을 준수하는 것은 아닙니다.
또한 앱에서 실제로 사용하는 모든 서버가 HTTPS를 지원하는지 확인해야 합니다.
14. 인증서 검증을 비활성화하면 안 되는 이유
개발 과정에서 자체 서명 인증서나 인증서 오류 때문에 다음과 같은 코드를 검색하게 되는 경우가 있습니다.
HostnameVerifier { _, _ -> true }이 코드는 호스트 이름 검증을 무조건 통과시킵니다.
또한 모든 인증서를 신뢰하는 TrustManager를 구현하는 예제도 인터넷에서 쉽게 찾을 수 있습니다.
하지만 이러한 방식은 중간자 공격에 취약해질 수 있으므로 실제 서비스에 적용해서는 안 됩니다.
인증서 오류가 발생한다면 다음 사항을 확인해야 합니다.
서버 인증서의 유효 기간
인증서 체인 구성
서버 호스트 이름과 인증서의 일치 여부
기기 날짜 및 시간
인증 기관의 신뢰 여부
개발용 인증서가 필요하다면 디버그 빌드에 한정된 신뢰 설정을 사용하는 방법을 검토할 수 있습니다.
15. Certificate Pinning은 반드시 필요한가?
OkHttp는 Certificate Pinning 기능을 제공합니다.
이 기능을 사용하면 특정 공개 키에 대한 핀을 설정할 수 있습니다.
하지만 Certificate Pinning은 모든 앱에 무조건 적용해야 하는 기능은 아닙니다.
인증서나 키가 변경되었을 때 앱이 서버에 연결하지 못하는 문제가 발생할 수 있기 때문입니다.
특히 개인 개발 앱에서는 인증서 갱신, 백업 핀, 장애 대응 계획을 충분히 검토해야 합니다.
일반적인 앱에서는 먼저 다음 사항을 우선 적용하는 것이 좋습니다.
HTTPS 사용
정상적인 인증서 검증 유지
최신 TLS 설정 사용
인증 토큰의 안전한 관리
서버 권한 검증 강화
Certificate Pinning은 운영상의 위험과 보안 요구사항을 함께 평가한 뒤 선택해야 합니다.
16. R8 난독화란?
R8은 Android 앱의 코드 축소, 최적화, 난독화에 사용되는 도구입니다.
릴리스 빌드에서 R8을 활성화하면 사용하지 않는 코드를 제거하고 클래스 및 메서드 이름을 변경할 수 있습니다.
예를 들어 다음과 같은 클래스가 있다고 가정하겠습니다.
class BirdRepository {
fun getBirdList() {
// ...
}
}난독화 이후에는 내부 이름이 짧게 변경될 수 있습니다.
하지만 R8 난독화는 암호화가 아닙니다.
APK 역공학을 어렵게 만드는 데 도움이 될 수 있지만, 비밀 API Key나 인증 정보를 안전하게 숨기는 수단으로 사용해서는 안 됩니다.
17. R8 활성화하기
app/build.gradle.kts의 릴리스 빌드 설정을 확인합니다.
android {
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile(
"proguard-android-optimize.txt"
),
"proguard-rules.pro"
)
}
}
}isMinifyEnabled는 코드 축소 및 난독화와 관련된 설정입니다.
isShrinkResources는 사용하지 않는 리소스를 제거하는 데 사용됩니다.
단, 실제 프로젝트의 Android Gradle Plugin 버전에 따라 설정 방식이나 기본 동작이 달라질 수 있습니다.
또한 R8 활성화 후에는 반드시 릴리스 빌드를 테스트해야 합니다.
디버그 빌드에서 정상 동작한다고 해서 릴리스 빌드에서도 모든 기능이 정상 동작한다고 보장할 수 없습니다.
18. Retrofit과 Gson에서 R8 오류가 발생하는 이유
Retrofit과 Gson은 리플렉션이나 어노테이션 정보를 활용할 수 있습니다.
따라서 R8이 필요한 타입 정보나 필드를 제거·변경하면 데이터 파싱에 문제가 발생할 수 있습니다.
예를 들어 다음과 같은 데이터 클래스가 있다고 가정하겠습니다.
data class BirdDto(
val id: Long,
val name: String,
val scientificName: String
)Gson은 JSON 필드 이름을 기준으로 데이터를 매핑할 수 있습니다.
하지만 난독화 이후 필드 이름이 변경되면 예상하지 못한 문제가 발생할 수 있습니다.
SerializedName 사용하기
data class BirdDto(
@SerializedName("id")
val id: Long,
@SerializedName("name")
val name: String,
@SerializedName("scientificName")
val scientificName: String
)이렇게 하면 JSON 필드 이름을 명시적으로 지정할 수 있습니다.
다만 @SerializedName만 추가한다고 모든 R8 문제가 해결되는 것은 아닙니다.
Retrofit, Gson, 사용하는 라이브러리 버전의 기본 R8 규칙과 필요한 keep 규칙을 확인해야 합니다.
특히 모든 클래스를 무조건 keep하는 방식은 코드 축소와 난독화의 효과를 크게 줄일 수 있습니다.
19. ProGuard 규칙은 최소한으로 관리하기
다음과 같은 규칙을 사용할 수 있습니다.
-keep class com.example.app.model.** { *; }이 규칙은 해당 패키지의 클래스와 멤버를 유지하도록 지정합니다.
하지만 너무 넓은 범위의 keep 규칙은 난독화 효과를 줄입니다.
따라서 실제로 필요한 모델이나 리플렉션 대상에만 적용하는 것이 좋습니다.
또한 최근 버전의 라이브러리는 자체 소비자 규칙을 제공하는 경우가 있습니다.
무조건 인터넷에서 오래된 ProGuard 설정을 복사하기보다 현재 사용 중인 라이브러리 문서를 먼저 확인해야 합니다.
20. WebView JavaScript 인터페이스 보안
WebView에서 JavaScript와 Android 코드를 연결할 때는 addJavascriptInterface()를 사용할 수 있습니다.
webView.addJavascriptInterface(
AndroidBridge(),
"Android"
)이 기능은 편리하지만 보안상 주의가 필요합니다.
JavaScript 인터페이스가 노출된 웹페이지에서는 해당 객체의 공개된 인터페이스 메서드를 호출할 수 있기 때문입니다.
특히 신뢰할 수 없는 페이지나 iframe이 포함된 페이지에서는 위험이 커질 수 있습니다.
다음과 같은 기능을 JavaScript 인터페이스에 직접 노출하는 것은 주의해야 합니다.
사용자 인증 토큰 반환
파일 삭제
민감한 설정 변경
임의의 URL 실행
외부 명령 실행
개인정보 조회
가능하면 JavaScript 인터페이스의 기능을 최소화하고, 신뢰할 수 있는 콘텐츠에만 연결해야 합니다.
21. WebView에서 허용된 도메인만 로딩하기
WebView가 특정 서비스의 페이지를 표시하는 용도라면 허용된 도메인만 로딩하도록 제한할 수 있습니다.
private fun isAllowedUrl(url: String): Boolean {
val uri = Uri.parse(url)
return uri.scheme == "https" &&
uri.host == "example.com"
}WebViewClient에서 다음과 같이 사용할 수 있습니다.
webView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView,
request: WebResourceRequest
): Boolean {
val url = request.url.toString()
return !isAllowedUrl(url)
}
}이 코드는 메인 프레임 탐색에 대한 기본적인 URL 제한 예시입니다.
실제 프로젝트에서는 리다이렉션, 서브도메인, iframe, 외부 앱 스킴, 파일 URL 등을 별도로 검토해야 합니다.
특히 example.com.evil.com과 같은 도메인을 허용하지 않도록 문자열 포함 검사 대신 정확한 호스트 비교를 사용하는 것이 중요합니다.
또한 shouldOverrideUrlLoading()만으로 모든 WebView 리소스 요청을 통제할 수 있는 것은 아닙니다.
22. WebView 디버깅 기능 관리
개발 중에는 Chrome DevTools를 이용해 WebView를 디버깅할 수 있습니다.
WebView.setWebContentsDebuggingEnabled(true)하지만 릴리스 앱에서 디버깅 기능을 불필요하게 활성화하지 않는 것이 좋습니다.
예를 들어 다음과 같이 설정할 수 있습니다.
WebView.setWebContentsDebuggingEnabled(
BuildConfig.DEBUG
)단, WebView 디버깅 기능의 기본 동작은 Android 및 WebView 버전에 따라 달라질 수 있습니다.
따라서 실제 릴리스 빌드에서 디버깅 가능 여부를 확인하는 것이 좋습니다.
23. SQLite 데이터베이스 보안
Android 앱에 포함된 SQLite 데이터베이스는 일반적으로 앱 전용 저장소에 위치합니다.
하지만 앱 데이터에 접근할 수 있는 환경에서는 DB 파일이 추출될 수 있습니다.
따라서 SQLite에 다음과 같은 정보를 평문으로 저장하는 것은 신중하게 검토해야 합니다.
사용자 비밀번호
장기 인증 토큰
금융 관련 민감 정보
보호가 필요한 개인정보
일반적인 도감 데이터나 공개 콘텐츠는 반드시 암호화해야 하는 것은 아닙니다.
반면 민감한 사용자 데이터를 저장한다면 데이터 최소 수집, 접근 통제, 암호화, 백업 정책 등을 함께 고려해야 합니다.
SQLite 자체는 기본적으로 데이터베이스 파일 전체를 자동 암호화하지 않습니다.
DB 전체 암호화가 필요하다면 별도의 암호화 솔루션과 키 관리 구조를 검토해야 합니다.
24. Android 백업 정책 확인하기
Android 앱 데이터는 백업 정책에 따라 백업 및 복원될 수 있습니다.
하지만 Android Keystore에 저장된 키는 일반적인 백업으로 다른 기기에 그대로 이전되지 않습니다.
따라서 암호화된 토큰 파일만 백업되면 새 기기에서 복호화할 수 없는 문제가 발생할 수 있습니다.
이러한 문제를 방지하려면 민감한 인증 정보나 복호화할 수 없는 암호문을 백업 대상에서 제외하는 방법을 고려해야 합니다.
Android 12 이상에서는 dataExtractionRules를 이용한 백업 및 기기 간 전송 규칙을 확인해야 합니다.
이전 버전에서는 fullBackupContent 설정도 고려해야 합니다.
백업 규칙은 Android 버전과 전송 방식에 따라 동작이 다를 수 있으므로 실제 기기에서 복원 테스트를 수행하는 것이 좋습니다.
25. 로그에 민감한 정보를 남기지 않기
개발 중 다음과 같은 코드를 작성하는 경우가 있습니다.
Log.d("Auth", "Token: $accessToken")하지만 인증 토큰을 로그에 기록하는 것은 피해야 합니다.
또한 Retrofit과 OkHttp의 BODY 로깅을 활성화하면 다음과 같은 정보가 로그에 포함될 수 있습니다.
Authorization 헤더
세션 쿠키
사용자 입력 데이터
개인정보
서버 응답의 민감한 필드
개발 환경에서도 실제 사용자 데이터를 사용하는 경우에는 로그를 최소화해야 합니다.
릴리스 빌드에서는 불필요한 디버그 로그가 남지 않도록 점검하는 것이 좋습니다.
26. Google Play 배포 전 보안 점검
앱을 Google Play에 배포하기 전에는 다음 사항을 확인하는 것이 좋습니다.
APK에 서버 비밀 API Key가 포함되지 않았는지 확인
공개 API Key의 사용 제한 설정 확인
인증 토큰을 평문으로 저장하지 않는지 검토
Android Keystore 키 관리 정책 확인
HTTPS 사용 및 인증서 검증 유지
불필요한 HTTP 평문 통신 제한
R8 활성화 후 릴리스 빌드 테스트
Retrofit과 Gson 파싱 동작 확인
WebView JavaScript 인터페이스 최소화
WebView 허용 도메인 정책 확인
WebView 디버깅 설정 확인
SQLite 민감 정보 저장 여부 확인
앱 백업 및 복원 정책 확인
로그에 토큰과 개인정보가 남지 않는지 확인
Google Play Data Safety 신고 내용과 실제 데이터 처리 방식 일치 여부 확인
27. 보안 기능 검증 방법
보안 기능은 코드만 작성했다고 완료되는 것이 아닙니다.
실제 릴리스 빌드에서 검증해야 합니다.
API Key 확인
APK 분석 도구를 이용해 문자열과 리소스에 비밀 키가 포함되어 있는지 확인합니다.
R8 확인
릴리스 APK 또는 AAB로 앱을 실행하고 Retrofit, Gson, WebView 기능이 정상적으로 동작하는지 확인합니다.
토큰 저장 확인
앱 전용 저장소에 인증 토큰이 평문으로 기록되지 않는지 확인합니다.
HTTPS 확인
HTTP 연결이 불필요하게 허용되어 있지 않은지, 인증서 검증이 정상적으로 동작하는지 확인합니다.
WebView 확인
허용되지 않은 도메인에서 민감한 JavaScript 인터페이스를 호출할 수 없는지 검토합니다.
백업 및 복원 확인
앱을 재설치하거나 다른 기기로 데이터를 복원했을 때 암호화 저장소가 안전하게 처리되는지 확인합니다.
28. 마무리
Android 앱 보안은 단순히 R8 난독화를 활성화하거나 API Key를 문자열 리소스로 이동하는 것으로 완성되지 않습니다.
앱에 포함된 데이터는 추출될 수 있다는 전제에서 설계해야 합니다.
실무에서는 다음 원칙을 우선 적용하는 것이 좋습니다.
서버 비밀 API Key는 Android 앱에 포함하지 않습니다.
인증 토큰은 필요 최소한으로 저장하고 안전하게 관리합니다.
Android Keystore를 활용하되 키 백업과 복원 문제를 함께 고려합니다.
HTTPS 인증서 검증을 비활성화하지 않습니다.
R8 난독화는 보안의 보조 수단으로 활용합니다.
WebView JavaScript 인터페이스는 최소한의 기능만 노출합니다.
릴리스 빌드에서 실제 보안 동작을 검증합니다.
특히 개인 개발 앱에서는 모든 보안 기능을 한 번에 구현하기보다 위험도가 높은 부분부터 개선하는 것이 현실적입니다.
가장 먼저 비밀 API Key 노출 여부, 인증 토큰 저장 방식, HTTPS 설정, WebView 인터페이스를 점검하는 것을 추천합니다.
Android 앱 보안의 핵심은 공격을 완벽하게 막는다고 가정하는 것이 아니라, 민감한 정보의 노출 가능성을 줄이고 서버와 클라이언트의 책임을 명확하게 분리하는 것입니다.
참고 자료
Android Developers — 앱 보안 개요
https://developer.android.com/privacy-and-security/securityAndroid Developers — Android Keystore
https://developer.android.com/privacy-and-security/keystoreAndroid Developers — Network Security Configuration
https://developer.android.com/privacy-and-security/security-configAndroid Developers — 앱 축소 및 난독화
https://developer.android.com/build/shrink-codeAndroid Developers — WebView 보안
https://developer.android.com/privacy-and-security/risks/insecure-webview-native-bridgesAndroid Developers — 데이터 백업
https://developer.android.com/identity/data/autobackupAndroidX Security Crypto
https://developer.android.com/jetpack/androidx/releases/securityOWASP Mobile Application Security
https://mas.owasp.org/
태그: Android, Kotlin, Android Security, API Key, Android Keystore, AES-GCM, EncryptedSharedPreferences, R8, ProGuard, Retrofit, OkHttp, HTTPS, WebView 보안, SQLite 보안, Google Play