스키마를 정의해 타입 안전하게 저장하는 DataStore.
// app/src/main/proto/settings.proto
syntax = "proto3";
option java_package = "com.example";
option java_multiple_files = true;
message UserSettings {
bool dark_mode = 1;
float font_scale = 2;
repeated string recent_queries = 3;
}
object SettingsSerializer : Serializer<UserSettings> {
override val defaultValue: UserSettings = UserSettings.getDefaultInstance()
override suspend fun readFrom(input: InputStream): UserSettings =
try { UserSettings.parseFrom(input) }
catch (e: InvalidProtocolBufferException) { throw CorruptionException("손상", e) }
override suspend fun writeTo(t: UserSettings, output: OutputStream) = t.writeTo(output)
}
val Context.settingsStore by dataStore("settings.pb", SettingsSerializer)
val darkMode: Flow<Boolean> = ctx.settingsStore.data.map { it.darkMode }
suspend fun setDarkMode(on: Boolean) {
ctx.settingsStore.updateData { it.toBuilder().setDarkMode(on).build() }
}
Preferences와의 차이
-
Preferences — 키가 문자열. 오타가 런타임에 드러난다
- 값이 Any 로 다뤄져 타입 캐스팅 실수가 가능하다
- 중첩 구조를 표현할 수 없다
-
Proto — 스키마가 코드로 생성된다 — 오타가 컴파일 에러
- 중첩 메시지 · 리스트 · enum 을 그대로 담는다
- 바이너리라 파일이 작고 파싱이 빠르다
-
대가: proto 파일과 빌드 설정이 늘어난다
스키마 진화 규칙
protobuf 는 필드 번호로 값을 찾는다
안전한 변경
- 새 필드를 새 번호로 추가한다 (구버전은 무시한다)
- 필드 이름을 바꾼다 (번호가 그대로면 데이터는 유지된다)
위험한 변경
- 필드 번호를 바꾼다 → 데이터가 엉뚱한 필드로 들어간다
- 타입을 바꾼다 → 파싱이 깨진다
- 번호를 재사용한다 → 구버전 데이터가 새 필드로 읽힌다
삭제할 땐 reserved 로 번호를 막아 둔다
- reserved 2; reserved "font_scale";
손상 처리
val Context.settingsStore by dataStore(
fileName = "settings.pb",
serializer = SettingsSerializer,
corruptionHandler = ReplaceFileCorruptionHandler { UserSettings.getDefaultInstance() }
)
없으면 CorruptionException 이 그대로 올라와 앱이 설정을 못 읽는다 기본값으로 되돌리는 편이 대부분 옳다
면접 함정
- ❌ "Proto DataStore는 항상 낫다" → 키가 다섯 개면 Preferences가 충분하다. 빌드 설정 비용이 있다.
- ❌ "필드를 지우면 된다" → 번호가 재사용되면 옛 데이터가 잘못 읽힌다.
reserved를 쓴다.