이전 포스팅에서는 Proto DataStore 소개와 가지고 있는 장점 그리고 초기 세팅 방법을 알아보았다. 이번에는 Proto DataStore 사용방법, 의존성 주입을 통한 사용에 대해서 소개한다.
DataStore 구조
public interface DataStore<T> {
public val data: Flow<T>
public suspend fun updateData(transform: suspend (t: T) -> T): T
}
DataStore는 데이터를 읽기위한 프로퍼티 data와 데이터를 쓰기 위한 함수 updateData로 구성되어 있다. data는 Flow의 형태로, updateData는 suspend function의 형태를 띄고 있다.
데이터 읽기
DataStore에 사용할 함수와 프로퍼티를 정의한 클래스를 UserPreferencesDataSource라고 하겠다. DataStore에 저장된 데이터를 읽기 위해서, userPreferencesStore.data로 부터 Flow를 생성한다.
val userPreferencesFlow: Flow<UserPreferences> = userPreferencesStore.data
해당 Flow는 DataStore의 데이터를 읽기위한 효율적인 접근을 제공한다.
- 가장 마지막에 발행된 값 접근
- 값에 변화가 생길 때 마다 발행
- Flow 내부 예외 처리에 의존하여 예외 발생 시 활동 정의 가능
- Dispatcher.IO에 의해서만 실행 됨. -> Ui Thread Block 방지
- proto 파일에 대한 정의대로 자동적으로 UserPreferences 클래스 생성 -> 추가적인 Data Class를 생성할 필요가 없음.
데이터 쓰기
DataStore에 데이터를 쓰기 위해, 이전에 확인한 suspend function updateData를 활용한다. 해당 함수는 매개 변수로 DataStore의 Data를 접근할 수 있는 함수를 전달할 수 있다. 실제 사용에서는 람다를 통해 호출할 수 있다.
suspend fun setNickname(nickname: String){
userPreferencesStore.updateData { prefs ->
prefs.toBuilder().setNickname(nickname).build()
}
}
해당 코드를 자세하게 살펴보자면,
- userPreferencesStore는 DataStore<UserPreferences>의 인터페이스이다.
- toBuilder는 UserPreferences를 데이터를 변경 가능한 Builder로 변환한다.
- setNickname은 값을 변경 할 수 있는 함수이다.
- build는 UserPreferences로 다시 변경한다.
데이터를 수정하는 작업은 atomic-read-modify-write operation으로 실행되며, 이는 데이터에 대한 처리를 할 때 다른 스레드가 데이터에 접근하는 것을 막는다. 해당 명령은 strong-consistency를 보장하며, race-condition을 방지한다.
updateData를 활용하는 방법이 DataStore의 데이터를 접근할 수 있는 유일한 방법이며, 외부에서 수동으로 변경해도 Proto의 데이터는 수정되지 않는다.
예외 처리하기
이전 SharedPreferences는 파싱 에러를 런타임 예외로 던져 발생한 예외에 대해 처리를 하지 못해 크래쉬가 발생할 수 밖에 없었다. 반면 DataStore는 데이터 읽기/쓰기 오류가 발생할 때 IO 예외를 발생시켜, 발생한 예외에 대해 대처를 할 수 있게 되었다.
val userDataFlow: Flow<User> = userPreferencesStore.data
.catch { exception ->
if (exception is IOException) {
Timber.e(exception)
emit(UserPreferences.getDefaultInstance())
} else {
throw exception
}
}
.map{
User(it.userId, it.nickname)
}
Flow의 .catch를 통해서 쉽게 예외처리를 할 수 있다. (try-catch)도 가능하다.
Hlt를 통해 Proto DataStore 사용하기
DataStore는 지정한 파일에 두 개 이상의 DataStore 인스턴스를 생성하면 DataStore에 손상이 갈 수 있어, 싱글톤으로 선언해야 한다. Hilt를 통해 의존성 주입을 하고, 싱글톤으로 활용할 수 있다. Hilt를 설명하는 포스팅은 아니기 때문에, Hilt에 대한 초기 세팅은 생략하도록 하겠다. 다른분의 포스팅을 확인하자.
@Module
@InstallIn(SingletonComponent::class)
object DataStoreModule {
@Provides
@Singleton
fun providesUserPreferencesDataStore(
@ApplicationContext context: Context,
userPreferencesSerializer: UserPreferencesSerializer,
): DataStore<UserPreferences> =
DataStoreFactory.create(
serializer = userPreferencesSerializer,
produceFile = { context.dataStoreFile("user_preferences.pb") },
scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
)
}
해당 코드를 확인해보면,
- DataStoreFactory를 통해 DataStore 객체를 생성하였다.
- Serializer에 이전 포스팅에서 구현한 Serializer를 전달한다.
- scope는 IO작업과 transform 함수가 실행되는 scope를 의미하며, 해당 코드에는 create 함수의 Default를 사용하였다.
- produceFile은 입력한 context와 FileName에 따라서 파일을 생성한다. this.applicationContext.filesDir + datastore의 하위 디렉토리에 파일이 저장된다.
이렇게 의존성이 주입된 DataStore를 사용할 수 있다.
class UserPreferencesDataSource @Inject constructor(
private val userPreferencesStore : DataStore<UserPreferences>
){
val userDataFlow: Flow<User> = userPreferencesStore.data
.catch { exception ->
if (exception is IOException) {
Timber.e(exception)
emit(UserPreferences.getDefaultInstance())
} else {
throw exception
}
}
.map{
User(it.userId, it.nickname)
}
suspend fun setNickname(nickname: String){
userPreferencesStore.updateData { prefs ->
prefs.toBuilder().setNickname(nickname).build()
}
}
suspend fun setUserId(userId: Long){
userPreferencesStore.updateData { prefs ->
prefs.copy {
this.userId = userId
}
}
}
}
마지막으로
이렇게 Proto DataStore + Protocol Buffers(1), Proto DataStore + Hilt(2)에 대한 포스팅이 끝났다. Android Developer에서도 확인할 수 있듯이, SharedPreference 에서 DataStore로 Migration하는 것을 권장할 수 있다. 하지만, 왜 바꿔야 하는지, 바꾸는 과정은 어떻게 되는지 명확하게 알지 못했다. 직접 Migration하면서, 포스팅하면서 부족했던 DataStore에 대한 지식을 채울 수 있었다. 나중에 Migration을 하는 상황이 발생해, 같은 팀원이 "왜 바꿔야 할까요?"를 물으면 바꾸지 않을 이유가 없다고 설명하고 싶다😊
참고문헌
https://medium.com/androiddevelopers/datastore-and-dependency-injection-ea32b95704e3
DataStore and dependency injection
In the following posts from our Jetpack DataStore series, we will cover several additional concepts to understand how DataStore interacts…
medium.com
https://medium.com/androiddevelopers/all-about-proto-datastore-1b1af6cd2879
All about Proto DataStore
In this post, we will learn about Proto DataStore, one of two DataStore implementations. We will discuss how to create it, read and write…
medium.com