안드로이드 로컬에서 작은양의 데이터를 저장할때 크게 2가지 선택지가 있습니다.
SharedPreferences와 DataStore이 2가지는 모두 로컬에 데이터를 저장할수 있다는 장점이 있습니다.
그렇다면 각선택지는 어느상황에 쓰는것이 적절할까요?
가벼운 데이터, 예를 들어 설정 값이나 사용자의 기본 프로필 정보와 같은 작은 데이터를 저장할 때 적합합니다.
키-값 쌍으로 데이터를 저장하며, 간단하게 사용할 수 있습니다.
xml 파일 형태로 데이터를 저장합니다. (data/data/패키지명/shared_prefs/SharedPreferences
파일 형태로 저장되므로 너무 많은 데이터를 넣게 되면 메모리 예외가 나타날 수 있습니다
파일 형태로 저장되므로 메모리 손상의 위험에 영향이 갈 수 있습니다
보안에 취약하므로 보안이 요구되는 데이터들은 SharedPreferences로 저장하지 않는 것이 좋습니다
어플리케이션이 삭제되기 전까지 데이터가 보존됩니다
데이터 양이 적고 간단한 구조일 때 유용합니다.
object MySharedPreferences {
private lateinit var preferences: SharedPreferences
fun init(context: Context) {
preferences = context.getSharedPreferences(context.packageName, Context.MODE_PRIVATE)
}
fun setAppPreferencesPrimitive(pair: Pair<String, Any>) {
val key = pair.first
when (val value = pair.second) {
is String -> preferences.edit().putString(key, value).apply()
is Int -> preferences.edit().putInt(key, value).apply()
is Boolean -> preferences.edit().putBoolean(key, value).apply()
is Long -> preferences.edit().putLong(key, value).apply()
is Float -> preferences.edit().putFloat(key, value).apply()
else -> error("Only primitive types can be stored in SharedPreferences")
}
}
fun getAppPreferencesString(key: String): String {
return preferences.getString(key, "") ?: ""
}
fun getAppPreferencesInt(key: String): Int {
return preferences.getInt(key, 0) ?: 0
}
fun getAppPreferencesFloat(key: String): Float {
return preferences.getFloat(key, 0F) ?: 0F
}
fun getAppPreferencesLong(key: String): Long {
return preferences.getLong(key, 0L) ?: 0L
}
fun getAppPreferencesBoolean(key: String): Boolean {
return preferences.getBoolean(key, false) ?: false
}
fun removeAppPreferences(key: String) {
val prefEditor: SharedPreferences.Editor? = preferences.edit()
prefEditor?.remove(key)
prefEditor?.apply()
}
var isTestMode
get() = getAppPreferencesBoolean("TEST_MODE")
set(value) {
preferences.edit().putBoolean("TEST_MODE", value).apply()
}
}
SharedPreferences는 동기적으로 데이터를 가지고 오기때문에 이렇식으로 이런식으로 SharedPreferences를 관리할수도 있습니다.
Android Jetpack의 최신 데이터 저장 라이브러리로, 프로토콜 버퍼를 사용하여 키-값 쌍 또는 유형이 지정된 객체를 저장할 수 있는 데이터 저장소 솔루션입니다.
Datastore는 Kotlin 코루틴 및 Flow를 사용하여 비동기적이고 일관된 트랜잭션 방식으로 데이터를 저장합니다.

SharedPreferences가 있는데도 DataStore가 나온것은 SharedPreferences의 한계 때문입니다. SharedPreferences를 UI 쓰레드에서 호출하게된면 쓰레드를 차단하여 버벅거림을 유발할 수 있어 ANR같은 상황을 야기시킬수 있으며 오류의 알림이나, 마이그레이션, type safety같은 한계로 DataStore가 나오게된것 입니다.
Datastore는 Preferences Datastore와 Proto Datastore라는 두 가지 구현을 제공합니다.
데이터 양이 증가함에 따라 파일 크기가 급격히 증가하고 CPU가 파일을 읽는 데 더 많은 비용이 듭니다
프로토콜 버퍼(Protocol buffers)는 기존 SharedPreferences의 XML보다 빠르고 크기가 작은 구조화된 데이터를 표현하는 새로운 방법입니다.
데이터의 일관성을 유지하면서 복잡한 데이터 구조를 다룰 수 있습니다.
데이터 변경 사항을 관찰하고 리액트할 수 있는 Kotlin의 Flow를 사용하여 데이터를 관리할 수 있습니다.
[versions]
datastore = "1.1.0"
protobuf = "0.9.1"
protobuflib = "3.25.1"
[libraries]
androidx-preferences-datastore = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
androidx-proto-datastore = { group = "androidx.datastore", name = "datastore", version.ref = "datastore" }
protobuf = { group = "com.google.protobuf", name = "protobuf-javalite", version.ref = "protobuflib" }
[plugins]
protobuf = { id = "com.google.protobuf", version.ref = "protobuf" }
plugins {
alias(libs.plugins.protobuf) apply false
}
import com.google.protobuf.gradle.*
plugins {
alias(libs.plugins.protobuf)
}
dependencies {
//use Preferences DataStore
implementation(libs.androidx.preferences.datastore)
//use Proto Data Store
implementation(libs.androidx.proto.datastore)
implementation(libs.protobuf)
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.25.1"
}
generateProtoTasks {
all().forEach { tasks ->
tasks.builtins {
id("java") {
option("lite")
}
}
}
}
}
syntax = "proto3";
option java_package = "com.example.myapplication";
option java_multiple_files = true;
message Users{
repeated User user = 1;
}
message User {
int32 id = 1;
string name = 2;
int32 age = 3;
Data data = 4;
}
message Data {
int32 loginCount = 1;
int32 btnCount = 2;
}
class MainActivity : AppCompatActivity() {
private val myPreferenceDataStore: MyPreferenceDataStore by lazy {
MyPreferenceDataStore(this)
}
private lateinit var numberTextView: TextView
private lateinit var userTextView: TextView
private lateinit var button: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
lifecycleScope.launch {
myPreferenceDataStore.increaseLoginCounter()
}
initView()
}
private fun initView() {
numberTextView = findViewById(R.id.tv_number)
userTextView = findViewById(R.id.tv_user)
initObservableData()
button = findViewById(R.id.btn_increase)
button.setOnClickListener { increaseNum() }
}
private fun initObservableData() {
lifecycleScope.launch {
myPreferenceDataStore.exampleCounterFlow.collectLatest { i: Int ->
numberTextView.text = i.toString()
}
}
lifecycleScope.launch {
myPreferenceDataStore.userFlow.collectLatest { user ->
userTextView.text = "ID: ${user.id}, Name: ${user.name}, Age: ${user.age}, Login Count: ${user.data.loginCount}, Button Count: ${user.data.btnCount}"
}
}
lifecycleScope.launch {
myPreferenceDataStore.usersFlow.collectLatest { users ->
Log.e("TAG", "initObservableData: users.userCount : ${users.userCount}", )
}
}
}
private fun increaseNum() = lifecycleScope.launch {
myPreferenceDataStore.increaseBtnCounter()
myPreferenceDataStore.increaseUsers()
}
}
object UserSerializer : Serializer<User>{
override val defaultValue: User
get() = User.getDefaultInstance()
override suspend fun readFrom(input: InputStream): User {
try {
return User.parseFrom(input)
} catch (exception: Exception) {
throw RuntimeException("Error reading user from input stream", exception)
}
}
override suspend fun writeTo(t: User, output: OutputStream) {
return t.writeTo(output)
}
}
object UsersSerializer : Serializer<Users> {
override val defaultValue: Users
get() = Users.getDefaultInstance()
override suspend fun readFrom(input: InputStream): Users {
try {
return Users.parseFrom(input)
} catch (exception: Exception) {
throw RuntimeException("Error reading user from input stream", exception)
}
}
override suspend fun writeTo(t: Users, output: OutputStream) {
return t.writeTo(output)
}
}
val Context.preferencesDataStore: DataStore<Preferences>
by preferencesDataStore(name = "settings")
val Context.userProtoDataStore: DataStore<User>
by dataStore(
fileName = "userSettings.pb",
serializer = UserSerializer
)
val Context.usersProtoDataStore: DataStore<Users>
by dataStore(
fileName = "usersSettings.pb",
serializer = UsersSerializer
)
class MyPreferenceDataStore(context: Context) {
private val preferencesDataStore = context.preferencesDataStore
private val userProtoDataStore = context.userProtoDataStore
private val usersProtoDataStore = context.usersProtoDataStore
val exampleCounterFlow: Flow<Int> = preferencesDataStore.data
.map { preferences ->
// No type safety.
preferences[EXAMPLE_COUNTER] ?: 0
}
val userFlow : Flow<User> = userProtoDataStore.data
val usersFlow : Flow<Users> = usersProtoDataStore.data
suspend fun increaseBtnCounter() {
preferencesDataStore.edit { settings ->
val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0
settings[EXAMPLE_COUNTER] = currentCounterValue + 1
}
userProtoDataStore.updateData { currentSettings ->
currentSettings.toBuilder()
.setData(
currentSettings.data.toBuilder()
.setBtnCount(currentSettings.data.btnCount + 1)
.build()
)
.build()
}
}
suspend fun increaseLoginCounter() {
userProtoDataStore.updateData { currentSettings ->
currentSettings.toBuilder()
.setData(
currentSettings.data.toBuilder()
.setLoginCount(currentSettings.data.loginCount + 1)
.build()
)
.build()
}
}
suspend fun increaseUsers(){
usersProtoDataStore.updateData { currentSettings : Users ->
currentSettings.toBuilder()
.addUser(userFlow.first())
.build()
}
}
companion object {
private val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
}
}

이처럼 데이터스토어는 비동기적으로 값을 처리해야하고 flow로 값을감지하기에 SharedPreferences보다는 어려울수 있습니다. 그렇지만 성능면에서나 안드로이드의 지원에서나 좋은점이 많습니다. 간단하게 테스트용이나 본인만 사용할 앱이아니라면 SharedPreferences보다는 데이터스토어의 사용이 더 좋아보입니다.
reference
https://hapen385.tistory.com/29
https://developer.android.com/topic/libraries/architecture/datastore?hl=ko
https://kotlinworld.com/category/Android%20Jetpack/Datastore