[Android] DataStore 첫 인상 살펴보기

H43RO·2021년 9월 22일
15

Android 와 친해지기

목록 보기
12/26
post-thumbnail

SharedPreferences 쓰지 마.

그게 뭔데.

SharedPreferences 쓰지 말라고.

그거 어떻게 하는건데.


이번 포스팅에선 'SharedPreferences' 를 쓰지 않는 방법에 대해 알아보고자 한다. (?)

어느 날 사라진 SharedPreferences 사용 가이드

우리는 Key-Value 타입으로 간단하고 작은 데이터를 로컬에 저장하기 위해 SharedPreferences 라는 녀석을 애용하곤 했다.

그러나 어느 날, 안드로이드 공식 문서에서 SharedPreferences 사용 가이드 문서가 감쪽같이 사라졌다. 그 이유는 JetPack 라이브러리의 구성요소 중 하나인 DataStore 가 혜성같이 등장했기 때문이다.

안드로이드 차원에서 DataStore 의 사용을 거의 멱살잡고 강권하고 있다. 대체 어떤 메리트가 있길래 이러는걸까? 천천히 살펴보자.


DataStore?

이 녀석은 프로토콜 버퍼 (구조화된 데이터를 직렬화하는 메커니즘) 를 사용하여 Key-Value 타입 혹은 유형이 지정된 객체를 로컬에 저장할 수 있는 애다. 솔직이 이름 너무 성의 없는 것 같다

한 가지 멋있는 점은, Coroutine + Flow 를 사용할 수 있어 비동기적이고 일관적인 트랜잭션 방식으로 데이터를 저장한다는 점이다.

DataStoreKey-Value 타입으로 구성되어 있는 Preferences DataStore사용자가 정의한 데이터를 저장할 수 있는 Proto DataStore 이렇게 두 종류가 존재한다. Proto DataStore 를 사용하게 되면, 프로토콜 버퍼를 이용하여 스키마를 정의해야 한다. 이는 데이터의 타입을 보장해줄 뿐더러, SharedPreferences 보다 훨씬 빠르고 단순하다.


DataStore 의 거부할 수 없는 매력

[이미지 출처] https://android-developers.googleblog.com/2020/09/prefer-storing-data-with-jetpack.html

일단 항목을 자세히 안 봐도 뭔가 DataStore 가 더 많은 걸 할 수 있는 모양이다. 항목들을 보면 약 파는 것도 아니다.

세 줄 요약

  • 코루틴 + Flow 를 통해 Read/Write 에 대한 비동기 API 제공함
  • Main Thread (UI Thread) 를 호출해도 안전함 (Dispatcher.IO 밑에서 동작)
  • RuntimeException 으로부터 안전함

확실히 SharedPreferences 에 비해 얻는 이점이 많은 것 같다. 괜히 강권하는게 아닌 것 같다. 필자도 쓱 훑어보니 러닝 커브도 높은 편이 아닌 것 같아 DataStore 로 넘어가도 괜찮을 것 같다는 생각을 했다. 따라서 슬슬 최근 개발중인 앱에서 SharedPreferences 를 썼던 로직을 마이그레이션 해보려고 한다.

백문이 불여일타. 직접 만져보며 사용법을 익혀보자.


직접 만들어보기

라이브러리 추가

우선 항상 그래왔듯이, app 단위의 build.gradledependencies 에다가 아래와 같이 추가해주자.

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

DataStore 객체 생성

아래처럼 DataStore 를 생성해주자. 처음에 preferencesDataStore 등의 키워드가 인식 안 될 수 있는데, 마우스를 갖다대보면 Auto Import 된다.

class DataStoreModule(private val context: Context) {
    private val Context.dataStore by preferencesDataStore(name = "data_store")

    private val stringKey = stringPreferencesKey("key_name") // String 타입 저장 키값
    private val intKey = intPreferencesKey("key_name") // Int 타입 저장 키값
}

DataStore 에서 사용할 키 값은 'type'PreferencesKey("key_name") 과 같이 선언할 수 있다. 해당 예시는 String, Int 타입을 저장하고 싶은 경우이다.


데이터 읽는 Flow 만들기

여기서부터 진국이다. 코루틴의 Flow 를 사용할 수 있다. DataStore 에서 데이터를 읽어올 때에, 해당 데이터를 Flow 객체로 전달하게 된다.

// `stringKey` Key 값에 대응하는 Value 반환
val textData: Flow<String> =
	context.dataStore.data
	    .catch { exception ->
                if (exception is IOException) {
		    emit(emptyPreferences())
		} else {
		    throw exception
		}
	    }
	    .map { preferences ->
                preferences[stringKey] ?: ""  // 아까 만든 Key 이용
	    }

map() 을 활용하여 아까 만든 키 값 (stringKey) 에 대응하는 Value 를 가져오는 동작을 한다. 위에서 말했던 것처럼 Flow 형태로 가져오게 된다.

또한 catch() 를 활용하여, 데이터를 읽어오는 것을 실패하는 경우 발생하는 IOException 을 처리해줌으로써 비어있는 값을 전달해준다.


데이터 쓰는 메소드 만들기

edit() 메소드를 활용하여 데이터를 쓸 수 있다. 그런데 이 작업은 반드시 비동기적으로 수행되어야 하므로, suspend 키워드를 통해 해당 함수가 코루틴 영역에서 동작할 수 있도록 해준다.

// String 값을 `stringKey` 의 Value 로 저장
suspend fun setTextData(text: String) {
    context.dataStore.edit { preferences ->
        preferences[stringKey] = text  // 아까 만든 Key 이용
    }
}

사용해보기

1. 데이터 가져오기

DataStore 에서 읽은 데이터를 TextView 에 적용한다고 해보자. 그럼 아래와 같이 코드를 작성해볼 수 있다. CoroutineScope 내에서 수행되어야 한다!

CoroutineScope(Dispatchers.Main).launch {
    MyApplication.getInstance().getDataStore().textData.collect {
        textView.text = it
    }
}

딱 원하는 타이밍에 한 번만 값을 얻으려면 아래와 같이 사용해도 될 것이다.

CoroutineScope(Dispatchers.Main).launch {
    textView.text = MyApplication.getInstance().getDataStore().textData.first()
}

2. 데이터 저장하기

아까 우리가 만든 setTextData() 이라는 Suspending 함수를 활용하여 DataStore데이터를 저장하는 동작을 구현해보자. 매우 간단하다!

CoroutineScope(Dispatchers.Main).launch {
    MyApplication.getInstance().getDataStore().setTextData("H43RO")
}

🤚🏻 만약 동기적인 동작이 필요한 경우, 아래와 같이 사용하면 된다.

runBlocking {
    val text = MyApplication.getInstance().getDataStore().textData.first()
}

그러나 반드시 UI 쓰레드를 차단할 정도의 동작은 금해야 한다. 알다시피 ANR 혹은 UI 버벅거림이 발생하기 때문이다!


지금까지 DataStore 의 첫 인상을 살펴보았다. 이는 분명히 SharedPreferences 를 대체하기 위해 등장한 녀석인만큼, 많은 장점을 지니고 있다. 특히 Coroutine Flow 를 자체적으로 지원해주다보니 훨씬 비동기적이고 안전한 R/W 동작을 할 수 있는 것이 가장 큰 장점으로 다가온다. (물론 RxJava 도 지원한다!)

시간이 된다면 적극적으로 SharedPreferences → DataStore 마이그레이션을 고려해보면 좋을 것 같다.

참고자료

https://developer.android.com/topic/libraries/architecture/datastore?gclid=Cj0KCQjwqKuKBhCxARIsACf4XuHSV6c0dQKCbCAO0rH42Pc-MFbVKxhgf1YRYxu2qf_yPmkeU5m3WfoaAqfKEALw_wcB&gclsrc=aw.ds#kts

https://android-developers.googleblog.com/2020/09/prefer-storing-data-with-jetpack.html

https://kangmin1012.tistory.com/47

profile
어려울수록 기본에 미치고 열광하라

0개의 댓글