오프라인 우선 앱을 만들 때 가장 어려운 부분은 로컬 저장이 아니라 "그다음"이다. 네트워크가 끊긴 동안 쌓인 변경을 어디에 보관할지, 앱이 죽어도 유실되지 않게 할지, 서버와 로컬이 동시에 수정된 레코드는 누가 이기는지. 이 세 가지를 직접 구현하다 보면 WorkManager 큐, 플래그 컬럼, 재시도 로직이 앱 DB 스키마 곳곳에 스며들어 나중에 걷어내기 힘들어진다.
SyncForge는 이 문제를 꽤 명확한 원칙 하나로 푼다. 앱의 엔티티는 기존처럼 Room(또는 자체 저장소)에 두고, 동기화에 필요한 상태는 라이브러리가 소유하는 별도의 SQLDelight DB에 격리하는 것이다. 아웃박스와 충돌 기록이 앱 스키마를 오염시키지 않는다는 점이 이 라이브러리의 핵심 가치다.
아웃박스 패턴을 라이브러리가 대신 소유한다
흐름은 단순하다. UI에서 저장하면 Room에 즉시 반영되고, 같은 변경을 아웃박스에 기록한다. 아웃박스는 디스크에 영속되므로 앱이 강제 종료돼도 살아남는다. 이후 sync()가 호출되면 푸시/풀이 백엔드와 통신한다.
// 저장은 지금까지 하던 Room 코드 그대로
taskDao.upsert(task)
// 변경을 아웃박스에 적재 — 오프라인이면 여기서 대기
syncForge.enqueueChange(task)
// 네트워크 복구, 앱 포그라운드 진입 등 원하는 시점에
syncForge.sync()
주목할 부분은 전송 계층이 플러거블하다는 점이다. Ktor든 OkHttp든 기존 네트워크 스택 위에 얹을 수 있고, 백엔드는 문서화된 REST 계약(POST /sync/push, GET /sync/pull)만 구현하면 된다. 목 서버와 백엔드 스타터가 저장소에 포함돼 있어 서버 없이도 전체 흐름을 돌려볼 수 있다.
인증도 선택적으로 내장돼 있다. 토큰을 TokenStore에 영속하고, 푸시 중 401을 만나면 리프레시 후 자동 재시도하는 흐름까지 라이브러리가 처리한다. 직접 짜면 은근히 손이 많이 가는 부분이라 반가운 지점이다.
충돌은 숨기지 않고 드러낸다
동기화 라이브러리를 고를 때 진짜 기준은 "충돌이 났을 때 무슨 일이 벌어지는가"다. SyncForge는 충돌을 별도 DB에 기록하고 해결 전략을 설정하도록 설계돼 있다. Last-Write-Wins로 자동 처리할 수도 있고, 충돌 레코드를 UI에 노출해 사용자가 고르게 할 수도 있다. Compose 헬퍼가 제공되어 동기화 상태(Offline/Pending 등)를 화면에 붙이는 작업도 수월하다.
셋업과 제약
Maven Central에 올라와 있고 BOM으로 버전을 맞춘다. 패키지명은 dev.syncforge.*, Gradle 좌표는 studio.syncforge를 쓴다는 점만 헷갈리지 않으면 된다.
dependencies {
implementation(platform("studio.syncforge:syncforge-bom:0.9.0-rc.4"))
implementation("studio.syncforge:syncforge")
ksp("studio.syncforge:syncforge-ksp")
}
KMP 지원 범위는 Android, iOS, JVM 데스크톱, 네이티브 macOS다. 엔티티 저장소는 Android에서 Room, 다른 타깃에서는 자체 스토어를 쓰는 구조라 공통 코드에 동기화 로직을 모을 수 있다.
어디에 맞고 어디에 안 맞나
잘 맞는 경우는 명확하다. 백엔드를 직접 통제할 수 있고, 태스크·메모·체크리스트처럼 레코드 단위 충돌 해결로 충분한 도메인이라면 직접 구현 대비 절약되는 코드가 상당하다. Room 기반 기존 앱에 스키마 변경 없이 얹을 수 있다는 것도 큰 장점이다.
반대로 Firebase나 Supabase처럼 서버까지 통째로 제공받고 싶다면 방향이 다르다. SyncForge는 서버 계약을 정의할 뿐 서버를 주지 않는다. 공동 편집 문서처럼 필드 수준 병합이나 CRDT가 필요한 도메인에도 레코드 단위 충돌 모델은 부족할 수 있다.
가장 큰 고려사항은 성숙도다. 현재 0.9.0-rc.4, 즉 1.0 이전 릴리스 후보 단계라 API가 바뀔 여지가 있다. 사이드 프로젝트나 신규 화면 한 곳에서 검증해 보고 넓히는 접근이 안전하다. 다만 아웃박스를 앱 DB에서 분리하는 설계 자체는 라이브러리 채택 여부와 무관하게 직접 구현할 때도 참고할 만하다.
※ 본 글은 정보 제공 목적이며 특정 제품·서비스의 추천이 아닙니다.
'안드로이드' 카테고리의 다른 글
| Compose에서 state를 어디서 읽느냐가 성능을 가른다 (0) | 2026.07.13 |
|---|---|
| NavController를 갈아엎은 이유, Navigation 3 뜯어보기 (0) | 2026.07.10 |
| dependencies { } 는 왜 함수처럼 안 생겼는데 함수일까 (0) | 2026.07.03 |
| 백만 줄 스레드 코드를 코루틴으로 바꾸되, 호출부는 한 줄도 건드리지 않는 법 (0) | 2026.06.30 |
| Kotlin 빌드 도구의 새 이름: Kotlin Toolchain 0.11이 바꾸는 것들 (0) | 2026.06.29 |