Developing Myself Everyday
article thumbnail

 

이 게시글은 아래의 공식 문서를 보고 작성했습니다.

 

앱 아키텍처: 데이터 영역 - Datastore - Android 개발자  |  Android Developers

데이터 영역 라이브러리에 관한 이 앱 아키텍처 가이드를 통해 Preferences DataStore 및 Proto DataStore, 설정 등을 알아보세요.

developer.android.com


 

 

 

SharedPreferences


DataStore를 이야기하기 전에 SharedPreferences를 이야기하는 것은 당연할 것입니다. 왜냐면 2개의 개념 다 안드로이드 애플리케이션에서 데이터를 저장하고 관리하는 방법이고 DataStoreSharedPreferencesd을 개선한 버전이기 때문입니다.

 

SharedPreferences는 간단한 키-값 형태의 데이터를 저장하기 위한 메커니즘으로, 주로 설정 값이나 간단한 사용자 데이터를 저장하는 데 사용됩니다. SharedPreferences는 기본적으로 동기적으로 작동하며, 단일 파일 내에 키-값 쌍을 저장합니다.

 

동기적으로 작동한다는 게 맘에 들지는 않지만 일단 넘어가겠습니다.

 

 

SharedPreferences구현

SharedPreferences 객체는 앱에서 여러번 사용되기 때문에 싱글턴으로 생성하는 것이 좋습니다. 다만 여기선 간략하게 넘어가기 위해 인스턴스를 생성해 보겠습니다.

val sharedPreferences = getSharedPreferences("MyAppPreferences", Context.MODE_PRIVATE)

`getSharedPreferences`를 통해 인스턴스를 얻을 수 있습니다. Context.MODE_PRIVATE 모드로 SharedPreferences를 생성하면 해당 앱에서만 접근 가능하며, 다른 앱과 데이터가 공유되지 않습니다.

 

 

SharedPreferences에 데이터를 저장할 때는 해당 데이터 타입에 맞게 put 메서드를 사용합니다.

val editor = sharedPreferences.edit()
editor.putString("username", "john_doe")
editor.putInt("age", 25)
editor.putBoolean("is_logged_in", true)
editor.apply() // 변경 사항을 저장

 

 

저장된 데이터를 읽을 때는 SharedPreferences의 get 메서드를 사용합니다.

val username = sharedPreferences.getString("username", "")
val age = sharedPreferences.getInt("age", 0)
val isLoggedIn = sharedPreferences.getBoolean("is_logged_in", false)

 

 

데이터를 삭제하려면 remove 메서드를 사용합니다.

editor.remove("username") 
editor.apply() // 변경 사항을 저장

 

 

SharedPreferences의 단점

1. Synchronous Blocking: SharedPreferences는 동기적으로 작동하며, 값을 읽거나 쓸 때 블로킹될 수 있습니다. 이는 UI 스레드에서 사용할 경우 앱의 응답성을 저하시킬 수 있습니다.

2. No Type Safety: SharedPreferences는 모든 데이터를 String 형태로 저장하며, 별도의 변환 없이 사용자 정의 객체를 저장할 수 없습니다. 이로 인해 데이터 유형에 대한 안정성이 떨어질 수 있습니다.

3. Limited Data Structure: SharedPreferences는 단순한 Key-Value 저장소로서 복잡한 데이터 구조를 지원하지 않습니다. 복잡한 데이터를 저장하려면 번거로운 변환 작업이 필요합니다.

4. Lack of Data Observation: SharedPreferences는 데이터 변경을 감지하거나 이에 대한 알림을 제공하지 않습니다. 따라서 데이터 변경 시 다른 구성 요소들에게 변화를 알리기 위해 추가적인 코드가 필요합니다.

 

이러한 단점을 해결한 것이 DataStore입니다.

 

 

 

DataStore


DataStore는 안드로이드 Jetpack 라이브러리의 일부로, 데이터를 안정적이고 일관된 방식으로 저장하기 위해 설계되었습니다. DataStore는 기본적으로 비동기적으로 작동하며, 다양한 백엔드 스토리지를 지원하여 데이터를 저장할 수 있습니다. Datastore는 Kotlin 코루틴 및 Flow를 사용하여 비동기적이고 일관된 트랜잭션 방식으로 데이터를 저장합니다.

 


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

 

DataStore은 SharedPreferences와 다르게 Thread Safe 하고 Non-Blokcing입니다.

 

DataStore은 프로토콜 버퍼를 사용하는 Proto DataStore와 Key-Value를 사용하는 Preferences DataStore 구현을 제공합니다.

 

이를 비교한 그림은 아래와 같습니다.

 

 

Proto DataStore Preferences DataStore을 더 자세히 비교하면 아래와 같습니다.

 

 

 

 

Room vs DataStore

그럼 그냥 Room을 사용하면 되지 않을까 라는 생각이 들 수 있습니다. 구글은 아래의 그림으로 이런 고민을 해결해 줍니다.

 

 

Preferences DataStore


Preferences DataStore 구현

데이터 모델은 구성하는 방식은 Key-Value로 SharedPreferences와 같습니다.

 

속성 위임을 사용하여 preferencesDataStore Datastore<Preferences>의 인스턴스를 만듭니다. kotlin 파일의 최상위 수준에서 인스턴스를 한 번 호출한 후 애플리케이션의 나머지 부분에서는 이 속성을 통해 인스턴스에 액세스 합니다. 이렇게 하면 더 간편하게 DataStore를 싱글톤으로 유지할 수 있습니다. 

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

 

 

Preferences DataStore 읽기

Preferences의 데이터를 가져올 때 Flow를 사용하게 됩니다. 키 유형 함수를 통해 인스턴스에 저장된 값의 키를 설정하고 DataStore에 있는 해당하는 키의 값을 가져옵니다.

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

 

 

Preferences DataStore 쓰기

데이터를 쓰기 위해 Datastore는 정지 함수 `DataStore.edit(transform: suspend (MutablePreferences) -> Unit)`를 제공합니다. 이 함수는 Datastore에서 트랜잭션 방식으로 상태를 업데이트할 수 있는 transform 블록을 허용합니다.

suspend fun incrementCounter() {
  context.dataStore.edit { settings ->
    val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0
    settings[EXAMPLE_COUNTER] = currentCounterValue + 1
  }
}

 

 

 

 

 

Proto Datastore


SharedPreferencesPreferences Datastore의 단점 중 하나는 스키마를 정의하거나 올바른 유형으로 키에 액세스 할 방법이 없다는 점입니다. Proto Datastore는 프로토콜 버퍼를 사용해 스키마를 정의하는 방식으로 이 문제를 해결합니다. Proto Datastore를 사용하면 저장된 유형을 인식하고 제공하기 때문에 키를 사용할 필요가 없게 됩니다.


프로토콜 버퍼는 구조화된 데이터를 직렬화하는 메커니즘입니다. 개발자가 데이터를 어떻게 구조화할지 한 번 정의하면 컴파일러가 구조화된 데이터를 쉽게 쓰고 읽을 수 있도록 소스 코드를 생성합니다.

 

 

Proto 파일 만들기

Proto Datastore를 사용하려면 [app/src/main/proto/] 디렉터리의 proto 파일에 사전 정의된 스키마가 있어야 합니다. 

syntax = "proto3";

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

message Settings {
  int32 example_counter = 1;
}

 

 

Serializer 구현

proto 파일에 정의한 데이터 유형을 읽고 쓰는 방법을 Datastore에 알리려면 serializer를 구현해야 합니다. 또한 serializer는 디스크에 데이터가 없는 경우(아직 파일이 생성되지 않은 경우) 반환될 기본값을 정의합니다.

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

 

 

Proto DataStore 만들기

Datastore 인스턴스를 만들기 위해 dataStore 위임을 사용하며 수신기로 Context를 사용합니다. 이 위임에는 다음과 같은 두 가지 필수 매개변수가 있습니다.

  • Datastore가 작동할 파일의 이름
  • Datastore에 사용되는 유형을 위한 serializer.
val Context.settingsDataStore: DataStore<Settings> by dataStore(
  fileName = "settings.pb",
  serializer = SettingsSerializer
)

 

 

Proto DataStore 읽기

데이터를 읽는 방식은 Preferences DataStore에서 데이터를 읽는 방식과 거의 같습니다.

val exampleCounterFlow: Flow<Int> = context.settingsDataStore.data
  .map { settings ->
    // The exampleCounter property is generated from the proto schema.
    settings.exampleCounter
  }

 

 

 

 

Proto DataStore 쓰기

Proto Datastore는 저장된 객체를 트랜잭션 방식으로 업데이트하는 `updateData()`함수를 제공합니다.  `updateData()`함수는 데이터의 현재 상태를 데이터 유형의 인스턴스로 제공하고 원자적 읽기-쓰기-수정 작업을 통해 트랜잭션 방식으로 데이터를 업데이트합니다.

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

 

 

 

마무리하며

이렇게 해서 안드로이드 애플리케이션에서 데이터를 저장하고 관리하는 방법인 SharedPreferences와 DataStore에 대해 알아보았습니다. 이전에 했던 프로젝트에선 SharedPreferences를 사용해서 로그인을 처리했었습니다. 앞으로 다른 프로젝트를 진행할 때 DataStore을 사용해보고 싶다는 생각이 듭니다.

'Android > AAC' 카테고리의 다른 글

[AAC] WorkManager  (0) 2023.08.25
[AAC] Paging 라이브러리  (0) 2023.08.16
[AAC] LiveData  (0) 2023.08.12
[AAC] LifeCycle  (0) 2023.08.03
profile

Developing Myself Everyday

@배준형

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!