๐ง ๋ค์ด๊ฐ๋ฉฐ
์๋๋ก์ด๋์์ ๋จ์ํ ๋ฐ์ดํฐ์ ์ ๋ก์ปฌ์ ์ ์ฅํ๊ธฐ ์ํด์ ํํ SharedPreferences๋ฅผ ์ฌ์ฉํ๋ค.
์๋๋ก์ด๋์ ๊ธฐ๋ณธ์ ์ผ๋ก ๋ด์ฅ๋์ด ์๋(=Since api level 1) ๊ฒ์ผ๋ก ํ์๋ ์์ฃผ ์ฌ์ฉํ๋ค.
ํ์ง๋ง Primitive data๋ง ์ ์ฅ ๊ฐ๋ฅํ์ผ๋ฉฐ, ์ปค์คํ ๋ฐ์ดํฐ ํ์ ์ ์ฅ์ ์ํด GSON์ ํตํด Json String์ผ๋ก ๋ณํํ์ฌ ์ ์ฅํ๋ ์ฝ๋๋ค์ด ์๊ฒจ๋ฌ๋ค. ๊ทธ๋ฆฌ๊ณ ๋ฏธ์ํ ์ฒ๋ฆฌ์ ์ข ์ข ๋ฐํ์ ์๋ฌ, ANR์ ๋ฐ๊ฒฌํ ์ ์์๋ค.
์ด์ ๋ Jetpack DataStore๋ฅผ ํตํด ๋ ์์ ํ๊ฒ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ ์ ์๋ค. ์์๋ณด๋๋ก ํ์.
๐ SharedPreferences ๋จ์
SharedPreferences๋ฅผ ์ ์ ํ๊ฒ ์ฌ์ฉํ์ง ๋ชปํ์ ๋ ์ฌ๋ฌ ๋จ์ ๋ค์ ์ฐพ์ ์ ์๋ค.
- ์ค์ XML ํ์ผ I/O ์์ ์ ํ๋ ๊ฒ์ผ๋ก UI Thread์์ ์์ ํ ๊ฒฝ์ฐ ์์ ํ์ง ์๋ค.
- Runtime Exception์ผ๋ก๋ถํฐ ์์ ํ์ง ์๋ค.
- XML ํ์ผ์ด๊ธฐ์ ์ธ๋ถ์์ ์ฝ๊ฒ ํ์ผ์ ์ฝ์ ์ ์๋ค. (DataStore๋ ์ฝ์ ์๋ ์๋ค)
- ๋น๋๊ธฐ API๋ฅผ ์ ๊ณตํ์ง๋ง Listener๋ฅผ ํตํด์๋ง ๊ฐ์ ์ฝ์ ์ ์๋ค.
- Type-Safety๋ฅผ ์ ๊ณตํ์ง ์๋๋ค.
๐ค DataStore?
๋์ ์์ ์ค๋ช ํ๋ฏ์ด SharedPreferences๋ฅผ ๋์ฒดํ๊ธฐ ์ํด Jetpack์์ ๋ฐํํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค.
Kotlin coroutine๊ณผ Flow๋ฅผ ์ฌ์ฉํ์ฌ ๋น๋๊ธฐ์ ์ผ๋ก, ์ผ๊ด๋๊ฒ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ ์ ์๋ค.
DataStore์๋ SharedPreferences์ฒ๋ผ key-value ํํ๋ก ์ ์ฅํ ์ ์๋ Preferences DataStore,
์ปค์คํ
๋ฐ์ดํฐ ํ์
์ Protocol buffer๋ฅผ ํตํ์ฌ ์ ์ฅํ๋ Proto DataStore๋ก ๋๋๋ค.
Preferences DataStore์ ์์ฝ๊ฒ๋ Type-Safety๋ฅผ ์ ๊ณตํ์ง๋ ์๋๋ค.
๊ทธ๋ฌ๋ Proto DataStore์์ ๋ฏธ๋ฆฌ ์ ์๋ schema๋ฅผ ํตํ์ฌ Type-Safety๋ฅผ ๋ณด์ฅํ๋ค.
์๋ ํ๋ฅผ ํตํด์ SharedPreferences๋ฅผ ๋์ฒดํ์ฌ DataStore๋ฅผ ์ ์ฌ์ฉํด์ผ ํ๋์ง, ๋น๊ตํ๋ฉฐ ์์๊ฐ ์ ์๋ค.

๐จ Implementation
ํด๋น ํฌ์คํ
์์๋ Preferences DataStore์ ๊ตฌํ๋ง ๋ค๋ฃจ๋๋ก ํ๊ฒ ๋ค. (Proto DataStore ์ถํ ํฌ์คํ
)
Preferences DataStore์ ์๋ ON/OFF ๊ฐ(boolean)์ ์ ์ฅํ๋ ์์๋ฅผ ๋ค๋ฃจ๊ฒ ๋ค.
๐ Dependency ์ถ๊ฐ
build.gradle์ DataStore Preferences๋ฅผ ์ถ๊ฐํ๋ค.
Proto DataStore๋ ๋ค๋ฅธ dependency๋ฅผ ์ถ๊ฐํด์ผํ๋ฏ๋ก ๊ณต์ ๋ฌธ์๋ฅผ ์ฐธ๊ณ ํ์. ๊ณต์ ๋ฌธ์
dependencies {
implementation("androidx.datastore:datastore-preferences:1.0.0-rc01")
}
๐ DataStore Instance ์์ฑ
์๋จ ํ์ผ(Top-Level) ๋๋ Application ํด๋์ค, ์ฑ๊ธํค ํํ ๋ฑ ์ ์ ํ ์์น์ Instance๋ฅผ ํ ๋ฒ๋ง ์์ฑํ์ฌ ์ฌ์ฉํด์ผํ๋ค.
preferencesDataStore๋ผ๋ Read-only์ ์์ ํ๋กํผํฐ(delegated properties)๋ฅผ ์ฌ์ฉํ์ฌ ๋ง๋ ๋ค.
ํ์๋ ํ์ผ์ ์์ฑํ์ฌ ๊ธ๋ก๋ฒํ ๋ ํผ๋ฐ์ค๋ฅผ ๊ฐ๋๋ก ํ์๋ค.
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
name = DataStoreKey.DATA_STORE_NAME // like "user_info"
)
name ๋งค๊ฐ ๋ณ์๋ ํ์์ ์ผ๋ก ์ ๋ ฅํด์ผํ๋ฉฐ DataStore์ ์ด๋ฆ์ ์ ๋ ฅํด์ฃผ๋ฉด ๋๋ค. ํด๋๋ผ๊ณ ์๊ฐํ๋ฉด ์ฝ๋ค.
๐ DataStore Key ์ ์
์ ์ฅํ๊ณ ์ ํ๋ ๋ฐ์ดํฐ์ ํํ์ ๋ฐ๋ผ์ Key์ ์ด๋ฆ์ ์ ๋ ฅํ๋ค.
ํฌ์คํ ์์์์๋ ์๋ ON/OFF ์ํ๋ฅผ ํ์ํ๊ธฐ ์ํด boolean ํํ๋ฅผ ์ฌ์ฉํ ๊ฒ์ด๋ฉฐ, Int, String ๋ฑ ๋ค๋ฅธ ํ์ ์ ์ฌ์ฉํ๊ธฐ ์ํด์๋ ๋ค๋ฅธ ํจ์๋ฅผ ์ฌ์ฉํ๋ฉด ๋๋ค.
val IS_NOTIFICATION_ON = booleanPreferencesKey("is_notification_on")
DataStore Key๋ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๊ฑฐ๋ ์ฝ์ด์ฌ ๋ ํ์ํ๋ค.
์ดํ๋ฆฌ์ผ์ด์ ๊ฐ๋ฐ์ ํ๋ค๋ณด๋ฉด ์ฌ๋ฌ ํค์ ๊ด๋ฆฌ๊ฐ ํ์ํ ์ ์์ผ๋ DataStore Key๋ฅผ ๊ด๋ฆฌํ๋ object๋ฅผ ๋ง๋ค์ด ์ ์ฅํ๋ ๊ฒ๋ ๋ฐฉ๋ฒ์ด๋ค.
๐ ๊ฐ ์ ์ฅํ๊ธฐ
DataStore.edit()์ ํธ๋ ์ ์ ๋ฐฉ์์ผ๋ก ๊ฐ์ ์ ์ฅํ ์ ์๋ transform ๋๋ค ๋ธ๋ก์ ์ ๊ณตํ๋ค.
ํด๋น ๋ธ๋ก์ ์ธ์์ธ MutablePreferences(ํ๋จ ์ฝ๋๋ pref๋ก ์ง์นญ)์ ๋ณ๊ฒฝํ Key์ ๊ฐ์ ๋ฃ์ด์ค๋ค.
class SomeWhereActivity : AppCompatActivity() {
val dataStore = applicationContext.dataStore
suspend fun saveUserNotificationState(state: Boolean) {
dataStore.edit { pref ->
pref[DataStoreKey.IS_NOTIFICATION_ON] = state
}
}
//..
}
๐ ๊ฐ ๋ถ๋ฌ์ค๊ธฐ
Preferences DataStore๋ Flow<T> ๋ฐ์ดํฐ๋ฅผ ๋ฐํํ๋ค. Preferences ์ ์ฒด์ ์ ๊ทผํ ๊ฒ์ด๋ฏ๋ก map() ์ฐ์ฐ์๋ฅผ ํตํด Key ๊ฐ์ ์ ์ ํ๊ฒ ๋ฐํํ ์ ์๋๋ก ์ฒ๋ฆฌํ๋ค.
๋ํ ๋ฐ์ดํฐ๋ฅผ ์ฝ์ด์ค๋ ๋์ค์ ์ค๋ฅ๊ฐ ๋ฐ์ํ ๊ฒฝ์ฐ IOException์ ๋ฐ์์ํค๋๋ฐ Flow์ ์ฐ์ฐ์์ธ catch()๋ฅผ ํตํด ์์ธ ์ฒ๋ฆฌํ ์ ์๋ค.
๊ณต์ ๋ฌธ์์์๋ IOException์ผ ๋๋ง emptyPreferences()๋ฅผ ํตํด ๋น ์์ดํ ์ ๋ฐํ์ํค๊ณ ์ด์ธ๋ ์์ธ๋ฅผ ๊ทธ๋๋ก ๋ฐ์์ํค๋ ๊ฒ์ด ์ข๋ค๊ณ ์ถ์ฒํ๊ณ ์๋ค.
class SomeWhereActivity : AppcompatActivity() {
// ..
suspend fun getUserNotificationState(): Flow<Boolean> = dataStore.data
.catch { e ->
if (e is IOException) {
emit(emptyPreferences())
} else {
throw e
}
}.map { prefs ->
prefs[DataStoreKey.IS_NOTIFICATION_ON] ?: false
}
}
๐ UI ์์
๋ฐ์ดํฐ๊ฐ ๋ฐฉ์ถ๋๋ฉด SnackBar๋ฅผ ํตํด์ ํ์ฌ ์ํ๋ฅผ ๋ํ๋ด๊ณ Action ๋ฒํผ์ ๋ง๋ค์ด ํ์ฌ ์ํ์ ๋ฐ๋๋๋๋ก ์ค์ ํ ์ ์๋ ๊ฐ๋จํ ์ฝ๋์ด๋ค.
class SomeWhereActivity : AppCompatActivity() {
//..
private fun getUserNotificationState() {
lifecycleScope.launch {
getUserNotificationState().collect { state ->
withContext(Dispatchers.Main) {
makeSnackBar(state)
}
}
}
}
private fun makeSnackBar(state: Boolean) {
Snackbar.make(binding.root, "์๋ฆผ์ด ํ์ฌ ${if (state) "ON" else "OFF"} ์ํ์
๋๋ค.", Snackbar.LENGTH_LONG)
.setAction(if (state) "OFFํ๊ธฐ" else "ONํ๊ธฐ") {
lifecycleScope.launch {
saveUserNotificationState(!state)
}
}
.show()
}
}
๐ SharedPreferences์์ Migration!
Migrationํ๋ ๋ฐฉ๋ฒ์ ๋งค์ฐ ๊ฐ๋จํ๋ค.
Instance๋ฅผ ์์ฑํ ๋ preferencesDataStore ๋งค๊ฐ ๋ณ์์ produceMigrations๋ฅผ ์ด์ฉํ๋ค.
SharedPreferencesMigration ๋ ๋ฒ์งธ ๋งค๊ฐ ๋ณ์์ SharedPreferences์์ ์ฌ์ฉํ๋ ์ด๋ฆ์ ๋ฃ์ด์ฃผ๋ฉด ๋ชจ๋ key-value๊ฐ ๋ณต์ฌ๋๋ฉฐ ๊ธฐ์กด์ ์กด์ฌํ๋ XML ํํ์ ๋ฐ์ดํฐ๋ ์ญ์ ๋๋ค.
์ฃผ์ํด์ผํ ์ ์ ๋ฐ์ดํฐ๊ฐ ํ ๋ฒ๋ง ์ด์ ๋๊ธฐ์ DataStore๋ก Migrationํ ์ดํ์๋ ๋ ์ด์ SharedPreferences์ ์ฌ์ฉ์ ๋ฉ์ถฐ์ผํ๋ค.
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
name = DataStoreKey.DATA_STORE_NAME,
produceMigrations = { context -> listOf(SharedPreferencesMigration(context, "ex_shared_name")) }
)
๐ ๋ง์น๋ฉฐ
Coroutine๊ณผ Flow๋ฅผ ์ฌ์ฉํ๊ธฐ์ ํด๋น ๋ด์ฉ์ ํ์ต์ด ์ด๋ฃจ์ด์ง์ง ์๋๋ค๋ฉด ์ด๋ ต๊ฒ ์ง๋ง,
์๊ณ ๋ง ์๋ค๋ฉด ์ด๋ ต์ง ์๊ณ SharedPreferences์ ๋จ์ ์ ์๋นํ ๋ณด์ํ ์ ์๋ค๊ณ ์๊ฐํ๋ค.
Dependency ์ถ๊ฐ๋ฅผ ํ ๋์ฒ๋ผ ์์ง์ Stable release ์ํ๊ฐ ์๋๋ค.
๊ทธ๋๋ ์ด๊ธฐ์ ๊ณต๊ฐ๋ ์ดํ๋ก ๊พธ์คํ ์ ๋ฐ์ดํธ๋ก ํ์ฌ RC ๋ฒ์ ๊น์ง ์ฌ๋ผ๊ฐ๊ธฐ์ Migration์ ๊ณ ๋ฏผํด๋ด๋ ์ข์ ๊ฒ ๊ฐ๋ค.
Proto DataStore๋ Proto Buffer์ syntax๊ฐ ์กฐ๊ธ ํฌํจ๋์ง๋ง ์ด ๋ํ ์ด๋ ต์ง ์๊ฒ ์ ์ฉํ ์ ์๋ค๊ณ ์๊ฐํ๋ค.
๊ธฐํ๊ฐ ๋๋ค๋ฉด ์ถํ ํฌ์คํ ์ผ๋ก ๋ค๋ฃจ๋๋ก ํ๊ฒ ๋ค.
Repository๋ฅผ ์ฌ์ฉํ์ฌ ์ ์ฉ๋ ์์ ํ๋ก์ ํธ๋ ๊นํ๋ธ์์ ํ์ธํ ์ ์๋ค. ๊นํ๋ธ ๋ฐ๋ก๊ฐ๊ธฐ
๐ References
๋๊ธ