refactor(preferences): DataStore 설정 저장 안정성을 높인다

This commit is contained in:
2026-03-11 12:32:34 +09:00
parent 8e1dabbb80
commit 418b734c3f
16 changed files with 847 additions and 285 deletions

View File

@@ -0,0 +1,57 @@
# DataStore 전환 및 SharedPreferences 마이그레이션
- [x] 공식 문서의 DataStore 권고 문구 확인 및 근거 정리
- [x] 프로젝트 내 SharedPreferences 사용 지점 식별
- [x] `SharedPreferenceManager`를 DataStore 기반으로 전환
- [x] `LanguageManager`를 DataStore 기반 읽기/쓰기로 전환
- [x] 채팅방 `chat_room_prefs`를 DataStore + 마이그레이션으로 전환
- [x] 앱 초기화 경로에 DataStore 초기화 반영
- [x] 진단/테스트/빌드 검증 결과 기록
- [x] 핵심 런타임 회귀 자동 테스트(androidTest) 추가
## 검증 기록
### 2026-03-09
- 무엇/왜/어떻게: Android 공식 문구를 확인한 뒤, 기본 설정 저장소(`SharedPreferenceManager`)와 채팅방 전용 저장소(`chat_room_prefs`)를 각각 DataStore로 전환하고 `SharedPreferencesMigration`으로 기존 설치 기기의 데이터가 손실 없이 이관되도록 구현했다. 기존 호출부 대량 수정을 피하기 위해 기존 매니저 API를 유지하고 내부 저장소만 교체했다.
- 공식 문서 근거: "If you're using `SharedPreferences` to store data, consider migrating to DataStore instead." (`App Architecture: Data Layer - DataStore - Android Developers`, https://developer.android.com/topic/libraries/architecture/datastore)
- 실행 명령: `lsp_diagnostics` (변경 Kotlin 파일 8개)
- 결과: 현재 실행 환경에 Kotlin LSP 서버가 없어 진단 불가(`No LSP server configured for extension: .kt`).
- 실행 명령: `./gradlew :app:testDebugUnitTest :app:assembleDebug`
- 결과: `BUILD SUCCESSFUL` (단위 테스트/디버그 빌드 성공).
- 실행 명령: Oracle 리뷰(`bg_0bc7de61`)
- 결과: 리스너 콜백 쓰레드 호환성 리스크(백그라운드 스레드에서 UI 리스너 호출 가능)와 읽기 전용 `SharedPreferences` 뷰의 no-op `edit()` 리스크를 확인했고, `SharedPreferenceManager`에서 메인 스레드 디스패치 및 fail-fast `edit()` 예외 처리로 반영했다.
- 실행 명령: `./gradlew :app:testDebugUnitTest :app:assembleDebug` (Oracle 피드백 반영 후 재검증)
- 결과: `BUILD SUCCESSFUL`.
- 실행 명령: `./gradlew :app:lintDebug`
- 결과: 실패. 기존 프로젝트 이슈(`AndroidManifest.xml``MissingClass(com.facebook.FacebookActivity)` 포함 18 errors, 577 warnings)로 중단되었고, 재실행에서도 동일 결과다.
- 실행 명령: `./gradlew :app:ktlintCheck`
- 결과: 실패. 기존 코드베이스 전반의 스타일 이슈(다수 파일)로 `:app:ktlintMainSourceSetCheck` 실패.
- 무엇/왜/어떻게: 요청에 따라 "첫 접근 시 이관" 지점 주석을 추가했다. `AppPreferencesDataStoreProvider``SharedPreferencesMigration` 등록부와 `SharedPreferenceManager`/`ChatRoomPreferenceManager``dataStore.data.first()` 트리거 지점에 주석을 배치해, 언제 이관이 실행되는지 코드를 읽는 즉시 파악할 수 있게 했다.
- 실행 명령: `./gradlew :app:assembleDebug :app:testDebugUnitTest`
- 결과: `BUILD SUCCESSFUL`.
- 무엇/왜/어떻게: 사용자 요청에 맞춰 "전체 일괄 치환" 대신 값 변화 반응 지점과 즉시 사용 안정성이 중요한 지점을 우선 리팩터링했다. `SharedPreferenceManager`/`ChatRoomPreferenceManager` 내부 `runBlocking`을 제거하고 비동기 수집 + 메모리 캐시 기반으로 변경했으며, 기존 `OnSharedPreferenceChangeListener` 의존 화면 4개를 `Flow` 수집으로 전환했다.
- 반응형 전환 파일: `MainActivity`, `HomeFragment`, `LiveFragment`, `AudioContentPlaylistDetailActivity`에서 `isPlayerServiceRunningFlow`/`roleFlow``repeatOnLifecycle`로 수집해 UI를 갱신하도록 변경.
- 실행 명령: `./gradlew :app:assembleDebug :app:testDebugUnitTest` (비동기 리팩터링 반영 후)
- 결과: `BUILD SUCCESSFUL`.
- 실행 명령: `./gradlew :app:lintDebug` (최종)
- 결과: 실패. 기존 프로젝트 이슈(`AndroidManifest.xml``MissingClass(com.facebook.FacebookActivity)` 포함 18 errors, 577 warnings)로 동일하게 중단.
### 2026-03-11
- 무엇/왜/어떻게: 핵심 런타임 회귀 2건을 수정했다. (1) `SharedPreferenceManager`/`ChatRoomPreferenceManager`가 초기 캐시 반영 전에 기본값을 반환하던 문제를 막기 위해 `init()`에서 `dataStore.data.first()`로 초기 스냅샷을 먼저 로딩한 뒤 `initialized`를 설정하도록 조정했다. (2) `repeatOnLifecycle(STARTED)` 재수집 시 미니플레이어가 중복 연결되던 문제를 막기 위해 `MainActivity`/`AudioContentPlaylistDetailActivity``mediaControllerFuture` 가드와 지연 실행 runnable 정리(`removeCallbacks`)를 추가했다.
- 반영 파일: `SharedPreferenceManager.kt`, `ChatRoomPreferenceManager.kt`, `MainActivity.kt`, `AudioContentPlaylistDetailActivity.kt`.
- 실행 명령: `lsp_diagnostics` (변경 Kotlin 파일 4개)
- 결과: 현재 실행 환경에 Kotlin LSP 서버가 없어 진단 불가(`No LSP server configured for extension: .kt`).
- 실행 명령: `./gradlew --stop && ./gradlew :app:testDebugUnitTest :app:assembleDebug`
- 결과: `BUILD SUCCESSFUL`.
- 실행 명령: `./gradlew :app:lintDebug`
- 결과: 실패. 기존 프로젝트 이슈(`AndroidManifest.xml``MissingClass(com.facebook.FacebookActivity)` 포함 18 errors, 577 warnings)로 동일하게 중단.
- 무엇/왜/어떻게: 수동 재현에 의존하지 않도록 런타임 회귀 자동 검증을 위한 계측 테스트를 추가했다. `DataStoreRuntimeRegressionTest`에서 초기 스냅샷 재초기화 경로를 검증하고, `MiniPlayerConnectionGuardTest`에서 `mediaControllerFuture`가 이미 존재할 때 `connectPlayerService()`가 조기 반환되는 가드를 검증한다. 이를 위해 `androidTest` 실행 환경(`testInstrumentationRunner`, `androidTestImplementation`)을 설정했고, 테스트 재초기화를 위해 `SharedPreferenceManager`/`ChatRoomPreferenceManager`/`AppPreferencesDataStoreProvider``resetForTest()` 훅을 추가했다.
- 반영 파일: `app/build.gradle`, `AppPreferencesDataStoreProvider.kt`, `SharedPreferenceManager.kt`, `ChatRoomPreferenceManager.kt`, `app/src/androidTest/java/kr/co/vividnext/sodalive/runtime/DataStoreRuntimeRegressionTest.kt`, `app/src/androidTest/java/kr/co/vividnext/sodalive/runtime/MiniPlayerConnectionGuardTest.kt`.
- 실행 명령: `./gradlew :app:testDebugUnitTest :app:assembleDebug :app:assembleDebugAndroidTest`
- 결과: `BUILD SUCCESSFUL`.
- 실행 명령: `./gradlew :app:connectedDebugAndroidTest`
- 결과: `BUILD SUCCESSFUL` (SM-G960N - 10, 4 tests passed).
- 실행 명령: `./gradlew :app:testDebugUnitTest :app:assembleDebug`
- 결과: `BUILD SUCCESSFUL`.
- 실행 명령: `./gradlew :app:lintDebug`
- 결과: 실패. 기존 프로젝트 이슈(`AndroidManifest.xml``MissingClass(com.facebook.FacebookActivity)` 포함 18 errors, 577 warnings)로 동일하게 중단.