๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
Dev/Android

[Android] SharedPreferences๋ณด๋‹ค ์•ˆ์ „ํ•˜๊ฒŒ DataStore๋ฅผ ์ด์šฉํ•˜์—ฌ ๋ฐ์ดํ„ฐ ์ €์žฅํ•˜๊ธฐ

by JUNE.C 2021. 7. 16.

๐Ÿง ๋“ค์–ด๊ฐ€๋ฉฐ

 ์•ˆ๋“œ๋กœ์ด๋“œ์—์„œ ๋‹จ์ˆœํ•œ ๋ฐ์ดํ„ฐ์…‹์„ ๋กœ์ปฌ์— ์ €์žฅํ•˜๊ธฐ ์œ„ํ•ด์„œ ํ”ํžˆ 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๋ฅผ ์™œ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋Š”์ง€, ๋น„๊ตํ•˜๋ฉฐ ์•Œ์•„๊ฐˆ ์ˆ˜ ์žˆ๋‹ค.

 

์ถœ์ฒ˜ : Android Developers Blog

 


๐Ÿ”จ 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

์•ˆ๋“œ๋กœ์ด๋“œ ๊ณต์‹ ๋ฌธ์„œ

 

๋Œ“๊ธ€