안드로이드 프레임워크·시스템 학습 노트 목차

DataStore — 설정·키값 저장 (SharedPreferences 대체)

작은 설정값(다크모드 여부·로그인 토큰·온보딩 완료)을 저장할 때, 오래된 **SharedPreferences**는 동기 API(commit)가 메인 스레드를 막고, 에러를 알리지 않으며, 타입 안전이 약하다. 구글의 현대 권장 대체가 DataStore코루틴·Flow 기반의 비동기·트랜잭션·타입 안전 저장소다. (출처: Android — DataStore.)

왜 SharedPreferences가 아니라 DataStore인가

SharedPreferences의 문제는 눈에 잘 안 띄지만 실재한다. 흔히 쓰는 코드부터 보자.

// SharedPreferences — 숨은 함정들
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
prefs.edit().putBoolean("dark", true).apply()  // apply: 디스크 쓰기를 백그라운드로 미루나,
                                                 //   첫 getSharedPreferences/get 호출은 메인에서 디스크를 읽어 막을 수 있음
val dark = prefs.getBoolean("dark", false)       // 동기 — 디스크 로딩 중이면 메인 스레드 블로킹(ANR)
prefs.edit().commit()                            // commit: 즉시 동기 디스크 쓰기 → 메인에서 부르면 ANR

핵심 문제 세 가지 — (1) commit()이나 첫 로딩이 메인 스레드를 막아 ANR 위험, (2) 쓰기 실패를 알 길이 없음(apply는 조용히 묻음), (3) 잘못된 타입으로 꺼내면 런타임에야 터짐. DataStore는 이 셋을 *코루틴/Flow*로 푼다.

// DataStore — 비동기·관찰 가능·에러를 예외로
val darkMode: Flow<Boolean> = ...   // 읽기는 Flow (값 바뀌면 자동 방출, 디스크 I/O는 백그라운드)
suspend fun setDark(on: Boolean)    // 쓰기는 suspend 트랜잭션 (실패 시 예외)

두 종류 — Preferences vs Proto

다이어그램 로딩 중…
  • Preferences DataStore키-값 저장(SharedPreferences와 모양은 비슷하나 비동기). 스키마는 없지만 키에 타입이 있어(booleanPreferencesKey) 잘못된 타입 접근을 줄인다. 빠르게 도입하기 좋다.
  • Proto DataStoreprotobuf 스키마로 구조를 정의완전한 타입 안전. 여러 연관 설정을 하나의 강타입 객체로 다룰 때 적합하다.

Preferences DataStore — 코드

// 1) DataStore를 Context 확장으로 한 번 선언 (앱당 파일 하나)
val Context.dataStore by preferencesDataStore(name = "settings")
val DARK = booleanPreferencesKey("dark_mode")

// 2) 읽기 — Flow로 관찰 (값이 바뀌면 자동 방출)
val darkMode: Flow<Boolean> = context.dataStore.data
    .map { prefs -> prefs[DARK] ?: false }   // 키가 없으면 기본값
    .catch { e -> if (e is IOException) emit(false) else throw e }  // 읽기 실패 처리

// 3) 쓰기 — suspend 트랜잭션 (edit 블록이 원자적)
suspend fun setDark(on: Boolean) {
    context.dataStore.edit { prefs -> prefs[DARK] = on }
}

여기서 *읽기가 Flow*라는 점이 SharedPreferences와 가장 다르다 — 설정이 바뀌면 그 변화가 구독 중인 모든 화면에 자동으로 흐른다. ViewModel에서 stateIn([09])으로 StateFlow로 노출하면 설정 화면에서 다크모드를 켜는 즉시 다른 화면도 반응한다(별도 새로고침 없이). 이것이 단방향 데이터 흐름([andarch UDF])과 자연스럽게 맞물린다.

흔한 함정 — DataStore엔 동기 읽기(get)가 없다. 앱 시작 시점에 값이 꼭 필요하면 runBlocking { dataStore.data.first() }로 한 번 읽거나(권장 안 함), Flow로 구독해 준비되면 반영하는 구조로 설계한다.

Proto DataStore — 타입 안전 스키마

.proto 파일로 메시지(설정 구조)를 정의하고 Serializer를 구현하면 — 오타·타입 오류가 컴파일 타임에 잡히는 강타입 설정 객체를 얻는다.

// user_prefs.proto
message UserPrefs { bool dark = 1; int32 font_scale = 2; string token = 3; }

읽기·쓰기 API는 Preferences와 비슷하되 키 대신 생성된 타입의 프로퍼티를 쓴다(prefs.dark). 여러 설정이 하나의 일관된 객체로 묶여, 부분 갱신도 updateData { it.toBuilder().setDark(true).build() }처럼 원자적이다.

SharedPreferences에서 이전

기존 앱은 DataStore가 SharedPreferences를 자동 마이그레이션하도록 도와준다 — preferencesDataStore(name, produceMigrations = { listOf(SharedPreferencesMigration(ctx, "old")) })기존 키-값을 한 번에 옮긴다. 점진 전환이 가능하다.

무엇을 저장하나 — DataStore vs Room

  • DataStore적은 양의 키-값/설정(다크모드·토큰·플래그·온보딩 여부). 대용량·관계형이 아니다.
  • Room([06]) — 구조화된 대량 데이터(목록·관계·쿼리·페이징).
  • 잘못 쓰면 — 큰 데이터(수천 항목)를 DataStore에 넣으면 변경마다 전체를 직렬화/역직렬화해 비효율적이고 느리다. 설정은 DataStore, 데이터는 Room이 원칙이다.

정리 — 설정은 비동기·Flow로

DataStore의 핵심 — SharedPreferences의 메인 스레드 블로킹·에러 무시·약한 타입을 코루틴/Flow로 해결, Preferences(키-값·간단) vs Proto(스키마·타입 안전), 읽기는 Flow로 관찰(설정 변화가 화면까지 자동 반영)·쓰기는 suspend 트랜잭션(실패는 예외), 동기 읽기가 없으니 구독 구조로 설계, 적은 설정은 DataStore·대량 구조 데이터는 Room. 신규 코드에선 SharedPreferences 대신 DataStore가 정답이다.

한 줄 요약 — DataStore=SharedPreferences 대체(코루틴/Flow·비동기·트랜잭션·에러 신호·동기 읽기 없음). Preferences(키-값·booleanPreferencesKey·간단) vs Proto(protobuf 스키마·타입 안전). 읽기 data.map{} Flow(자동 반영, UDF와 결합)·쓰기 edit{} suspend. SharedPreferencesMigration으로 이전. 설정은 DataStore, 대량 데이터는 Room.

(출처: Android — DataStore / Preferences / Proto DataStore / Migrate from SharedPreferences.)

네트워킹 — Retrofit·OkHttp·인터셉터·에러 처리알림·푸시 — Notification·채널·FCM