일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- rxjava disposable
- 스택 큐 차이
- ar vr 차이
- 객체
- 클래스
- android retrofit login
- 안드로이드 유닛 테스트
- jvm이란
- 플러터 설치 2022
- 2022 플러터 안드로이드 스튜디오
- 2022 플러터 설치
- 멤버변수
- rxjava cold observable
- 서비스 vs 쓰레드
- 자바 다형성
- android ar 개발
- 안드로이드 유닛 테스트 예시
- rxjava hot observable
- 안드로이드 레트로핏 사용법
- 안드로이드 유닛테스트란
- 안드로이드 라이선스
- 안드로이드 라이선스 종류
- 안드로이드 레트로핏 crud
- 안드로이드 os 구조
- jvm 작동 원리
- 큐 자바 코드
- Rxjava Observable
- ANR이란
- 스택 자바 코드
- 서비스 쓰레드 차이
- Today
- Total
나만을 위한 블로그
[Android] DataStore란? DataStore 예제 본문
간단한 데이터를 저장하는 방법으로 지금도 자주 사용되는 건 쉐어드 프리퍼런스일 것이다.
그런데 이것을 대신해서 사용할 수 있는 것을 안드로이드에서 만든 모양이다. 그게 이 포스팅의 제목이다.
DataStore란 이름에서부터 뭐 하는 놈인지는 느낌이 온다. 안드로이드 디벨로퍼에선 어떻게 설명하는지 확인해보자.
https://developer.android.com/topic/libraries/architecture/datastore?hl=ko
Jetpack DataStore는 프로토콜 버퍼를 사용해서 키밸류 쌍 또는 유형이 지정된 객체를 저장할 수 있는 데이터 저장소 솔루션이다. DataStore는 코루틴 및 flow를 써서 비동기적이고 일관된 트랜잭션 방식으로 데이터를 저장한다. 현재 쉐어드 프리퍼런스를 써서 데이터를 저장하고 있다면 DataStore로 이전하는 게 좋다
복잡한 대규모 데이터 세트, 부분 업데이트, 참조 무결성을 지원해야 할 경우 Room을 쓰는 게 좋다. DataStore는 소규모 단순 데이터 세트에 적합하며 부분 업데이트나 참조 무결성은 지원하지 않는다
DataStore는 Preferences Store, Proto DataStore 두 구현을 제공한다
- Preferences DataStore : 키를 써서 데이터를 저장하고 데이터에 접근한다. 유형 안전성을 제공하지 않으며 사전 정의된 스키마가 필요하지 않다
- Proto DataStore : 맞춤 데이터 유형의 인스턴스로 데이터를 저장한다. 유형 안전성을 제공하며 프로토콜 버퍼를 써서 스키마를 정의해야 한다
쉐어드처럼 키밸류 형태로 데이터를 저장할 수도 있지만 객체도 저장할 수 있다. 그런데 프로토콜 버퍼가 무슨 말인지 모르겠어서 찾아봤다.
https://developers.google.com/protocol-buffers?hl=ko
프로토콜 버퍼는 구조화된 데이터를 직렬화하기 위한 구글의 언어 중립적, 플랫폼 중립적, 확장 가능한 매커니즘이다. XML을 생각할 수 있지만 더 작고 빠르고 간단하다. 데이터 구조화 방법을 한 번 정의한 다음 특수 생성 소스코드를 써서 다양한 데이터 스트림, 언어를 사용해 구조화된 데이터를 쉽게 쓰고 읽을 수 있다
프로토콜 버퍼는 구글에서 개발하고 오픈소스로 공개한 직렬화 자료구조다. 다양한 언어를 지원하며 직렬화 속도가 빠르고 직렬화된 파일 크기도 작아서 Avro 파일 포맷과 함께 많이 사용된다...(중략)...프로토콜 버퍼는 한 파일에 최대 64MB까지 지원할 수 있고 JSON 파일로 전환이 가능하다. 반대도 가능하다
프로토콜 버퍼를 사용하면 (역)직렬화 속도가 빠르고 직렬화된 파일 크기를 월등히 줄일 수 있어 대용량 데이터를 처리할 때 성능이 좋다. JSON과 달리 사람이 읽기 어렵고 proto 파일이 없으면 데이터 해석이 불가능하단 단점도 있지만 스키마가 존재해서 이에 따라 쉽게 (역)직렬화가 가능하기 때문에 내부 서비스에서 사용하면 효과적이다
쉽게 말해서 대용량 데이터를 처리할 때에는 JSON보다 처리 속도가 빠른 자료구조라는 것 같다. 이걸 사용해서 키밸류 형태의 데이터와 객체를 저장할 수 있는 게 DataStore라는 것 같다.
본론으로 돌아와서, 다른 곳에선 DataStore를 어떻게 설명하는지 확인해봤다.
아래 링크는 DataStore에 대해 설명하는 포스팅이지만 이 다음 포스팅에선 DI, 직렬화, 테스트 등 코틀린 예제를 설명과 같이 보여주고 있다. 쭉 읽어보면 DataStore에 대한 전반적인 이해는 가능할 거라고 생각된다.
https://medium.com/androiddevelopers/introduction-to-jetpack-datastore-3dc8d74139e7
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는 유형이 지정된 객체를 저장하기 위해 키밸류 쌍 또는 프로토콜 버퍼를 써서 간단한 데이터 조각을 유지하기 위한 구글의 새로운 솔루션이다. 코루틴 및 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로 바꾸는 게 보인다.
생각보다 엄청나게 어렵진 않아서 조금만 신경쓰면 쉐어드처럼 간단한 값들은 저장해서 사용할 수 있을 듯하다.