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

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