2021년 10월 16일 토요일

[안드로이드] Jetpack의 DataStore를 이용하여 데이터 저장하기

 

Jetpack의 DataStore를 이용하여 데이터 저장하기

SharedPreference를 대체하기 위해, 데이터 저장을 개선하여 새롭게 DataStore라는 모듈이 Jetpack에서 alpha로 출시되었습니다. DataStore는 코틀린의 코루틴과 Flow으로 빌드되었으며, 2가지의 다른 구현체를 제공합니다.

  • Proto DataStore : Protocal buffers로 작성된 Object를 저장하는 구현체
  • Preferences DataStore : key-value쌍의 데이터를 저장하는 구현체

DataStore는 SharedPreference의 단점들을 개선하기 위해서, 데이터를 비동기적이고 일관성있고, 묶어서(transactionally) 저장하도록 구현되어 있습니다.

SharedPreferences vs DataStore



  • SharedPreferenece는 동기적인 API이기 때문에 UI스레드에서의 호출도 안전하긴 하지만, 실제로는 디스크 I/O 작업을 하기때문에 문제가 있습니다. 게다가 apply() 메서드는 fsync() 의 UI스레드를 블락합니다. 즉, 애플리케이션 내에서 서비스나 엑티비티가 start/stop하는 매 순간마다 fsync() 호출을 지연시키게 될 것이고, UI스레드가 블락되어 ANR이 발생할 여지가 높아지게 됩니다.
  • SharedPreference는 파싱에러를 런타임 예외로 던집니다.

특별한 경우가 아니면 DataStore의 2가지 구현 모두 설정값을 파일에 저장하고, 모든 연산을 Dispatchers.IO 에서 수행합니다. 그렇지만, 2가지 구현이 각각 다른 방법으로 데이터를 저장합니다.

  • Preference DataStore : SharedPreference와 같이 데이터 스키마를 정의하는 방법이 없고, Key가 안전한 데이터타입에 접근하는지를 보장하지 않습니다.
  • Proto DataStore : Protocal buffers를 이용하여 스키마를 정의할 수 있습니다. Protocal buffers는 데이터의 타입도 강하게 지정하는 것이 가능합니다. XML이나 다른 데이터포멧보다도 빠르고, 작고, 모호하지 않습니다. 하지만, Proto DataStore를 다루려면 새로운 직렬화 메커니즘을 익혀야합니다.

Room vs DataStore

부분업데이트가 필요하거나, 참조무결성 / 복잡하거나 큰 데이터의 지원이 필요한 경우라면, DataStore대신에 Room을 사용하는것이 낫습니다. DataStore는 작고 심플한 데이터에 가장 이상적으로 동작하며, 부분업데이트나 참조무결성은 지원하지 않습니다.

DataStore 사용

DataStore 디펜턴시를 추가해주어야 합니다. (Proto DataStore를 사용하려면 추가 설정을 해주어야 합니다. - 링크)

// Preferences DataStore
implementation "androidx.datastore:datastore-preferences:1.0.0-alpha01"


// Proto DataStore
implementation  "androidx.datastore:datastore-core:1.0.0-alpha01"

Proto DataStore로 작업할 때, app/src/main/proto/ 디렉터리의 proto파일 내에 스키마를 정의해주어야 합니다. 스키마를 정의하는 방법은 이 문서를 참고해주세요.

syntax = "proto3";

option java_package = "<your package name here>";
option java_multiple_files = true;

message Settings {
  int my_counter = 1;
}

DataStore 생성

Context.createDataStore() 확장함수를 이용하여 DataStore를 생성할 수 있습니다.

// with Preferences DataStore
val dataStore: DataStore<Preferences> = context.createDataStore(
    name = "settings"
)

Proto DataStore를 사용하려면 Serializer 인터페이스도 구한하여 DataStore에게 넘겨주어야 합니다.

object SettingsSerializer : Serializer<Settings> {
    override fun readFrom(input: InputStream): Settings {
        try {
            return Settings.parseFrom(input)
        } catch (exception: InvalidProtocolBufferException) {
            throw CorruptionException("Cannot read proto.", exception)
        }
    }

    override fun writeTo(t: Settings, output: OutputStream) = t.writeTo(output)
}


// with Proto DataStore
val settingsDataStore: DataStore<Settings> = context.createDataStore(
    fileName = "settings.pb",
    serializer = SettingsSerializer
)

DataStore로부터 데이터 읽기

DataStore는 데이터를 Flow, Preferences, Proto Schema로 가져올 수 있습니다. DataStore는 Dispatchers.IO에서 데이터를 접근하도록 보장하여 UI스레드가 블락되지 않도록 합니다.

  • Preferences DataStore
val MY_COUNTER = preferencesKey<Int>("my_counter")
val myCounterFlow: Flow<Int> = dataStore.data
     .map { currentPreferences ->
        // Unlike Proto DataStore, there's no type safety here.
        currentPreferences[MY_COUNTER] ?: 0   
   }
  • Proto DataStore
val myCounterFlow: Flow<Int> = settingsDataStore.data
    .map { settings ->
        // The myCounter property is generated for you from your proto schema!
        settings.myCounter 
    }

DataStroe에 데이터 쓰기

DataStore는 데이터를 저장하기 위해서 DataStore.updateData() 일시중단 함수를 제공합니다. 이 함수는 현재 저장된 데이터의 상태를 담은 Preferences 또는 proto Schema 객체를 파라미터로 넘겨줍니다. 이 updateData() 함수는 데이터의 읽기/쓰기/수정 연산이 원자적으로 동작하도록 합니다.

Preferences DataStore는 데이터 갱신을 좀 더 쉽게 할 수 있도록 DataStore.edit() 함수를 제공합니다. Preferences 객체를 받는 대신에, 직접 수정가능하도록 MutablePreferences 객체를 받습니다. updateData() 함수와 마찬가지로 변경이 다 완료되어 디스크에 저장된 후에야 변경사항이 적용됩니다.

  • edit() 예시 (Preferences DataStore)
suspend fun incrementCounter() {
    dataStore.edit { settings ->
        // We can safely increment our counter without losing data due to races!
        val currentCounterValue = settings[MY_COUNTER] ?: 0
        settings[MY_COUNTER] = currentCounterValue + 1
    }
}
  • updateData() 예시 (Proto DataStore)
suspend fun incrementCounter() {
    settingsDataStore.updateData { currentSettings ->
        // We can safely increment our counter without losing data due to races!
        currentSettings.toBuilder()
            .setMyCounter(currentSettings.myCounter + 1)
            .build()
    }
}

SharedPreferences를 DataStore로 마이그레이션하기

SharedPreferences에서 DataStore로 데이터를 마이그레이션 하려면, DataStore 빌더에 SharedPreferencesMigration 객체를 전달해 주어야 합니다. 그러면, 자동으로 SharedPreferences의 데이터를 DataStore에 마이그레이션해 줍니다. 마이그레이션 동작은 DataStore에서 데이터 접근을 하기 전에 수행됩니다. 즉, DataStore.data가 어떤 데이터를 반환하거나 DataStore.updateData()가 업데이트를 수행하기 전에 마이그레이션이 성공해야 함을 의미합니다.

Preferences DataStore로 마이그레이션하려면 SharedPreferencesMigration 구현체를 기본으로 사용하고, SharedPreferences의 이름을 생성자에 전달하면 됩니다.

val dataStore: DataStore<Preferences> = context.createDataStore(
    name = "settings",
    migrations = listOf(SharedPreferencesMigration(context, "settings_preferences"))
)

Proto DataStroe로 마이그레이션하려면 SharedPreferences의 데이터를 Proto Schema로 매핑하는 함수를 구현해 주어야 합니다.

val settingsDataStore: DataStore<Settings> = context.createDataStore(
    produceFile = { File(context.filesDir, "settings.preferences_pb") },
    serializer = SettingsSerializer,
    migrations = listOf(
        SharedPreferencesMigration(
            context,
            "settings_preferences"            
        ) { sharedPrefs: SharedPreferencesView, currentData: UserPreferences ->
            // Map your sharedPrefs to your type here
          }
    )
)

원문

댓글 없음:

댓글 쓰기