Android

[안드로이드]DataStore에 대하여

나맘임 2023. 7. 2. 18:10

DataStore

Jetpack 라이브러리의 구성요소로 포로토콜 버퍼를 사용하여 키-값 쌍 또는 유형이 지정된 객체를 저장할 수 있는 방식

Kotlin 코루틴 및 Flow 를 사용하여 비동기적이고 일관적인 트랜잭션 방식으로 데이터를 저장하는 것이 특징

위 방식 때문에  다중 스레드 환경에 최적화가 되어 있습니다.

기존 SharedPreferences의 대체제로 현재 안드로이드 공식 문서에서 SharedPreferences를 DataStore로 변경하라고 권고 하고 있습니다.

주로 간단한 앱 상태나 로그인 상태 등 간단한 데이터를 저장할 때 사용합니다.

 

프로토콜 버퍼란?
구글에서 만든 구조적 데이터 전송 방식의 일종
쉽게 말해 JSON과 비슷한 역할을 수행

구현 전 주의 사항

1. 같은 프로세스에서 특정 파일의 DataStore 인스턴스를 두 개 이상 만들면 안됩니다.

동일한 프로세스에서 특정 파일의 DataStore 인스턴스가 여러 개 활성화되어 있다면 데이터를 읽거나 업데이트할 때

IllegalStateExeception이 발생합니다.

2. DataStore의 일반 유형은 변경 불가능해야 합니다.

포토토콜 버퍼를 사용하는 Proto DataStore를 사용할 때, 포로토콜 버퍼의 유형 변경이 불가능해야합니다.

이를 Type Safety라고 불리며 이는 잠재적으로 심각하고 포착하기 어려운 버그의 발생을 막기 위함입니다.

이 때문에 Preferences DataStore보다 Proto DataStore가 더 복잡합니다.

TypeSafety(타입 안정성)란?
프로그래밍 언어에서 변수와 데이터의 타입을 강력하게 검사하여 컴파일 단계에서 타입 불일치 오류를 검출하는 기능
코드의 신뢰성을 높이고 버그를 줄이는 데 도움이 됨

3. 동일한 파일에서 SingleProcessDataStore와 MultiProcessDataStore를 함께 사용하면 안됩니다.

싱글 프로세스면 SingleProcessDataStore를, 멀티 프로세스면 MultiProcessDataStore를 사용해야 합니다.

구현 방식

구현 방식은 크게 두 가지 존재합니다.

1. Preferences DataStore

키를 사용하여 데이터를 저장하고 데이터에 액세스를 합니다. 이 구현 방식은 Proto에 비해 유형 안정성을 제공하지 않으나 사전 정의된 스키마가 필요하지 않습니다.

즉, 프로토콜 버퍼의 문법을 배우지 않아도 되기 때문에 Proto DataStore에 비해 구현이 쉽다는 장점이 있습니다.

하지만 TypeSafety가 보장되지 않기 때문에 간단한 Primitive Type이나 간단한 Collection만 저장할 수 있습니다.

 

데이터를 읽을 땐 땐 Flow를 쓸 땐 Dispatchers.IO를 사용합니다.

2. Proto DataStore

Preferences DataStore처럼 키와 벨류의 쌍으로 데이터를 저장할 수도 있지만 TypeSafety를 보장하기 때문에 우리가 원하는 클래스 객체를 데이터로 넣거나 뺄 수 있다는 장점이 존재합니다.하지만 TypeSafety를 보장하기 위해 추가적인 설정과 프로토콜 버퍼를 사용해야하기 때문에 구현이 복잡하다는 특징이 있습니다.

 

 

이 글에선 Preferences DataStore 구현만 해보도록 하겠습니다.

Preferences DataStore 구현 방법

버튼을 누르면 카운트 값이 올라가는 예제를 만들어보겠습니다.

Gradle 설정

    dependencies {
    	implementation"androidx.datastore:datastore-preferences-core:1.0.0"
    	implementation("androidx.datastore:datastore-preferences:1.0.0")
    }

DataStore 선언

Count.kt

import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore

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

kotlin 파일의 최상위 수준에서 인스턴스를 한 번 호출한 후 어플리케이션의 나머지 부분에서 이 속성을 통해 인스턴스에 액세스 합니다.

위 방식으로 DataStore를 싱글톤으로 유지할 수 있습니다.

데이터 쓰기 및 읽기

CountManager.kt

import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

class CountManager(
    private val dataStore: DataStore<Preferences>
) {
    companion object{
        val COUNT = intPreferencesKey("COUNT")
    }

    suspend fun initCounter(int: Int){
        dataStore.edit { settings->
            settings[COUNT] = int
        }
    }


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

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

읽기

	companion object{
    	val COUNT = intPreferencesKey("COUNT")
	}

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

DataStore<Preferences> 인스턴스에 저장해야 하는 각 값의 키를 정의해야 합니다.

그렇기 때문에 PreferencesKey라는 키 유형 함수를 사용해 키를 정의합니다.

위 코드에선 int 값을 저장하기 때문에 intPreferencesKey를 사용합니다.

(각 타입마다 PreferencesKey가 존재합니다)

 

data.map을 사용해 key에 해당하는 value를 가져오고 이 value 값은 Flow로 추출됩니다.

이제 이를 LiveData 로 observe 하여 데이터의 변경을 감지할 수 있게 됩니다.

 

쓰기

suspend fun initCounter(int: Int){
    dataStore.edit { settings->
        settings[COUNT] = int
    }
}

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

데이터를 업데이트하는 edit() 메서드를 제공합니다. 이를 통해 필요한 값을 업데이트할 수 있습니다.

이 edit 메서드의 코드 블록은 단일 트랜잭션으로 취급합니다.

 

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context=".view.main.MainActivity"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >
        <TextView
            android:id="@+id/count"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toTopOf="@+id/button"
            android:textSize="50dp"/>

        <Button
            android:id="@+id/button"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            app:layout_constraintTop_toBottomOf="@+id/count"
            app:layout_constraintBottom_toBottomOf="parent"/>



</androidx.constraintlayout.widget.ConstraintLayout>

MainActivity.kt

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.asLiveData
import com.example.myandroidstudy.databinding.ActivityMainBinding
import com.example.myandroidstudy.model.CountManager
import com.example.myandroidstudy.model.dataStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    private lateinit var countManager: CountManager

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        countManager = CountManager(dataStore)

        binding.apply {
            button.setOnClickListener {
                CoroutineScope(IO).launch {
                    countManager.incrementCounter()
                }
            }
        }

        countManager.getCounter.asLiveData().observe(this){
            if(it != null){
                binding.count.text = it.toString()
            }
        }
    }
}

결과물