밍맹의 생각날 때 적는 블로그

[Android/Kotlin] SharedPreferences 대신 쓰는 DataStore 본문

안드로이드

[Android/Kotlin] SharedPreferences 대신 쓰는 DataStore

mingmaeng 2021. 3. 12. 14:38

지금까지 우리들은 로컬에 간단한 데이터들을 저장하기 위해서 SharedPreferences를 사용했습니다.

하지만 현재 안드로이드에서는 DataStore의 사용을 적극 권장하고 있습니다. ( 개발자 문서에서도 SharedPreferences 사용 가이드를 빼버렸더군요 ㄷㄷ... )

 

 

DataStore가 무엇 인가요?

DataStore는 프로토콜 버퍼를 사용하여 키-값 쌍 또는 유형이 지정된 객체를 저장할 수 있는 데이터 저장소 솔루션입니다.

코루틴 및 Flow를 사용하여 비동기적이고 일관된 트랜잭션 방식으로 데이터를 저장하는 것이 특징이라고 할 수 있습니다.

 

DataStore는 키 - 값 으로 구성되어 있는 Preferences DataStore , 사용자가 정의한 데이터를 저장할 수 있는 Proto DataStore가 존재합니다. Protoco DataStore를 사용하게 되면 '프로토콜 버퍼'라는 것을 이용하여 스키마를 정의해야합니다. 이는 데이터의 타입을 보장해줄 뿐더러 SharedPreferences보다 빠르고 단순합니다.

 

* 프로토콜 버퍼란? -> 구글의 데이터를 직렬화 하기위한 메커니즘 정도로 생각하시면 됩니다.

 

DataStore는 어떤게 좋아요?

먼저 표를 하나 보겠습니다.

 

출처 : https://proandroiddev.com/lets-explore-jetpack-datastore-in-android-621f3564b57

딱 봐도 DataStore가 SharedPreferences보다 많은 것을 제공해 주고 있는 것을 볼 수 있습니다.

 

간단하게 요약하자면 DataStore는 다음과 같은 특징이 있습니다.

  • DataStore는 코루틴과 Flow를 통해 읽고 쓰기에 대한 비동기 API를 제공합니다.
  • DataStore는 UI 스레드를 호출해도 안전합니다. ( Dispather.IO 밑에서 작동하기 때문에 )
  • runtime exception으로부터 안전합니다. ( 이게 제일 중요한 특징 같네요 ㅎㅎ )

어떻게 써요?

해당 포스팅에서는 Preferences DataStore 사용법에 대해서 알아보겠습니다.

DataStore를 사용하기 위해서 먼저 gradle에 의존성을 추가 해줍니다.

( 해당 코드는 1.0.0-alpha07 기준입니다. 이전 버전을 사용하시는 분들은 조금 다를 수 있습니다. )

 

dependencies{

    // Preferences Data Store
    implementation "androidx.datastore:datastore-preferences:1.0.0-alpha07"
    implementation "androidx.datastore:datastore-core:1.0.0-alpha07"
    
}

 

 

DataStore 생성

DataStore를 사용하기 위해서는 생성을 해야겠죠? DataStore는 다음과 같이 생성할 수 있습니다.

class DataStoreModule(private val context : Context) {

    private val Context.dataStore  by preferencesDataStore(name = "dataStore")

    private val stringKey = stringPreferencesKey("key_name") // string 저장 키값
    private val intKey = intPreferencesKey("key_name") // int 저장 키값
    ...
 }

먼저 globalDataStore 속성을 통하여 DataStore를 생성합니다.

그 다음 키 - 값의 쌍으로 구성되어 있는 DataStore에 저장할 값의 키를 만들어줍니다.

DataStore에서 사용할 키 값은 'type'PreferencesKey("name") 로 선언할 수 있습니다. String 타입을 저장하고 싶다면 stringPreferencesKey를 저장하는 것 처럼 type에 맞는 키 유형 함수를 사용합니다.

 

DataStore에서 값 읽기

DataStore에서 데이터를 읽어올 때 해당 데이터는 Flow객체로 전달되게 됩니다.

DataStore에서 값을 읽기 위해서는 다음과 같이 사용하면 됩니다.

 

    val text : Flow<String> = context.dataStore.data
        .map { preferences ->
            preferences[stringKey] ?: ""
        }

 

map()을 이용해서 DataStore에 저장되어있는 값을  미리 지정해둔 키를 통해 가져옵니다. 이 때 반환되는 값은 Flow타입이 됩니다.

값을 읽어올 때 해당 작업이 실패하게 되면 IOException이 발생하게 됩는데 catch()문을 exception이 발생했을 경우 빈 값을 전달합니다.

 

    val text : Flow<String> = context.dataStore.data
        .catch { exception ->
            if (exception is IOException) {
                emit(emptyPreferences())
            } else {
                throw exception
            }
        }
        .map {preferences ->
            preferences[stringKey] ?: ""
        }

 

DataStore에 값 쓰기

DataStore의 값을 쓸 때는 edit() 를 이용합니다. DataStore에 값을 작성하기 위해서는 반드시 비동기로 작업해야 합니다.

그래서 suspend를 통해 값을 작성하는 함수가 코루틴 영역에서 동작할 수 있도록 처리합니다.

 

    suspend fun setText(text : String){
        context.dataStore.edit { preferences ->
            preferences[stringKey] = text
        }
    }

 

DataStore클래스의 모습을 전체적으로 보면 다음과 같습니다.

 

class DataStoreModule(private val context : Context) {

    private val Context.dataStore  by preferencesDataStore(name = "dataStore")

    private val stringKey = stringPreferencesKey("textKey")

	// stringKey 키 값과 대응되는 값 반환
    val text : Flow<String> = context.dataStore.data
        .catch { exception ->
            if (exception is IOException) {
                emit(emptyPreferences())
            } else {
                throw exception
            }
        }
        .map {preferences ->
            preferences[stringKey] ?: ""
        }
        
    // String값을 stringKey 키 값에 저장
    suspend fun setText(text : String){
        context.dataStore.edit { preferences ->
            preferences[stringKey] = text
        }
    }

}

 

생성한 DataStore를 사용해보자.

DataStore는 Singleton으로 관리되어야 합니다. 저 같은 경우는 Application에서 생성하여 사용하고 있습니다.

 

class SampleApplication : Application() {

    private lateinit var dataStore : DataStoreModule

    companion object {
        private lateinit var sampleApplication: SampleApplication
        fun getInstance() : SampleApplication = sampleApplication
    }

    override fun onCreate() {
        super.onCreate()
        sampleApplication = this
        dataStore = DataStoreModule(this)
    }

    fun getDataStore() : DataStoreModule = dataStore
}

 

Activity or Fragment에서 값 읽고 쓰기

생성한 DataStore를 Activity나 Fragment에서 사용하려면 다음과 같이 사용하면 됩니다.

 

읽기의 경우 DataStore 클래스에서 선언해 놓은 변수에 접근한 후 Flow객체를 반환 받고 collect() 를 이용하여 값을 읽어옵니다.

 

SampleApplication.getInstance().getDataStore().text.collect{ it ->
    // do it Something
}

 

DataStore에서 값을 읽어올 때 Flow 형태로 읽어오기 때문에 기본 설정 값 변경에 대해 반응합니다. 그래서 이 또한 비동기적으로 작업을 하게 되는데, 내가 원하는 타이밍에 한 번만 값을 받아와서 사용하고 싶을 때가 있습니다. (그런 상황이 굉장히 많을 거라고 생각합니다.)

그럴 때는 first() 함수를 이용해서 한 번만 값을 호출할 수 있습니다.

상황에 따라서 적절하게 이용하면 될 것 같습니다 :)

 

val text = SampleApplication.getInstance().getDataStore().text.first()

 

DataStore에 값을 저장하고 싶을 때는 DataClass에 미리 작성해 놓은 함수를 호출해 주면 됩니다.

이 때 함수는 suspend로 지정되어 있기 때문에 코루틴이나 RxJava를 통해 비동기적으로 호출해줍니다.

 

CoroutineScope(Dispatchers.Main).launch {
    val text = "Sample"
    SampleApplication.getInstance().getDataStore().setText(text)
 }

 

 


DataStore는 기존 SharedPreferences를 대체하기 위해 나온 Jetpack의 구성요소입니다. DataStore는 간단한 값들을 저장하는데 사용하시고, 문서에서 언급하듯이 복잡한 데이터나 대규모의 데이터 같은 경우는 Room을 이용하는 것을 추천합니다.

 

다음 포스팅에서는 SharedPreferences를 DataStore로 마이그레이션 하는 방법을 알아보도록 하겠습니다.

Comments