2 분 소요

class 로컬 데이터 {
    DataStore: 이제 여기는 얼씬도 말라. 알았어?   
    SharedPreferences: ...
}

로컬 저장

지금까지 안드로이드 로컬에 간단한 데이터를 저장하기 위해 SharedPreferences를 사용하였다.
구글에서는 DataStore 의 사용을 적극 권장하고 있고 (SharedPreferences는 구글 공식 문서에서도 사용가이드가 사라졌다..😨)

Datastore를 사용하면 어떤 좋은점들이 있어서 사용을 이렇게 권고하는 것인가? Datastore를 하나씩 알아가 보자!

🚀 DataStore

DataStoreKey-Value 타입으로 구성되어 있는 Preferences DataStore사용자가 정의한 데이터를 저장 할 수 있는 Proto DataStore 가 존재한다.

Proto DataStore 을 사용하기 위해서는 ‘프로토콜 버퍼’를 이용하여 스키마를 정의 해야한다. 이는 데이터의 타입을 보장과 더불어 SharedPreferences보다 빠르고 단순하다👍

DataStore의 좋은점?

[출처] https://android-developers.googleblog.com/2020/09/prefer-storing-data-with-jetpack.html

🤔 DataStore가 한눈에도 더 많은 것을 제공해 주는것을 볼 수 있다. 간단하게 요약하자면 다음과 같은 특징이 있다.

  • 코루틴 + Flow 를 사용하여 Read/Write 에 대한 비동기 API를 제공
  • UI Thread(Main Thread) 를 호출해도 안전 (Dispatcher.IO에서 동작한다)
  • RuntimeException 으로부터 안전

직접 사용해 보기

라이브러리 추가

build.gradle(app)에 아래와 같이 라이브러리를 추가해 준다.

// Datastore
implementation 'androidx.datastore:datastore-preferences:1.0.0'

DataStore 생성

class AppDatastoreManager(private val context: Context) : AppDataStore {

    private val Context.datastore: DataStore<Preferences> by preferencesDataStore(
        name = "datastore_name"
    )

    // String 타입 저장 Key 값
    private val stringKey = stringPreferencesKey("key_name")
    // Int 타입 저장 Key 값
    private val intKey = intPreferencesKey("key_name")
    .
    .
    .
}

DataStore 을 생성해 주기 위해서는 안드로이드 Context의 확장 프로퍼티로 선언해 주고 DataStore에서 사용할 키 값을 설정해 준다.

PreferencesKey 는 아래와 같은 7가지 타입이 있다.

데이터를 Read하는 Flow 생성

DataStore에서 데이터를 읽어올 때 코루틴의 Flow 를 사용하여 데이터를 Flow 객체로 전달한다.

val intValue :Flow<Int> =
    context.datastore.data
        .catch { exception ->
            if (exception is IOException) {
                emit(emptyPreferences())
            } else {
                throw exception
            }
        }.map { preference ->
            preference[intKey] ?: 0
        }

map() 함수를 사용하여 아까 생성한 키 값(intKey)에 대응하는 ValueFlow 형태로 가져오도록 한다.

또한 catch() 를 사용하여, 데이터 읽어오기에 실패하는 경우 IOException 을 처리하여 emptyPreferences() 로 비어있는 값을 전달해준다.

데이터를 Write하는 메소드 생성

DataStore에 값을 쓸 때는 edit()메소드를 이용한다. 또한 값을 쓸때는 반드시 비동기 로 작업이 되야하므로 suspend 키워드를 통해 해당 작업이 코루틴 영역에서 동작 할 수 있도록 해준다.

suspend fun setInt(data: Int) {
    context.datastore
        .edit { preferences ->
            preferences[intKey] = data
        }
}

이제 사용하기

DataStore은 Singleton으로 관리되어야 한다. 따라서 Application에서 초기화해주고 사용해보도록 하자.

class App: Application() {
    companion object {
        lateinit var datastore: AppDataStoreManager
    }

    override fun onCreate() {
        super.onCreate()
        initDatastore()
    }

    private fun initDatastore() {
        datastore = AppDatastoreManager(applicationContext)
    }
}

1. 데이터 값 읽고 쓰기

DataStore에서 읽은 데이터를 사용하기 위해서는 DataStore 클래스에서 선언해 놓은 변수에 접근한 후 Flow객체를 반환 받고 collect 함수를 이용하여 값을 읽어온다. CoroutineScope에서 수행되어야 한다.

CoroutineScope(Dispatchers.Main).launch {
    App.datastore.intValue.collect { it ->
    // 값을 사용하여 뷰에 적용
    }
}

딱 원하는 타이밍에 한 번만 값을 받아와서 사용하고 싶을때는 first() 함수를 이용할 수 있다.

CoroutineScope(Dispatchers.Main).launch {
    val intValue = App.datastore.intValue.first()
}

동기적인 동작이 꼭 필요한 경우 runBlocking을 사용할 수 있다.

runBlocking {
   val intValue = App.datastore.intValue.first()
}  

UI 스레드에서 동기 작업을 실행하면 ANR 또는 UI 버벅거림 이 발생할 수 있기 때문에 과도한 동작은 금해야한다😨

2. 데이터 저장

DataStore에 값을 저장하고 싶을때는 미리 작성해 놓은 함수(setInt())를 사용하면 된다. 이때에 suspending으로 지정되어 있기 때문에 코루틴이나 RxJava를 통해 비동기적으로 호출해 준다.

CoroutineScope(Dispatchers.Main).launch {
    App.datastore.setInt(26)
}

지금까지 SharedPreferences를 대체하여 사용하기 위한 DataStore를 알아보았다. 자체적으로 코루틴의 Flow를 사용하여 비동기적이고 안전하게 사용이 가능하다는 점을 보고 DataStore를 꾸준히 사용해 보면서 점점 더 큰 장점들을 알아가보면 좋을것 같다.

References

맨 위로 이동 ↑

댓글남기기