본문 바로가기
Android

Datastore

by 들풀민들레 2022. 7. 29.

 

수업시간에 나왔던 질문을 정리한 글입니다.

 

 

 

 

Datastore 는 안드로이드 앱에서 데이터를 영속적으로 저장하기 위한 JetPack 의 구성요소입니다.

Datastore 의 데이터 저장 방식이 키-값 형태임으로 기존에 사용하였던 Preference 와 유사한 구조로 데이터를 저장합니다. 그런데 값에 객체를 저장할 수 있음으로 Preference 보다는 조금더 구조화된 데이터를 저장할 수 있습니다.

물론 대량의 데이터가 구조화 되어 저장되어야 한다면 Room 을 이용하는 것이 좋습니다.

 

아래는 구글의 가이드입니다

 

복잡한 대규모 데이터 세트, 부분 업데이트, 참조 무결성을 지원해야 할 경우에는 Datastore 대신 Room을 사용하는 것이 좋습니다. DataStore는 소규모 단순 데이터 세트에 적합하며 부분 업데이트나 참조 무결성은 지원하지 않습니다.

 

또한 Datastore 는 코틀린의 Coroutine Flow 를 이용하여 비동기적으로 데이터를 저장합니다.

 

Preferences Datastore

 

Datastore 에서는 Preferences Datastore  Proto Datastore두가지 방법의 데이터 저장 기법을 제공합니다. Preferences Datastore 는 데이터 스키마가 정의되지 않는 방법으로 다양한 형태의 데이터를 저장하기 위해 사용됩니다. Proto Datastore  Protocol Buffer 를 이용해 데이터 스키마를 정의하고 그 스키마에 맞는 데이터를 저장하는 방법을 제공합니다.

 

먼저 Preferences Datastore 를 사용하는 방법에 대해 살펴 보겠습니다. Preferences Datastore 는 기존의 SharedPreference 와 비슷한 저장 방법입니다. -값 형태로 데이터가 저장되며 값은 숫자, 문자열등의 기초 데이터입니다.

 

val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "myStore")

 

myStore 라는 이름을 가지는 DataStore 객체를 생성하는 코드로 이용 편의성을 위해 Context 객체에 등록하였습니다.

 

Key

 

DataStore 는 데이터를 키-값 형태로 저장합니다. 데이터를 저장하거나 획득하기 위해서는 키가 있어야 하는데 키는 Preferences.Key 객체로 표현됩니다. 키를 만들기 위한 intPreferencesKey(), booleanPreferencesKey(), doublePreferencesKey(), floatPreferencesKey(), longPreferencesKey(), stringPreferencesKey(), stringSetPreferencesKey() 의 함수가 제공됩니다.

 

intPreferencesKey()  Preferences.Key<Int> 타입이 리턴되며 이 키에 저장되는 데이터는 Int 타입이 됩니다. 결국 Key 객체를 만들면서 어떤 타입의 데이터를 저장하기 위한 키인지를 지정하기 위해서 키를 Key 객체로 표현하는 것입니다.

 

val KEY_EXAMPLE_COUNTER = intPreferencesKey("counter")

 

intPreferencesKey() 의 매개변수는 키 이름이며 이렇게 만들어진 Key 객체로 저장하거나 획득하는 데이터 타입은 Int 타입이 됩니다.

 

 

 

데이터 저장

 

DataStore 에는 데이터를 저장하기 위한 edit() 와 데이터를 읽기 위한 data 를 제공합니다.

 

DataStore<Preferences>.edit(transform: suspend (MutablePreferences) -> Unit)

 

edit() 함수에 데이터를 저장하는 함수를 매개변수로 지정하며 매개변수 타입은 데이터 변경이 가능한 MutablePreferences 입니다.

 

dataStore.edit { settings ->
    val currentCounterValue = settings[KEY_EXAMPLE_COUNTER] ?: 0
    settings[KEY_EXAMPLE_COUNTER] = currentCounterValue +1
}

 

KEY_EXAMPLE_COUNT 키로 데이터를 획득하고 획득된 데이터에 1을 더한 후 다시 동일 키로 데이터를 저장한 예입니다.

 

데이터 획득

 

DataStore 의 데이터 획득은 data 프로퍼티를 이용하면 되는데 이 data 프로퍼티는 Flow 타입입니다.  DataStore 의 데이터가 Flow 로 전달된다는 이야기 입니다.

 

val exampleCounterFlow: Flow<Int> = dataStore.data
    .map { preferences ->
        preferences[KEY_EXAMPLE_COUNTER] ?: 0
    }

CoroutineScope(Dispatchers.Main).launch {
    val result = exampleCounterFlow.first()
    Toast.makeText(this@MainActivity, "$result", Toast.LENGTH_SHORT).show()
}

위의 예는 KEY_EXAMPLE_COUNT 키로 획득한 데이터를 Flow  first() 함수를 이용해 얻어서 토스트로 띄운 경우입니다.

 

 

 

 

Protocol Buffer

 

Datastore 를 이해하기 위해서는 먼저 Protocol Buffer 에 대한 이해가 필요합니다. Protocol Buffer 란 구글에서 오픈소스로 공개한 데이터 표현 언어입니다. 즉 안드로이드 만을 위한 언어가 아니며 다양한 곳에서 사용 가능합니다.

Protocol buffer 를 줄여서 protobuf 혹은 pb 라고 부르기도 합니다.

 

Protocol Buffer 의 주 목적은 데이터 표현입니다.

흔히 소프트웨어에서 이용하는 데이터는 XML, JSON 등으로 표현됩니다. 많이 사용되고 있지만 문자열 형식으로 데이터가 표현됨으로 사이즈가 크다는 단점이 있습니다. 물론 데이터 사이즈가 커지면 압축률이나 처리 속도에 단점을 가질 수 밖에 없습니다.

 

Protocol Buffer 는 데이터를 바이너리 형태로 표현합니다. 그럼으로 XML, JSON 에 비해 더 적은 용량으로 데이터를 저장할 수 있고 그로인하여 압축률, 처리속도를 좋게 할 수 있습니다.

그런데 바이너리 형태로 표현된 데이터는 사람이 해석할 수도 없고 대부분의 응용 프로그램에서 활용하기 너무 어려워 집니다. 그래서 Protocol Buffer 가 필요한 것입니다.

 

Protocol Buffer 은 일종의 데이터 표현 언어이며 이 Protocol Buffer 를 이용해 데이터 스키마를 정의합니다. 스키마 정의는 .proto 파일에 작성됩니다.

데이터 스키마를 정의한 .proto 파일을 컴파일러를 이용해 원하는 언어로 컴파일해서 사용합니다.

이렇게 되면 .proto 에 정의된 스키마를 참조하여 데이터를 인코딩 혹은 디코딩할 수 있게 됩니다.

 

간단하게 Protocol Buffer 를 이용해 데이터는 선언하는 방법에 대해 살펴 보겠습니다.

자세한 Protocol Buffer 선언 방법은 https://developers.google.com/protocol-buffers/docs/proto3?hl=ko 을 참조 하세요.

 

syntax = "proto3";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}

 

message 예약어로 하나의 데이터 메시지를 선언합니다. 위의 예는 SearchRequest 라는 이름의 메시지를 선언한 것입니다. 이 메시지에는 query, page_number, result_per_page 라는 필드가 선언되어 있습니다. 필드는 타입과 이름으로 정의되며 타입에는 double, float, int32, int64, bool, bytes 등이 선언될 수 있습니다.

 

핃드를 선언할 때 숫자값을 지정하는데 이는 바이너리 인코딩시에 사용되는 필드의 식별자입니다. 1부터 536,870,911 까지 지정이 가능한데 가능하다면 1~15 범위의 숫자를 사용하길 권장합니다.

 

message SearchResponse {
  message Result {
    string url = 1;
    string title = 2;
    repeated string snippets = 3;
  }
  repeated Result results = 1;
}

 

필드의 타입이 다른 message 일 수도 있습니다. 또한 하나의 message 내에 다른 message 를 선언하는 것도 가능합니다. 위의 예에서 results  SearchResponse 의 필드인데 Result 메시지 타입으로 선언되어 있습니다.

 

message SomeOtherMessage {
  SearchResponse.Result result = 1;
}

 

하나의 message 안에 선언된 message 는 다른 곳에서 사용이 가능합니다.

 

필드를 선언하면서 required, optional, repeated 예약어를 추가할 수 있습니다.

 

  • required : 필수 입력 필드, 이 필드에 값이 꼭 대입되어야 한다는 의미
  • optional : 값이 대입되지 않아도 되는 필드, 만약 default 값이 선언되어 있다면 값이 대입되지 않았을 때 default 값이 적용되며 만약 default 값이 선언되지 않았다면 system default 값이 적용됩니다. 숫자 타입은 0, bool 타입은 false, String 은 empty 입니다.
  • repeated : 값이 여러 개 지정될 수 있는 필드를 선언, 일종의 배열 개념이라고 보면 됩니다.

 

 

 

Proto Datastore

 

Proto DataStore protocol buffer 에 선언된 유형의 객체를 키-값 형태로 저장하는 방법을 제공합니다.

 

설정

 

안드로이드 스트디오에서 protobuf 를 이용하기 위해서 플러그인을 설치합니다.

 

File->Settings

 

 

프로젝트 수준의 build.gradle 에 아래와 같이 plugin 을 추가합니다.

 

id 'com.google.protobuf' version '0.8.18' apply false

 

모듈 수준의 build.gradle 파일을 아래와 같이 설정합니다.

 

plugins {
    …………………………
    id "com.google.protobuf"
}
………………………………
dependencies {


    implementation 'androidx.datastore:datastore-core:1.0.0'
    implementation("com.google.protobuf:protobuf-java:3.19.4")
    implementation("com.google.protobuf:protobuf-kotlin:3.19.4")

}

protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:3.14.0"
    }
    generateProtoTasks {
        all().each { task ->
            task.builtins {
                java {
                    option 'lite'
                }
            }
        }
    }
}

 

protobuf 파일 선언

 

protobuf 파일은 확장자가 proto 인 파일이며 모듈의 src/main/proto 폴더에 선언되어 있어야 합니다.

 

만들어진 proto 파일을 아래처럼 작성합니다. 간단하게 숫자값을 저장하는 Settings 라는 이름의 메시지를 선언한 것입니다.

 

syntax = "proto3";

option java_package = "com.example.test4_data_store";
option java_multiple_files = true;

message Settings {
  int32 example_counter = 1;
}

 

proto 파일에 선언한 메시지는 데이터 유형입니다. 그런데 이 유형의 데이터를 프로그램에서 이용하기 위해서는 프로그램에 맞는 클래스로 만들어 저야 합니다. 즉 안드로이드 프로그램에서 위의 Settings 유형의 데이터를 표현하기 위한 Settings 라는 클래스가 만들어져야 합니다.

 

proto 파일에 의한 클래스를 만들기 위해서는 모듈을 빌드 시켜야 합니다. proto 파일을 만든 후  Build->Make Module 메뉴를 클릭해 클래스가 만들어지게 합니다.

 

DataStore 객체 선언

 

Protobuf 를 지원하는 DataStore 객체를 선언하기 위해서는 먼저 Serializer 를 구현하는 클래스를 선언해야 합니다. protobuf 를 지원하는 DataStore 객체 생성시 생성자 매개변수로 이 객체를 지정하게 되며 DataStore 에서는 이 Serializer 객체의 readFrom(), writeTo() 함수를 호출하여 어떻게 데이터를 읽고 써야 하는지를 참조하게 됩니다.

Serializer 의 제네릭 정보로 지정하는 Settings proto 파일에 정의한 메시지이며 저장하고 읽기 위한 데이터 유형입니다.

 

 

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)
}

 

이렇게 만들어진 Serializer 객체를 지정하여 DataStore 객체를 생성합니다.

 

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

 

데이터 쓰기

 

protobuf 를 지원하는 DataStore updateData() 함수로 데이터를 저장하며 data 프로퍼티로 데이터를 획득하게 됩니다. currentSettings proto 파일에 정의한 메시지 타입의 객체입니다.

 

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

 

데이터 읽기

 

val exampleCounterFlow: Flow<Int> = settingsDataStore.data
    .map { settings ->
        settings.exampleCounter
    }