관리 메뉴

나만을 위한 블로그

[Android] DataStore란? DataStore 예제 본문

Android

[Android] DataStore란? DataStore 예제

참깨빵위에참깨빵 2022. 7. 24. 02:01
728x90
반응형

간단한 데이터를 저장하는 방법으로 지금도 자주 사용되는 건 쉐어드 프리퍼런스일 것이다.

그런데 이것을 대신해서 사용할 수 있는 것을 안드로이드에서 만든 모양이다. 그게 이 포스팅의 제목이다.

DataStore란 이름에서부터 뭐 하는 놈인지는 느낌이 온다. 안드로이드 디벨로퍼에선 어떻게 설명하는지 확인해보자.

 

https://developer.android.com/topic/libraries/architecture/datastore?hl=ko 

 

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

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

developer.android.com

Jetpack DataStore는 프로토콜 버퍼를 사용해서 키밸류 쌍 또는 유형이 지정된 객체를 저장할 수 있는 데이터 저장소 솔루션이다. DataStore는 코루틴 및 flow를 써서 비동기적이고 일관된 트랜잭션 방식으로 데이터를 저장한다. 현재 쉐어드 프리퍼런스를 써서 데이터를 저장하고 있다면 DataStore로 이전하는 게 좋다
복잡한 대규모 데이터 세트, 부분 업데이트, 참조 무결성을 지원해야 할 경우 Room을 쓰는 게 좋다. DataStore는 소규모 단순 데이터 세트에 적합하며 부분 업데이트나 참조 무결성은 지원하지 않는다

DataStore는 Preferences Store, Proto DataStore 두 구현을 제공한다
- Preferences DataStore : 키를 써서 데이터를 저장하고 데이터에 접근한다. 유형 안전성을 제공하지 않으며 사전 정의된 스키마가 필요하지 않다
- Proto DataStore : 맞춤 데이터 유형의 인스턴스로 데이터를 저장한다. 유형 안전성을 제공하며 프로토콜 버퍼를 써서 스키마를 정의해야 한다

 

쉐어드처럼 키밸류 형태로 데이터를 저장할 수도 있지만 객체도 저장할 수 있다. 그런데 프로토콜 버퍼가 무슨 말인지 모르겠어서 찾아봤다.

 

https://developers.google.com/protocol-buffers?hl=ko 

 

Protocol Buffers  |  Google Developers

Protocol buffers are a language-neutral, platform-neutral extensible mechanism for serializing structured data.

developers.google.com

프로토콜 버퍼는 구조화된 데이터를 직렬화하기 위한 구글의 언어 중립적, 플랫폼 중립적, 확장 가능한 매커니즘이다. XML을 생각할 수 있지만 더 작고 빠르고 간단하다. 데이터 구조화 방법을 한 번 정의한 다음 특수 생성 소스코드를 써서 다양한 데이터 스트림, 언어를 사용해 구조화된 데이터를 쉽게 쓰고 읽을 수 있다

 

https://bcho.tistory.com/1182

 

구글 프로토콜 버퍼 (Protocol buffer)

구글 프로토콜 버퍼 조대협 (http://bcho.tistory.com) 텐서 플로우로 모델을 개발하다가 학습이 끝난 모델을 저장하여, 예측하는 데 사용하려고 하니, 모델을 저장하는 부분이 꽤나 복잡하여 찾아보니

bcho.tistory.com

프로토콜 버퍼는 구글에서 개발하고 오픈소스로 공개한 직렬화 자료구조다. 다양한 언어를 지원하며 직렬화 속도가 빠르고 직렬화된 파일 크기도 작아서 Avro 파일 포맷과 함께 많이 사용된다...(중략)...프로토콜 버퍼는 한 파일에 최대 64MB까지 지원할 수 있고 JSON 파일로 전환이 가능하다. 반대도 가능하다

 

https://velog.io/@pdg03092/%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C-%EB%B2%84%ED%8D%BC%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80Protocol-Buffer

 

프로토콜 버퍼란 무엇인가(Protocol Buffer) 서버, 클라이언트 예제와 함께 feat. express & Vue

회사에서 주로 mfc를 다루다가 최근 웹프론트엔드 파트 업무를 맡게 되면서 한꺼번에 html, css, javascript, vue를 접하게 되었다...혼돈... 틀린 부분이 있다면 알려주시면 감사하겠습니다(❁´◡\`❁)

velog.io

프로토콜 버퍼를 사용하면 (역)직렬화 속도가 빠르고 직렬화된 파일 크기를 월등히 줄일 수 있어 대용량 데이터를 처리할 때 성능이 좋다. JSON과 달리 사람이 읽기 어렵고 proto 파일이 없으면 데이터 해석이 불가능하단 단점도 있지만 스키마가 존재해서 이에 따라 쉽게 (역)직렬화가 가능하기 때문에 내부 서비스에서 사용하면 효과적이다

 

쉽게 말해서 대용량 데이터를 처리할 때에는 JSON보다 처리 속도가 빠른 자료구조라는 것 같다. 이걸 사용해서 키밸류 형태의 데이터와 객체를 저장할 수 있는 게 DataStore라는 것 같다.

본론으로 돌아와서, 다른 곳에선 DataStore를 어떻게 설명하는지 확인해봤다.

아래 링크는 DataStore에 대해 설명하는 포스팅이지만 이 다음 포스팅에선 DI, 직렬화, 테스트 등 코틀린 예제를 설명과 같이 보여주고 있다. 쭉 읽어보면 DataStore에 대한 전반적인 이해는 가능할 거라고 생각된다.

 

https://medium.com/androiddevelopers/introduction-to-jetpack-datastore-3dc8d74139e7

 

Introduction to Jetpack DataStore

DataStore is a Jetpack data storage library that provides a safe and consistent way to store small amounts of data, such as preferences or…

medium.com

DataStore는 기본 설정 또는 응용 프로그램 상태 같은 소량의 데이터를 안전하고 일관되게 저장하는 방법을 제공하는 제트팩 데이터 저장소 라이브러리다. 비동기 데이터 저장을 가능하게 하는 코루틴과 Flow를 기반으로 한다. 쓰레드 세이프하고 차단되지 않기 때문에 쉐어드를 대체하는 걸 목표로 한다. 2가지 다른 구현을 제공한다. 유형이 지정된 객체(프로토콜 버퍼 지원)를 저장하는 Proto DataStore,  키밸류 쌍을 저장하는 Preferences DataStore다
앱에서 쉐어드를 쓰면서 재현하기 어려운 쉐어드 관련 문제를 경험했을 수 있다. 포착되지 않은 예외(uncaught exceptions)로 인해 분석에서 이상한 충돌이 발생하거나 호출할 때 UI 쓰레드를 차단하거나 앱 전체에서 일관되지 않고 지속되는 데이터가 발생한다. DataStore는 이런 모든 문제를 해결하기 위해 구축됐다. 아래는 쉐어드와 DataStore 간의 직접적인 비교다

대부분의 데이터 저장소 API를 쓰면 데이터가 수정될 때 비동기식으로 알림을 받아야 하는 경우가 많다. 쉐어드는 일부 비동기 지원을 제공하지만 OnSharedPreferenceChangeListener를 통해 변경된 값에 대한 업데이트를 가져오는 경우에만 제공된다. 그러나 이 콜백은 메인 쓰레드에서 호출된다. 마찬가지로 파일 저장 작업을 백그라운드로 오프로드하려면 SharedPreferences.apply()를 쓸 수 있지만 이렇게 하면 fsync()의 UI 쓰레드가 차단돼 잠재적으로 버벅거림 및 ANR을 유발할 수 있다. 이는 서비스가 시작 or 중지되거나 액티비티가 일시 중지(pauses) 또는 중지(stops)될 때마다 발생할 수 있다.
이에 비해 DataStore는 코루틴과 Flow의 기능을 사용해서 데이터 검색 및 저장을 위한 완전 비동기식 API를 제공해 UI 쓰레드를 차단할 위협을 줄인다. Flow에 익숙하지 않은 사람들에겐 비동기식으로 계산 가능한 값의 흐름일 뿐이다...(중략)

 

https://www.raywenderlich.com/18348259-datastore-tutorial-for-android-getting-started

 

DataStore Tutorial For Android: Getting Started

In this tutorial you’ll learn how to read and write data to Jetpack DataStore, a modern persistance solution from Google.

www.raywenderlich.com

DataStore는 유형이 지정된 객체를 저장하기 위해 키밸류 쌍 또는 프로토콜 버퍼를 써서 간단한 데이터 조각을 유지하기 위한 구글의 새로운 솔루션이다. 코루틴 및 Flow를 써서 모든 트랜잭션을 비동기식으로 만들어 모든 데이터 저장 및 가져오기 작업의 성능, 안전성을 높인다. 제트팩 도구 세트의 일부라서 Jetpack DataStore라고도 한다

 

다른 글에서 설명하는 내용도 위 포스팅들과 안드로이드 디벨로퍼와 별반 차이가 없기 때문에 생략하고 간단한 예제로 DataStore를 사용해 본다.

의존성은 안드로이드 디벨로퍼에 오늘 날짜인 22.07.22 기준으로 기재된 걸 사용했고 Preferences DataStore만 확인한다.

 

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

 

그리고 User라는 파일을 만들고 그 안에 이 코드를 넣는다.

 

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 = "user_prefs")

 

by 키워드를 써서 DataStore<Preferences>의 구현을 preferencesDataStore()에 맡기는 Context 확장 프로퍼티를 만들었다. 이 밑에 타입 별로 DataStore에 저장하거나 가져오는 함수를 구현해도 되지만 여기선 이것만 사용한다.

다음으로 이 User를 사용하는 UserManager 클래스를 만든다.

 

import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

class UserManager(
    private val dataStore: DataStore<Preferences>
) {
    companion object {
        val USER_AGE_KEY = intPreferencesKey("USER_AGE")
        val USER_FIRST_NAME_KEY = stringPreferencesKey("USER_FIRST_NAME")
        val USER_LAST_NAME_KEY = stringPreferencesKey("USER_LAST_NAME")
        val USER_GENDER_KEY = booleanPreferencesKey("USER_GENDER")
    }

    suspend fun storeUser(
        age: Int,
        frontName: String,
        lastName: String,
        isMale: Boolean
    ) {
        dataStore.edit {
            it[USER_AGE_KEY] = age
            it[USER_FIRST_NAME_KEY] = frontName
            it[USER_LAST_NAME_KEY] = lastName
            it[USER_GENDER_KEY] = isMale
        }
    }

    val userAgeFlow: Flow<Int?> = dataStore.data.map {
        it[USER_AGE_KEY]
    }

    val userFirstNameFlow: Flow<String?> = dataStore.data.map {
        it[USER_FIRST_NAME_KEY]
    }

    val userLastNameFlow: Flow<String?> = dataStore.data.map {
        it[USER_LAST_NAME_KEY]
    }

    val userGenderFlow: Flow<Boolean?> = dataStore.data.map {
        it[USER_GENDER_KEY]
    }

}

 

DataStore는 내부적으로 코루틴을 사용하기 때문에 User 데이터를 저장하는 storeUser()의 앞에 suspend 키워드가 붙은 걸 볼 수 있다.

그리고 intPreferencesKey(), stringPreferencesKey() 등의 함수가 보이는데 이것은 저장될 값들의 키를 정의하는 함수다. 타입에 맞는 함수를 쓰고 그 안에 키로 사용할 문자열을 넣어주면 된다. 같은 이름의 키가 여러 개 있다면 ClassCastException이 발생한다.

 

이제 액티비티를 만들어준다. 데이터바인딩을 사용했다.

 

<?xml version="1.0" encoding="utf-8"?>
<layout 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">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".datastore.DataStoreTestActivity">

        <Button
            android:id="@+id/btn_save"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="60dp"
            android:padding="18dp"
            android:text="Save user"
            android:textColor="@android:color/white"
            android:textSize="15sp"
            android:textStyle="bold"
            app:layout_constraintEnd_toEndOf="@+id/switch_gender"
            app:layout_constraintStart_toStartOf="@+id/switch_gender"
            app:layout_constraintTop_toBottomOf="@+id/switch_gender" />

        <EditText
            android:id="@+id/et_lname"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:ems="10"
            android:hint="이름"
            app:layout_constraintEnd_toEndOf="@+id/et_fname"
            app:layout_constraintStart_toStartOf="@+id/et_fname"
            app:layout_constraintTop_toBottomOf="@+id/et_fname" />

        <EditText
            android:id="@+id/et_age"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:ems="10"
            android:hint="나이"
            android:inputType="number"
            app:layout_constraintEnd_toEndOf="@+id/et_lname"
            app:layout_constraintStart_toStartOf="@+id/et_lname"
            app:layout_constraintTop_toBottomOf="@+id/et_lname"
            tools:layout_editor_absoluteY="317dp" />

        <EditText
            android:id="@+id/et_fname"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="30dp"
            android:ems="10"
            android:hint="나머지 이름"
            app:layout_constraintEnd_toEndOf="@+id/tv_gender"
            app:layout_constraintStart_toStartOf="@+id/tv_gender"
            app:layout_constraintTop_toBottomOf="@+id/tv_gender"
            tools:layout_editor_absoluteY="178dp" />

        <TextView
            android:id="@+id/tv_fname"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="30dp"
            android:textStyle="bold"
            android:textColor="@color/colorPrimaryDark"
            android:textSize="20sp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/tv_lname"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:textStyle="bold"
            android:textColor="@color/colorPrimaryDark"
            android:textSize="20sp"
            app:layout_constraintEnd_toEndOf="@+id/tv_fname"
            app:layout_constraintStart_toStartOf="@+id/tv_fname"
            app:layout_constraintTop_toBottomOf="@+id/tv_fname" />

        <TextView
            android:id="@+id/tv_age"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:textStyle="bold"
            android:textColor="@color/colorPrimaryDark"
            android:textSize="20sp"
            app:layout_constraintEnd_toEndOf="@+id/tv_lname"
            app:layout_constraintStart_toStartOf="@+id/tv_lname"
            app:layout_constraintTop_toBottomOf="@+id/tv_lname" />

        <TextView
            android:id="@+id/tv_gender"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:textStyle="bold"
            android:textColor="@color/colorPrimaryDark"
            android:textSize="20sp"
            app:layout_constraintEnd_toEndOf="@+id/tv_age"
            app:layout_constraintStart_toStartOf="@+id/tv_age"
            app:layout_constraintTop_toBottomOf="@+id/tv_age" />

        <androidx.appcompat.widget.SwitchCompat
            android:id="@+id/switch_gender"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:text="성별"
            android:textSize="20sp"
            app:layout_constraintEnd_toEndOf="@+id/et_age"
            app:layout_constraintStart_toStartOf="@+id/et_age"
            app:layout_constraintTop_toBottomOf="@+id/et_age" />

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.asLiveData
import com.example.kotlinprac.R
import com.example.kotlinprac.databinding.ActivityDataStoreBinding
import com.example.kotlinprac.databinding.ActivityDataStoreTestBinding
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch

class DataStoreTestActivity : AppCompatActivity() {

    private lateinit var binding: ActivityDataStoreTestBinding
    private lateinit var userManager: UserManager
    private var age = -1
    private var frontName = ""
    private var lastName = ""
    private var gender = ""

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_data_store_test)

        userManager = UserManager(dataStore)

        binding.run {
            buttonSave()
            observeData()
        }
    }

    private fun ActivityDataStoreTestBinding.buttonSave() {
        btnSave.setOnClickListener {
            frontName = etFname.text.toString()
            lastName = etLname.text.toString()
            age = etAge.text.toString().toInt()
            val isMale = switchGender.isChecked

            CoroutineScope(IO).launch {
                userManager.storeUser(age, frontName, lastName, isMale)
            }
        }
    }

    private fun observeData() {
        userManager.userAgeFlow.asLiveData().observe(this) {
            if (it != null) {
                age = it
                binding.tvAge.text = it.toString()
            }
        }

        userManager.userFirstNameFlow.asLiveData().observe(this) {
            if (it != null) {
                frontName = it
                binding.tvFname.text = it
            }
        }

        userManager.userLastNameFlow.asLiveData().observe(this) {
            if (it != null) {
                lastName = it
                binding.tvLname.text = it
            }
        }

        userManager.userGenderFlow.asLiveData().observe(this) {
            if (it != null) {
                gender = if (it) "남성" else "여성"
                binding.tvGender.text = gender
            }
        }
    }

}

 

버튼을 누르면 입력된 값들을 DataStore에 저장하고, 저장된 값이 바뀌면 이를 관찰해서 텍스트뷰에 표시하는 간단한 예시다. observeData() 안에서 데이터를 읽을 때 preferencesFlow를 LiveData로 바꾸는 게 보인다.

생각보다 엄청나게 어렵진 않아서 조금만 신경쓰면 쉐어드처럼 간단한 값들은 저장해서 사용할 수 있을 듯하다.

반응형
Comments