본문 바로가기
Development/Android

Android - DataStore로 SharedPreferences를 대체하자.

by du.it.ddu 2023. 7. 7.
반응형

어플리케이션을 개발하다보면 앱 내부에 데이터를 저장해야 할 때가 있다.

그 중에서 어떤 유저가 특정 기능을 활성화 했는지 여부와 같이 굉장히 작은 규모의 데이터가 저장되어야 할 때가 있다.
이럴때 우린 SharedPreferences를 사용해서 저장하곤 했다.

하지만 SharedPreferences는 여러가지 단점이 있다.
UI쓰레드에서 호출하기에 안전하지 않고, 예외나 런타임 안정성 등이 있다. 그리고 비동기 API에 대한 대응도 부족한 편이다.

이런 단점 때문인지, 구글에서 이를 보완하기 위해 Jetpack DataStore를 개발하였다.

https://developer.android.com/topic/libraries/architecture/datastore?hl=ko 

 

앱 아키텍처: 데이터 영역 - Datastore - Android 개발자  |  Android Developers

데이터 영역 라이브러리에 관한 이 앱 아키텍처 가이드를 통해 Preferences DataStore 및 Proto DataStore, 설정 등을 알아보세요.

developer.android.com

단순히 SharedPreferences를 대체하려는 의도보다는 좀 더 큰 그림이 있지만,
우리에게 있어서 SharedPreferences의 대체제가 나타났다는 사실은 큰 희소식이다.

DataStore는 어떤 특징이 있길래 SharedPreferences를 대체할 수 있을까?
아래 표를 보면 직관적으로 알 수 있다.

DataStore는 Flow와 RxJava와의 호환을 통해 비동기 API를 제공한다.
동기 API를 제공하지 않는 것을 볼 수 있는데, SharedPreferences를 사용할때도 동기 API를 사용하는 것은 UI 쓰레드에서 호출하는 것이 안전하지 않기 때문에 지양하고 있다. 또한 데이터 영역의 작업이므로 동기 API는 개인적으로 그다지 의미가 없다.

내부적인 처리로 UI 쓰레드에서의 호출이 안전하고, 오류나 런타임 예외 처리 등에 있어서 많은 강점을 보이는 것을 알 수 있다.
그럼 가벼운 예시로 사용방법을 알아보자.


가장 먼저 DataStore 의존성을 추가해주자.

implementation "androidx.datastore:datastore-preferences:1.0.0"

 

그리고 Activity 또는 Fragment 혹은 데이터 레이어에서 Context를 통해 DataStore 객체를 선언해준다.
SharedPreferences를 선언하는것과 크게 다르지 않다.

class MainActivity : ComponentActivity() {
    private val dataStore: DataStore<Preferences> by preferencesDataStore("app.pref")

    ...
}

라이브러리에서 Delegate를 제공하기 때문에 아주 쉽게 선언할 수 있다.


그리고 DataStore에 사용할 키 값을 선언하고, 값이 변화했을 때 이를 얻을 수 있는 Flow 객체를 생성한다.

class MainActivity : ComponentActivity() {
    private val dataStore: DataStore<Preferences> by preferencesDataStore("app.pref")

    private val clickCountFlow: Flow<Int> by lazy {
        dataStore.data.map { pref -> pref[PREF_KEY_CLICK_COUNT] ?: 0 }
    }
    
    ...
    
    companion object {
        private val PREF_KEY_CLICK_COUNT = intPreferencesKey("PREF_KEY_CLICK_COUNT")
    }    
}

키 값에 대한 선언도 훨씬 직관적으로 변했다.
키를 정의할 때 어떤 유형의 값에 대한 키인지도 명시되어 있다.
위 예시에서는 Int 형의 값을 가지는 키를 선언했으며, 다양한 타입을 지원한다.

그리고 dataStore의 data 필드에 접근하고, 이 필드에서 키값을 사용하여 원하는 값으로 변경한다.
여기서 "by lazy" 가 사용되었는데, Activity와 같은 곳에서 최상단에 정의할 경우, Context가 존재하지 않으면 크래쉬가 발생한다.


class MainActivity : ComponentActivity() {
    ...

    private fun increaseClickCount() {
        lifecycleScope.launch {
            dataStore.edit { pref ->
                val currentValue = pref[PREF_KEY_CLICK_COUNT] ?: 0
                pref[PREF_KEY_CLICK_COUNT] = currentValue + 1
            }
        }
    }
    ...
}

값을 변화할 때는 DataStore 객체의 edit을 사용한다.
suspend 함수이기 때문에 CoroutineScope 내에서 사용할 수 있다. 위 예시는 LifecycleScope를 사용했다.
값을 변화하는 것은 기존의 SharedPreferences와 매우 유사하다.


class MainActivity : ComponentActivity() {
    ...
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            DataStoreTheme {
                Surface(
                    ...
                ) {
                    Column(
                        ...
                    ) {
                        val clickCount = clickCountFlow.collectAsState(initial = 0).value

                        Text("Click count : $clickCount")
                        ...
                    }
                }
            }
        }
    }
    ...
}

값을 읽을때는 Flow의 값을 읽는것과 동일하다.

Compose라면 위와 같이 Flow.collectAsState 를 활용할 수 있다.
아니라면 CoroutineScope에서 Flow.collect와 같은 함수를 통해 값을 읽어들이면 된다.
Flow를 사용하기 때문에 값이 변화하면 자동으로 값을 수신할 수 있다.

SharedPreferences를 사용할 때 보다 훨씬 간편하고 UI쓰레드에서 안정적으로 값을 수신할 수 있다.


기존에 SharedPreferences를 사용하고 있었더라도 Migration 기능이 제공되기 때문에 기존에 저장된 값들을 날리지 않고 문제없이 적용할 수 있다.

이제 SharedPreferences는 고이 보내줄때가 되었다.

반응형