오프라인 퍼스트 아키텍처(Offline First Architecture)
오프라인 퍼스트 아키텍처(Offline First Architecture)
오프라인 퍼스트(Offline First) 설계란, 활성 네트워크 연결이 없더라도 애플리케이션이 정상적으로 동작하도록 로컬에 저장된 데이터를 우선 활용하고, 네트워크 연결이 복구되었을 때 원격 서버와 동기화하는 방식을 말합니다. 모바일 기기에서는 불안정하거나 간헐적인 인터넷 환경이 흔하기 때문에, 이러한 접근 방식은 사용자 경험을 크게 높여 줍니다. 특히 안드로이드 앱에서는 지하철이나 엘리베이터 등 네트워크가 끊기기 쉬운 환경에서 자주 사용되므로, 오프라인 퍼스트 아키텍처의 중요성은 더욱 커지고 있습니다.
이번 면접 질문을 통하여 아래 내용들을 학습하실 수 있습니다.
- 안드로이드에서 오프라인 퍼스트 아키텍처의 핵심 원칙을 설명할 수 있습니다.
- Room과 WorkManager가 어떻게 협력하여 로컬 영속성(local persistence)과 백그라운드 동기화를 지원하는지 파악할 수 있습니다.
- 읽기 관통(read through) 캐싱과 쓰기 관통(write through) 캐싱 전략을 정의할 수 있습니다.
- 로컬 데이터와 원격 데이터 간 동기화 시 충돌 해결(conflict resolution) 방법을 식별할 수 있습니다.
Room을 활용한 로컬 데이터 영속화
신뢰할 수 있는 오프라인 퍼스트 전략은 로컬 저장소를 단일 진실 공급원(single source of truth)으로 삼는 것에서 시작됩니다. Room 데이터베이스는 안드로이드에서 구조화된 데이터를 저장하기 위한 공식 권장 솔루션으로, 컴파일 타임에 SQLite 쿼리를 생성하여 런타임 오류를 사전에 방지합니다. 또한 Kotlin Flow와 통합되어 리액티브 UI 업데이트를 지원하며, 코루틴을 활용한 비동기 처리도 지원합니다.
@Entity
data class Article(
@PrimaryKey val id: Int,
val title: String,
val content: String,
val isSynced: Boolean = false
)
@Dao
interface ArticleDao {
@Query("SELECT * FROM Article")
fun getAllArticles(): Flow<List<Article>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertArticle(article: Article)
}
위의 코드에서 isSynced 플래그는 각 레코드가 서버에 푸시되었는지 여부를 추적하는 역할을 합니다. 이 플래그를 통해 아직 동기화되지 않은 레코드만 선별하여 서버로 전송할 수 있습니다. UI 계층은 getAllArticles()를 Flow로 관찰하므로, 네트워크 가용 여부와 관계없이 항상 최신 로컬 상태를 반영하게 됩니다.
WorkManager를 활용한 백그라운드 동기화
WorkManager는 앱이 종료되거나 기기가 재시작되더라도 반드시 실행되어야 하는 지연 작업(deferred task)을 안정적으로 처리합니다. 네트워크 가용성과 같은 제약 조건(constraints)을 지원하고 실패 시 자동 재시도 기능을 제공하기 때문에, 로컬 변경 사항을 원격 서버에 동기화하는 데 적합한 도구입니다.
class SyncWorker(
appContext: Context,
params: WorkerParameters
) : CoroutineWorker(appContext, params) {
override suspend fun doWork(): Result {
val dao = AppDatabase.getInstance(applicationContext)
.articleDao()
val unsynced = dao.getUnsyncedArticles() // 아직 동기화되지 않은 기사 목록 조회
return if (syncToServer(unsynced)) {
unsynced.forEach { dao.markSynced(it.id) } // 동기화 성공 시 플래그 업데이트
Result.success()
} else {
Result.retry() // 실패 시 지수 백오프로 재시도
}
}
}