Android

DataStore

까망사과 2022. 11. 30. 02:00

DataStore는 프로토콜 버퍼를 사용하여 키-값 쌍 및 커스텀 객체를 저장할 수 있는 데이터 저장 솔루션이다. 내부적으로 Coroutine 및 Flow를 사용하기 때문에 비동기 트랜잭션을 통해 데이터를 일관성 있게 저장할 수 있다. 공식 문서에서는 DataStore를 규모가 작고 단순한 데이터셋에만 사용하고 반대의 경우에는 Room 라이브러리를 사용하도록 권장하고 있다.

 

DataStore는 두 가지 방식으로 구현할 수 있다.

 

Preferences DataStore

SharedPreferences와 유사하게 키-값 쌍을 사용하여 데이터를 저장하는 방식이다. primitive 및 간단한 컬렉션 타입만 지원하기 때문에 타입 안전성을 보장하지 않는다.

 

객체 생성하기

androidx.datastore.preferences 패키지의 preferencesDataStore 함수를 사용하여 DataStore<Preferences> 프로퍼티에 대한 위임을 생성한다. String 파라미터는 DataStore가 저장되는 파일의 이름을 나타낸다. 이 함수는 Kotlin 파일의 최상위 레벨에서 한 번만 호출해야 하며 이후에 DataStore를 사용하려면 동일한 인스턴스를 참조해야 한다. 생성된 위임을 소유하는 객체는 Context 인스턴스이어야 하므로 Context의 확장 프로퍼티로 정의하여 사용한다.

// Kotlin 파일의 최상위 레벨에서 호출하여 싱글톤으로 사용한다.
val Context.myDataStore by preferencesDataStore(name = "my_prefs")

 

데이터 읽기 및 쓰기

Preferences의 데이터를 구별할 때 Preferences.Key를 키로 사용한다. Key 인스턴스를 참조하려면 데이터 타입에 대응하는 함수를 사용해야 한다. 각 함수와 이에 대응하는 데이터 타입은 다음과 같다. String 파라미터는 키의 이름을 나타낸다.

  • intPreferencesKey(String): Preferences.Key<Int>
  • longPreferencesKey(String): Preferences.Key<Long>
  • floatPreferencesKey(String): Preferences.Key<Float>
  • doublePreferencesKey(String): Preferences.Key<Double>
  • booleanPreferencesKey(String): Preferences.Key<Boolean>
  • stringPreferencesKey(String): Preferences.Key<String>
  • byteArrayPreferencesKey(String): Preferences.Key<ByteArray>
  • stringSetPreferencesKey(String): Preferences.Key<Set<String>>

 

데이터를 저장할 때는 DataStore<Preferences>의 확장 함수인 edit(suspend (MutablePreferences) -> Unit)을 사용할 수 있다. 파라미터로 전달되는 함수의 각 코드는 단일 트랜잭션으로 간주된다. 이 함수의 소스를 보면 알 수 있듯 DataStore<T>#updateData(suspend (T) -> T)가 호출된다.

 

androidx.datastore.preferences.core.Preferences.kt

public suspend fun DataStore<Preferences>.edit(
    transform: suspend (MutablePreferences) -> Unit
): Preferences {
    this.updateData {
        it.toMutablePreferences().apply { transform(this) }
    }
}

 

데이터에 액세스할 때는 DataStoredata 프로퍼티와 map 함수를 사용한다.

val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
val exampleCounterFlow: Flow<Int> = context.dataStore.data
    .map { preferences ->
        // No type safety.
        preferences[EXAMPLE_COUNTER] ?: 0
    }

 

Proto DataStore

DataStore과 프로토콜 버퍼를 사용하여 타입이 지정된 객체를 디스크에 저장하는 방식이다. Proto DataStore를 사용하려면 app/src/main/proto 디렉터리의 proto 파일에 사용하고자 하는 데이터 타입 스키마를 미리 정의해야 한다.

 

객체 생성하기

Proto DataStore 인스턴스는 두 단계를 거쳐 생성된다.

 

첫번째로 Serializer<T> 인터페이스를 구현한다. 이는 사전에 proto 파일에 정의한 데이터 타입을 직렬화/역직렬화하는 방식을 정의한다. 타입 파라미터 T는 불변형 타입을 사용해야 한다. 가변형 타입을 사용하는 경우 DataStore의 기능이 제대로 동작하지 않을 수 있다.

 

androidx.datastore.core.Serializer.kt

public interface Serializer<T> {

    // 디스크에 데이터가 없는 경우 반환하는 기본값
    public val defaultValue: T

    // 입력 스트림에서 가져온 객체를 언마셜링(역직렬화)
    public suspend fun readFrom(input: InputStream): T

    //  객체를 마셜링(직렬화)하여 출력 스트림에 전달
    public suspend fun writeTo(t: T, output: OutputStream)
}

 

구현 예시

object SettingsSerializer : Serializer<Settings> {
    override val defaultValue: Settings = Settings.getDefaultInstance()

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

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

 

두번째로 androidx.datastore 패키지의 dataStore 함수를 사용하여 DataStore<T> 인스턴스에 대한 위임을 생성한다. Preferences DataStore과 마찬가지로 파일 최상위 레벨에서 Context의 확장 프로퍼티로 정의하여 싱글톤으로 사용한다. String 파라미터는 DataStore가 저장되는 파일 이름을 나타내며 Serializer 프로퍼티는 해당 데이터 타입에 대한 Serializer 구현체를 나타낸다.

val Context.settingsDataStore: DataStore<Settings> by dataStore(
    fileName = "settings.pb",
    serializer = SettingsSerializer
)

 

데이터 읽기 및 쓰기

데이터를 저장할 때는 DataStore<T>#updateData(suspend (T) -> T)를 사용한다. 함수형 파라미터의 T 타입 파라미터는 현재 데이터의 상태를 나타내며 각 코드는 단일 트랜잭션으로 간주된다.

suspend fun incrementCounter() {
    context.settingsDataStore.updateData { currentSettings ->
        currentSettings.toBuilder()
            .setExampleCounter(currentSettings.exampleCounter + 1)
            .build()
    }
}

 

데이터에 액세스할 때는 DataStoredata 프로퍼티와 map 함수를 사용한다.

val exampleCounterFlow: Flow<Int> = context.settingsDataStore.data
    .map { settings ->
        // exampleCounter 프로퍼티는 proto 스키마에서 생성된다.
        settings.exampleCounter
    }

'Android' 카테고리의 다른 글

WorkManager (2) : 작업 상태  (0) 2022.12.02
WorkManager (1) : 작업 설정/예약하기  (0) 2022.12.02
Lifecycle  (0) 2022.11.11
데이터 바인딩 (4) : 양방향 바인딩  (0) 2022.11.05
데이터 바인딩 (3) : 바인딩 어댑터  (0) 2022.11.03