Ver5.0 ๐ถ ํฐ๋ ์์์๋ ๋ฉ์ฉกํ ์ฑ ๋ง๋ค๊ธฐ: ์๋๋ก์ด๋ ์คํ๋ผ์ธ ํผ์คํธ ์ฑ ๊ฐ๋ฐ ์์ ์ ๋ณต ๊ฐ์ด๋ ๐

๐ถ ํฐ๋ ์์์๋ ๋ฉ์ฉกํ ์ฑ ๋ง๋ค๊ธฐ: ์๋๋ก์ด๋ ์คํ๋ผ์ธ ํผ์คํธ ์ฑ ๊ฐ๋ฐ ์์ ์ ๋ณต ๊ฐ์ด๋ ๐
์งํ์ฒ ์ด ํ๊ฐ ๋ค๋ฆฌ๋ฅผ ๊ฑด๋๋ ์๊ฐ, ์๋ฆฌ๋ฒ ์ดํฐ ๋ฌธ์ด ๋ซํ๋ ์๊ฐ, ๋ฑ์ฐ๋ก ์ ์ ์ง์ '์๋น์ค ์ ๋จ' ํ์๊ฐ ๋จ๋ ์๊ฐโฆ
๊ทธ๋ ์ฑ์ด ํ์ ํ๋ฉด์ ๋ฑ
๊ธ๋ฑ
๊ธ ๋ก๋ฉ ์คํผ๋๋ง ๋๋ฆฌ๊ณ ์๋ค๋ฉด? ์ฌ์ฉ์๋ ์กฐ์ฉํ ์ฑ์ ๋ซ๊ณ , ๋ค์๋ ์ด์ง ์์์ง๋ ๋ชฐ๋ผ์. ๐
์คํ๋ผ์ธ ํผ์คํธ(Offline-first)๋ "๋คํธ์ํฌ๋ ์ธ์ ๋ ๋๊ธธ ์ ์๋ค"๋ฅผ ๊ธฐ๋ณธ ์ ์ ๋ก ๋๊ณ , ๊ธฐ๊ธฐ ์์ ๋ก์ปฌ ๋ฐ์ดํฐ๋ฅผ ์ฑ์ ๊ธฐ์ค(Source of Truth)์ผ๋ก ์ผ๋ ์ค๊ณ ์ฒ ํ์ ๋๋ค. ์ด ๊ธ์์๋ ์๋๋ก์ด๋ ๊ณต์ ์ํคํ ์ฒ ๊ฐ์ด๋๊ฐ ๊ถ์ฅํ๋ ๋ฐฉ์์ ๋ผ๋๋ก, RoomยทFlowยทWorkManager๋ฅผ ํ์ฉํ ์ค์ ๊ตฌํ, ๋๊ธฐํ ์ ๋ต, ์ถฉ๋ ํด๊ฒฐ, ํ ์คํธ๊น์ง ํ ๋ฒ์ ์ ๋ฆฌํด ๋ณผ๊ฒ์.
๐งญ 1. ์คํ๋ผ์ธ ํผ์คํธ๋ ์ ํํ ๋ฌด์์ผ๊น?
๋จผ์ ์คํด๋ถํฐ ํ๋ ํ๊ณ ๊ฐ๊ฒ์. ์คํ๋ผ์ธ ํผ์คํธ๋ "์ธํฐ๋ท ์์ด ์ฐ๋ ์ฑ"์ด ์๋๋๋ค. ์คํ๋ ค ๋๋ถ๋ถ์ ์คํ๋ผ์ธ ํผ์คํธ ์ฑ์ ์๋ฒ์ ์ด์ฌํ ํต์ ํ๋ ์ฑ์ด์์. ์ฐจ์ด์ ์ ํต์ ๊ฒฐ๊ณผ๋ฅผ ํ๋ฉด์ ๋ฐ๋ก ๋ฟ๋ฆฌ์ง ์๊ณ , ์ผ๋จ ๋ก์ปฌ ์ ์ฅ์์ ๊ธฐ๋กํ ๋ค ํ๋ฉด์ ๋ก์ปฌ ์ ์ฅ์๋ง ๋ฐ๋ผ๋ณธ๋ค๋ ๋ฐ ์์ต๋๋ค.
์๋๋ก์ด๋ ๊ฐ๋ฐ์ ๊ณต์ ๋ฌธ์(Android Developers์ "Build an offline-first app" ๊ฐ์ด๋)๋ ์คํ๋ผ์ธ ํผ์คํธ ์ฑ์ "์ธํฐ๋ท ์ ์ ์์ด๋ ํต์ฌ ๊ธฐ๋ฅ์ ์ ๋ถ ๋๋ ์ค์ํ ์ผ๋ถ๋ฅผ ์ํํ ์ ์๋ ์ฑ"์ผ๋ก ์ค๋ช ํ๊ณ , ์ด๋ฅผ ์ํด ๋ฐ์ดํฐ ๋ ์ด์ด์ ๋ก์ปฌ ๋ฐ์ดํฐ ์์ค์ ๋คํธ์ํฌ ๋ฐ์ดํฐ ์์ค๋ฅผ ํจ๊ป ๋๋ ๋ก์ปฌ ์ชฝ์ ์ ๊ท ์์ค(canonical source)๋ก ์ผ์ผ๋ผ๊ณ ์๋ดํฉ๋๋ค.
ํ๋ฉด ์ง์
โ API ํธ์ถ โ ์๋ต ๋๊ธฐ โ ํ๋ฉด ํ์.
๋คํธ์ํฌ๊ฐ ๋๋ฆฌ๋ฉด ํ๋ฉด๋ ๋๋ฆฌ๊ณ , ๋๊ธฐ๋ฉด ์๋ฌ ํ๋ฉด์ด ๋น๋๋ค. ์บ์๋ '์์ผ๋ฉด ์ข์ ๋ค'์ ๊ฐ๊น์ต๋๋ค.
ํ๋ฉด ์ง์
โ ๋ก์ปฌ DB ์ฆ์ ํ์ โ ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์๋ฒ์ ๋๊ธฐํ โ ๋ก์ปฌ DB ๊ฐฑ์ โ ํ๋ฉด ์๋ ๋ฐ์.
๋คํธ์ํฌ๋ '๋ฐ์ดํฐ๋ฅผ ์ต์ ์ผ๋ก ๋ง๋ค์ด ์ฃผ๋ ๋ณด์กฐ ์ธ๋ ฅ'์ด ๋ฉ๋๋ค.
๐ฏ ์ ์ง๊ธ ์คํ๋ผ์ธ ํผ์คํธ์ธ๊ฐ
5G ์๋์ ์ฌ ์คํ๋ผ์ธ ํ๋ น์ด๋๊ณ ์? ํ์ค์ ๋คํธ์ํฌ๋ ์๊ฐ๋ณด๋ค ์์ฃผ ํ๋ค๋ฆฝ๋๋ค. ์งํ ์ฃผ์ฐจ์ฅ, ๊ณ ์ธต ์๋ฆฌ๋ฒ ์ดํฐ, ๋นํ๊ธฐ ๋ชจ๋, ํด์ธ ๋ก๋ฐ ์ ํ, ๋ฐ์ดํฐ ์๊ธ ์ ์ฝ ๋ชจ๋, ์ฌ๋์ด ๋ชฐ๋ฆฐ ๊ณต์ฐ์ฅ๊ณผ ๊ฒฝ๊ธฐ์ฅ์ ํผ์ก ์ โฆ ๊ทธ๋ฆฌ๊ณ "์ฐ๊ฒฐ์ ๋์ด ์์ง๋ง ์ฌ์ค์ ์ ๋๋" ์ ์ยท๋ถ์์ ์ฐ๊ฒฐ(์ผ๋ช Lie-fi)์ด ๊ฐ์ฅ ๊ณจ์น ์ํ ์ํฉ์ด์ฃ .
๋ก์ปฌ DB ์กฐํ๋ ๋ณดํต ๋คํธ์ํฌ ์๋ณต๋ณด๋ค ํจ์ฌ ๋น ๋ฆ ๋๋ค. ํ๋ฉด์ด '์ฆ์' ๋จ๋ ๊ฒฝํ ์์ฒด๊ฐ ํ์ง์ด ๋ฉ๋๋ค.
์ฌ์ฉ์๊ฐ ์์ฑํ ๋ฉ๋ชจยท์ฃผ๋ฌธ์ยท์ ๊ฒํ๊ฐ ์ ์ก ์คํจ๋ก ๋ ์๊ฐ์ง ์์ต๋๋ค. "์ ์ฅํ๋๋ฐ ์ฌ๋ผ์ก์ด์" CS๊ฐ ์ค์ด๋ญ๋๋ค.
ํ์ํ ๋ณ๊ฒฝ๋ถ๋ง ๋ฌถ์ด์ ์ ์ ํ ์์ (์: Wi-Fi, ์ถฉ์ ์ค)์ ๋๊ธฐํํ๋ฉด ๋ถํ์ํ ํต์ ์ ์ค์ผ ์ ์์ต๋๋ค.
๋ฌผ๋ฅยท๋ฐฐ์ก, ์ค๋น ์ ๊ฒ, ๋์ , ์๋ฃ ๋ฐฉ๋ฌธ, ๊ฑด์ค ํ์ฅ์ฒ๋ผ ํต์ ์์ ์ง์ญ์์ ์ฐ๋ ์ ๋ฌด ์ฑ์ ์ฌ์ค์ ํ์์ ๋๋ค.
๐ก ํ ์ค ์ ์
์คํ๋ผ์ธ ํผ์คํธ = "ํ๋ฉด์ ํญ์ ๋ก์ปฌ์ ๋ณธ๋ค + ๋คํธ์ํฌ๋ ๋ก์ปฌ์ ์ต์ ์ผ๋ก ๋ง๋ ๋ค + ์ฐ๊ธฐ๋ ์ผ๋จ ๋ก์ปฌ์ ํ๊ณ ๋์ค์ ์๋ฒ๋ก ๋ณด๋ธ๋ค"
๐๏ธ 2. ๊ณต์ ์ํคํ ์ฒ๋ก ๋ณด๋ ๋ผ๋: ๋ฐ์ดํฐ ๋ ์ด์ด์ ์ฌ๊ตฌ์ฑ
์๋๋ก์ด๋ ๊ถ์ฅ ์ฑ ์ํคํ ์ฒ๋ ํฌ๊ฒ UI ๋ ์ด์ด, (์ ํ์ ) ๋๋ฉ์ธ ๋ ์ด์ด, ๋ฐ์ดํฐ ๋ ์ด์ด๋ก ๋๋ฉ๋๋ค. ์คํ๋ผ์ธ ํผ์คํธ์ ์น๋ถ์ฒ๋ ๋จ์ฐ ๋ฐ์ดํฐ ๋ ์ด์ด, ๊ทธ์ค์์๋ Repository์ ๋๋ค. Repository๊ฐ ๋ก์ปฌ๊ณผ ์๊ฒฉ ๋ฐ์ดํฐ ์์ค๋ฅผ ์กฐ์จํ๊ณ , ๋ฐ๊นฅ(ViewModel)์๋ "๋ก์ปฌ ๊ธฐ์ค์ ๋ฐ์ดํฐ ์คํธ๋ฆผ"๋ง ๋ ธ์ถํ๋ ๊ตฌ์กฐ์ฃ .
๐ ํต์ฌ ์์น ์ธ ๊ฐ์ง
๋ก์ปฌ ๋ฐ์ดํฐ ์์ค๊ฐ ๋จ์ผ ์ง์ค ๊ณต๊ธ์(SSOT)
์ฑ ์์ ๋ ์ด์ด๊ฐ ์ฝ๋ ๋ฐ์ดํฐ๋ ์ค์ง ๋ก์ปฌ DB์์ ๋์ต๋๋ค. ๋คํธ์ํฌ ์๋ต์ ViewModel์ ์ง์ ๋๊ธฐ์ง ์์ต๋๋ค.
์ฝ๊ธฐ๋ ๊ด์ฐฐ ๊ฐ๋ฅํ ์คํธ๋ฆผ์ผ๋ก
Room DAO๊ฐ Flow๋ฅผ ๋ฐํํ๋ฉด, ๋๊ธฐํ ์์
์ด DB๋ฅผ ๊ฐฑ์ ํ๋ ์ฆ์ ํ๋ฉด๋ ์๋์ผ๋ก ๋ฐ๋๋๋ค. "์๋ก๊ณ ์นจ ๋ฒํผ ๋๋ฅด๋ฉด ๋ค์ ๋ถ๋ฌ์ค๊ธฐ" ๊ฐ์ ์๋ ํ๋ฆ์ด ์ฌ๋ผ์ง๋๋ค.
๋คํธ์ํฌ ์์
์ ์ฑ ์๋ช
์ฃผ๊ธฐ์ ๋ถ๋ฆฌ
์ฌ์ฉ์๊ฐ ํ๋ฉด์ ๋ซ๊ฑฐ๋ ์ฑ์ด ์ข
๋ฃ๋ผ๋ ๋๊ธฐํ๋ ์ด์ด์ ธ์ผ ํฉ๋๋ค. ์ด ์ญํ ์ WorkManager๊ฐ ๋งก์ต๋๋ค.
๐๏ธ ๋ชจ๋ธ์ ์ธ ๊ฒน์ผ๋ก ๋๋๋ ์ด์
์ค๋ฌด์์๋ ๊ฐ์ 'ํ ์ผ' ๋ฐ์ดํฐ๋ผ๋ ๋ชจ๋ธ์ ๋ ์ด์ด๋ณ๋ก ๋ถ๋ฆฌํ๋ ๊ฒฝ์ฐ๊ฐ ๋ง์ต๋๋ค. ๊ตฌ๊ธ์ ๊ณต์ ์ํ ์ฑ Now in Android๋ ๋คํธ์ํฌ ๋ชจ๋ธ, ์ํฐํฐ(DB) ๋ชจ๋ธ, ์ธ๋ถ ๋ ธ์ถ์ฉ ๋ชจ๋ธ์ ๋ถ๋ฆฌํ๋ ๊ตฌ์กฐ๋ฅผ ๋ณด์ฌ ์ค๋๋ค.
| ๋ชจ๋ธ | ์์ ์ด๋ฆ | ์ญํ |
|---|---|---|
| ๋คํธ์ํฌ ๋ชจ๋ธ | NetworkTask | ์๋ฒ JSON ๊ตฌ์กฐ ๊ทธ๋๋ก. ์๋ฒ ์คํ ๋ณ๊ฒฝ์ ์ถฉ๊ฒฉ์ ์ฌ๊ธฐ์ ํก์ |
| ์ํฐํฐ ๋ชจ๋ธ | TaskEntity | Room ํ ์ด๋ธ ๊ตฌ์กฐ. ๋๊ธฐํ ์ํยท๋ก์ปฌ ์์ ์๊ฐ ๋ฑ ๋ฉํ๋ฐ์ดํฐ ํฌํจ |
| ์ธ๋ถ ๋ชจ๋ธ | Task | UIยท๋๋ฉ์ธ์ด ์ฐ๋ ๊น๋ํ ๋ชจ๋ธ. ๋๊ธฐํ ๋ฉํ๋ฐ์ดํฐ๋ ์จ๊น |
์ด๋ ๊ฒ ๋๋๋ฉด ์๋ฒ๊ฐ ํ๋๋ช
์ ๋ฐ๊ฟ๋ UI ์ฝ๋๋ ๋ฌด์ฌํ๊ณ , DB์ syncState ๊ฐ์ ๋ด๋ถ์ฉ ์ปฌ๋ผ์ ์ถ๊ฐํด๋ ํ๋ฉด ์ฝ๋๊ฐ ์ค์ผ๋์ง ์์ต๋๋ค. ์ฒ์์ ๋ฒ๊ฑฐ๋ก์ ๋ณด์ฌ๋ ์ฑ์ด ์ปค์ง์๋ก ํจ๊ณผ๊ฐ ์ปค์ง๋, ์ผ์ข
์ '์ํคํ
์ฒ ์ ๊ธ'์ด์์. ๐ฐ
๐งฑ 3. ๋ก์ปฌ ์ ์ฅ์ ๊ณ ๋ฅด๊ธฐ: Room, DataStore, ๊ทธ๋ฆฌ๊ณ ํ์ผ
์คํ๋ผ์ธ ํผ์คํธ์ ์ฒด๋ ฅ์ ๋ก์ปฌ ์ ์ฅ์์์ ๋์ต๋๋ค. ์๋๋ก์ด๋์์ ๋ง์ด ์ฐ๋ ์ ํ์ง๋ฅผ ๋น๊ตํด ๋ณผ๊ฒ์.
| ์ ์ฅ์ | ์ ๋ง๋ ๋ฐ์ดํฐ | ํน์ง |
|---|---|---|
| Room | ๋ชฉ๋ก, ๊ด๊ณํ ๋ฐ์ดํฐ, ๊ฒ์ยท์ ๋ ฌ์ด ํ์ํ ๋ฐ์ดํฐ | SQLite ์์ ์ถ์ํ ๊ณ์ธต. ์ปดํ์ผ ํ์ SQL ๊ฒ์ฆ, Flowยท์ฝ๋ฃจํด ์ง์, ๋ง์ด๊ทธ๋ ์ด์ ์ง์ |
| DataStore (Preferences) | ์ค์ ๊ฐ, ๋ง์ง๋ง ๋๊ธฐํ ์๊ฐ, ํ ํฐ ๊ฐ์ ์๋ ํค-๊ฐ | SharedPreferences ๋์ฒด๋ก ๊ถ์ฅ. ์ฝ๋ฃจํดยทFlow ๊ธฐ๋ฐ ๋น๋๊ธฐ API |
| DataStore (Proto) | ํ์ ์ด ๋ช ํํ ์๋ ๊ตฌ์กฐํ ๋ฐ์ดํฐ | Protocol Buffers ์คํค๋ง๋ก ํ์ ์์ ์ฑ ํ๋ณด |
| ํ์ผ ์ ์ฅ์ | ์ด๋ฏธ์ง, ์ค๋์ค, ์ฒจ๋ถ ํ์ผ, ๋์ฉ๋ ๋ฐ์ด๋๋ฆฌ | DB์๋ ํ์ผ ๊ฒฝ๋กยท๋ฉํ๋ฐ์ดํฐ๋ง ์ ์ฅํ๋ ๋ฐฉ์์ด ์ผ๋ฐ์ |
๐ก SQLDelight ๊ฐ์ ๋์๋ ์์ต๋๋ค. Kotlin Multiplatform์ผ๋ก iOS์ ๋ฐ์ดํฐ ๋ ์ด์ด๋ฅผ ๊ณต์ ํ๋ ค๋ ํ์ SQLDelight๋ฅผ ๊ฒํ ํ๊ณค ํ๊ณ , ์ต๊ทผ์๋ Room๋ KMP ์ง์์ ๋ํ ๊ฐ๊ณ ์์ด์. ๋ค๋ง ์์ ์๋๋ก์ด๋ ์ฑ์ด๋ผ๋ฉด Jetpack ์ํ๊ณ์ ๋ง๋ฌผ๋ฆฌ๋ Room์ด ๊ฐ์ฅ ๋ฌด๋ํ ์ถ๋ฐ์ ์ ๋๋ค.
๐งฉ ๋๊ธฐํ๋ฅผ ๊ณ ๋ คํ ์ํฐํฐ ์ค๊ณ
์คํ๋ผ์ธ ํผ์คํธ ์ํฐํฐ๋ "๋ฐ์ดํฐ + ๋๊ธฐํ ๋ฉํ๋ฐ์ดํฐ"๋ฅผ ํจ๊ป ํ์ต๋๋ค. ์๋๋ ํ ์ผ ์ฑ์ ์๋ก ๋ ์ํฐํฐ์์.
enum class SyncState { SYNCED, PENDING_CREATE, PENDING_UPDATE, PENDING_DELETE }
@Entity(tableName = "tasks")
data class TaskEntity(
@PrimaryKey val id: String, // ํด๋ผ์ด์ธํธ์์ ์์ฑํ UUID
val title: String,
val done: Boolean,
val updatedAt: Long, // ๋ง์ง๋ง ์์ ์๊ฐ(๋ฐ๋ฆฌ์ด)
val serverVersion: Long? = null, // ์๋ฒ๊ฐ ๋ถ์ฌํ ๋ฒ์ (์ถฉ๋ ๊ฐ์ง์ฉ)
val syncState: SyncState = SyncState.SYNCED,
val isDeleted: Boolean = false // ์ํํธ ์ญ์ (ํฐ์คํค)
)
์ฌ๊ธฐ์๋ ์คํ๋ผ์ธ ํผ์คํธ์ ๋จ๊ณจ ๋ ธํ์ฐ๊ฐ ์ธ ๊ฐ์ง ์จ์ด ์์ต๋๋ค.
์๋ฒ ์๋ ์ฆ๊ฐ ID๋ฅผ ๊ธฐ๋ค๋ฆฌ๋ฉด ์คํ๋ผ์ธ์์ ์ ํญ๋ชฉ์ ๋ง๋ค ์ ์์ต๋๋ค. UUID.randomUUID()๋ก ๊ธฐ๊ธฐ์์ ID๋ฅผ ๋ง๋ค๋ฉด ์์ฑ ์ฆ์ ๋ค๋ฅธ ๋ฐ์ดํฐ์ ์ฐ๊ฒฐ(์ฐธ์กฐ)ํ ์ ์์ด์.
๋ก์ปฌ์์ ํ์ ๋ฐ๋ก ์ง์ฐ๋ฉด "์ญ์ ํ๋ค"๋ ์ฌ์ค์ ์๋ฒ์ ์ ๋ฌํ ๋จ์๊ฐ ์ฌ๋ผ์ง๋๋ค. ์ญ์ ํ์๋ง ํด๋๊ณ ์๋ฒ ๋ฐ์ ํ ์ค์ ์ญ์ ํฉ๋๋ค.
์ด๋ค ํ์ด ์์ง ์๋ฒ์ ์ ๊ฐ๋์ง ์์์ผ ์ฌ์๋ํ ์ ์์ต๋๋ค. UI์ '์ ์ก ๋๊ธฐ' ๋ฐฐ์ง๋ฅผ ๋ณด์ฌ์ค ์๋ ์์ฃ .
๐ DAO: ์ฝ๊ธฐ๋ Flow, ์ฐ๊ธฐ๋ suspend
@Dao
interface TaskDao {
@Query("SELECT * FROM tasks WHERE isDeleted = 0 ORDER BY updatedAt DESC")
fun observeTasks(): Flow<List<TaskEntity>>
@Query("SELECT * FROM tasks WHERE syncState != 'SYNCED'")
suspend fun getPendingTasks(): List<TaskEntity>
@Upsert
suspend fun upsert(task: TaskEntity)
@Upsert
suspend fun upsertAll(tasks: List<TaskEntity>)
@Query("DELETE FROM tasks WHERE id = :id")
suspend fun hardDelete(id: String)
}
@Upsert๋ Room 2.5.0๋ถํฐ ์ ๊ณต๋๋ ์ด๋
ธํ
์ด์
์ผ๋ก, ๊ธฐ๋ณธ ํค๊ฐ ์์ผ๋ฉด ๊ฐฑ์ ํ๊ณ ์์ผ๋ฉด ์ฝ์
ํฉ๋๋ค. ์๋ฒ์์ ๋ฐ์ ๋ฐ์ดํฐ๋ฅผ ๋ก์ปฌ์ ๋ฎ์ด์ธ ๋ ์์ฃผ ํธ๋ฆฌํฉ๋๋ค. ๊ทธ๋ฆฌ๊ณ observeTasks()๊ฐ ๋ฐํํ๋ Flow๋ ํด๋น ํ
์ด๋ธ์ด ๋ฐ๋ ๋๋ง๋ค ์ ๋ชฉ๋ก์ ๋ค์ ๋ด๋ณด๋ด๊ธฐ ๋๋ฌธ์, ํ๋ฉด์ '๋ค์ ๊ทธ๋ฆฌ๋ ์ฑ
์'์ DB๊ฐ ๋์ ์ง๊ฒ ๋ฉ๋๋ค.
โ ๏ธ Enum ์ ์ฅ ์ฃผ์
Room์ enum์ ๊ธฐ๋ณธ์ ์ผ๋ก ์ด๋ฆ ๋ฌธ์์ด๋ก ์ ์ฅํ ์ ์์ง๋ง, ๋์ค์ enum ์ด๋ฆ์ ๋ฐ๊พธ๋ฉด ๊ธฐ์กด ๋ฐ์ดํฐ์ ์ด๊ธ๋ฉ๋๋ค. ์ด๋ฆ ๋ณ๊ฒฝ ๊ฐ๋ฅ์ฑ์ด ์๋ค๋ฉด TypeConverter๋ก ๊ณ ์ ์ฝ๋๊ฐ์ ์ ์ฅํ๋ ํธ์ด ์์ ํฉ๋๋ค. ์ ์ฟผ๋ฆฌ์ 'SYNCED' ๊ฐ์ ๋ฌธ์์ด ๋น๊ต๋ ์ ์ฅ ๋ฐฉ์์ ๋ง์ถฐ์ผ ํด์.
๐ 4. ์ฝ๊ธฐ ์ ๋ต: ๋ก์ปฌ์ ๋ณด์ฌ์ฃผ๊ณ , ์๊ฒฉ์ผ๋ก ์ฑ์ด๋ค
์คํ๋ผ์ธ ํผ์คํธ ์ฝ๊ธฐ์ ๊ธฐ๋ณธ ํจํด์ ์์ฃผ ๋จ์ํฉ๋๋ค. Repository๋ DAO์ Flow๋ฅผ ๊ทธ๋๋ก(๋ชจ๋ธ ๋ณํ๋ง ํด์) ๋ด๋ณด๋ธ๋ค. ๋์ด์์. ์๋ฒ ํธ์ถ์ ์ด ํ๋ฆ์ ๋ผ์ด๋ค์ง ์์ต๋๋ค.
class OfflineFirstTaskRepository @Inject constructor(
private val taskDao: TaskDao,
private val api: TaskApi,
) : TaskRepository {
override fun observeTasks(): Flow<List<Task>> =
taskDao.observeTasks().map { list -> list.map { it.asExternalModel() } }
// ๋๊ธฐํ๋ ๋ณ๋ ํจ์ โ ํ๋ฉด ํ๋ฆ๊ณผ ๋ถ๋ฆฌ
override suspend fun refresh(): Result<Unit> = runCatching {
val remote = api.getTasks()
taskDao.upsertAll(remote.map { it.asEntity() })
}
}
ViewModel์ ์ด Flow๋ฅผ stateIn์ผ๋ก UI ์ํ๋ก ๋ฐ๊ฟ ๋ก๋๋ค.
@HiltViewModel
class TaskViewModel @Inject constructor(
repository: TaskRepository
) : ViewModel() {
val uiState: StateFlow<TaskUiState> = repository.observeTasks()
.map { TaskUiState.Success(it) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = TaskUiState.Loading
)
}
WhileSubscribed(5_000)์ ํ๋ฉด ํ์ ์ฒ๋ผ ์งง๊ฒ ๊ตฌ๋
์๊ฐ ์ฌ๋ผ์ง๋ ๊ฒฝ์ฐ์๋ ์๋ฅ Flow๋ฅผ ์ ์งํ๊ณ , 5์ด ์ด์ ์๋ฌด๋ ๋ณด์ง ์์ผ๋ฉด ์ ์งํ๋๋ก ํ๋ ์ค์ ์
๋๋ค. ๊ณต์ ์ํ๋ค์์๋ ์์ฃผ ๋ณด์ด๋ ๊ฐ์ด์์.
๐ง ๋น ํ๋ฉด vs ๋ก๋ฉ vs ์คํ๋ผ์ธ: ์ํ๋ฅผ ์ ์งํ๊ฒ
๋ก์ปฌ ์ฐ์ ์ด๋ผ๊ณ ํด์ ๋ชจ๋ ์ํฉ์ด ์๋ฆ๋ต์ง ์์ต๋๋ค. ์ฑ์ ์ค์นํ๊ณ ์ฒ์ ์ฐ ์๊ฐ ์คํ๋ผ์ธ์ด๋ผ๋ฉด ๋ก์ปฌ DB๋ ํ ๋น์ด ์์ฃ . ์ด๋ "ํ ์ผ์ด ์์ต๋๋ค ๐"๋ฅผ ๋ณด์ฌ์ฃผ๋ฉด ๊ฑฐ์ง๋ง์ด ๋ฉ๋๋ค. ๊ทธ๋์ UI ์ํ๋ ์ต์ํ ๋ค์์ ๊ตฌ๋ถํ๋ ๊ฒ์ด ์ข์ต๋๋ค.
| ๋ก์ปฌ ๋ฐ์ดํฐ | ๋คํธ์ํฌ | ๊ถ์ฅ ํ๋ฉด |
|---|---|---|
| ์์ | ์ฐ๊ฒฐ๋จ | ๋ฐ์ดํฐ ํ์ + (์ ํ) ์๋จ์ ์์ํ ๊ฐฑ์ ํ์ |
| ์์ | ๋๊น | ๋ฐ์ดํฐ ํ์ + "์คํ๋ผ์ธ ยท ๋ง์ง๋ง ๋๊ธฐํ 10๋ถ ์ " ๋ฐฐ๋ |
| ์์(์ต์ด ์คํ) | ์ฐ๊ฒฐ๋จ | ์ค์ผ๋ ํค ๋ก๋ฉ |
| ์์(์ต์ด ์คํ) | ๋๊น | "์ฐ๊ฒฐ๋๋ฉด ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ฌ๊ฒ์" ์๋ด + ์ฌ์๋ |
| ์์(๋๊ธฐํ ์๋ฃ) | ๋ฌด๊ด | ์ง์ง ๋น ์ํ(Empty State) ์ผ๋ฌ์คํธ |
"์ง์ง ๋น์ด ์์"๊ณผ "์์ง ๋ชป ๋ฐ์์ด"์ ๊ตฌ๋ถํ๋ ค๋ฉด ์ต์ด ๋๊ธฐํ ์๋ฃ ์ฌ๋ถ๋ ๋ง์ง๋ง ๋๊ธฐํ ์๊ฐ์ DataStore ๋ฑ์ ์ ์ฅํด ๋๋ฉด ๋ฉ๋๋ค.
๐ ๋์ฉ๋ ๋ชฉ๋ก: Paging 3์ RemoteMediator
ํผ๋์ฒ๋ผ ๋์์ด ์ด์ด์ง๋ ๋ชฉ๋ก์ ์ ์ฒด๋ฅผ ํ ๋ฒ์ ๋ฐ์๋ ์ ์์ต๋๋ค. ์ด๋ด ๋ Paging 3 ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ RemoteMediator๊ฐ ์คํ๋ผ์ธ ํผ์คํธ ๋ฐฉ์์ ํ์ด์ง๋ค์ด์ ์ ์ง์ํฉ๋๋ค. ๊ตฌ์กฐ๋ ์ด๋ ์ต๋๋ค.
UI๋ Room์ด ๋ง๋ PagingSource๋ง ๊ตฌ๋
ํฉ๋๋ค.
๋ก์ปฌ ๋ฐ์ดํฐ๊ฐ ๋ฐ๋ฅ๋๋ฉด Paging์ด RemoteMediator.load()๋ฅผ ํธ์ถํฉ๋๋ค(์๋ก๊ณ ์นจยท์ยท๋ค ๋ฐฉํฅ์ LoadType์ผ๋ก ๊ตฌ๋ถ).
RemoteMediator๋ ๋คํธ์ํฌ์์ ๋ค์ ํ์ด์ง๋ฅผ ๋ฐ์ DB์ ์ ์ฅํฉ๋๋ค. ๋ค์ ํ์ด์ง ํค๋ ๋ณ๋ '๋ฆฌ๋ชจํธ ํค' ํ ์ด๋ธ์ ๋ณด๊ดํ๋ ๊ฒ์ด ์ผ๋ฐ์ ์ ๋๋ค.
DB๊ฐ ๋ฐ๋๋ฉด PagingSource๊ฐ ๋ฌดํจํ๋๊ณ , ์ ๋ฐ์ดํฐ๊ฐ ํ๋ฉด์ ์์ฐ์ค๋ฝ๊ฒ ์ด์ด ๋ถ์ต๋๋ค.
@OptIn(ExperimentalPagingApi::class)
val pager = Pager(
config = PagingConfig(pageSize = 30),
remoteMediator = ArticleRemoteMediator(api, db),
pagingSourceFactory = { db.articleDao().pagingSource() }
).flow
์คํ๋ผ์ธ์ผ ๋ ์ฌ์ฉ์๋ ์ด๋ฏธ ๋ฐ์ ๋ ํ์ด์ง๊น์ง ์คํฌ๋กคํ ์ ์๊ณ , ์ฐ๊ฒฐ์ด ๋์์ค๋ฉด ์ด์ด์ ๋ก๋๋ฉ๋๋ค. ๊ตฌ์กฐ ์์ฒด๊ฐ ์คํ๋ผ์ธ ํผ์คํธ์์.
โ๏ธ 5. ์ฐ๊ธฐ ์ ๋ต: ์ธ ๊ฐ์ง ์ ํ์ง
์ฝ๊ธฐ๋ ๋น๊ต์ ์ฝ์ต๋๋ค. ์ง์ง ์ด๋ ค์ด ๊ฑด ์ฐ๊ธฐ์ฃ . ๊ณต์ ๊ฐ์ด๋๋ ์คํ๋ผ์ธ ํผ์คํธ ์ฑ์ ์ฐ๊ธฐ๋ฅผ ํฌ๊ฒ ์ธ ๊ฐ์ง ์ ๋ต์ผ๋ก ์ ๋ฆฌํฉ๋๋ค. ๊ธฐ๋ฅ๋ง๋ค ๋ค๋ฅธ ์ ๋ต์ ์์ด ์ฐ๋ ๊ฒ์ด ์ ์์ ๋๋ค.
๋คํธ์ํฌ๋ก๋ง ์ฐ๊ธฐ๋ฅผ ์๋ํ๊ณ , ์ฑ๊ณตํ๋ฉด ๋ก์ปฌ์ ๊ฐฑ์ ํฉ๋๋ค. ์คํจํ๋ฉด ์ฌ์ฉ์์๊ฒ ์๋ฆฌ๊ฑฐ๋ ์ฆ์ ์ฌ์๋.
์ด์ธ๋ฆฌ๋ ๊ณณ: ๊ฒฐ์ , ์ก๊ธ, ์ข์ ์์ฝ์ฒ๋ผ ์๋ฒ์ ์ฆ๊ฐ ํ์ ์ด ํ์ํ ๋์.
์ฐ๊ธฐ ์์ฒญ์ ๋ก์ปฌ ํ(๋๋ DB)์ ๋ฃ๊ณ , ๋คํธ์ํฌ๊ฐ ๊ฐ๋ฅํ ๋ ์์๋๋ก ์ ์กํฉ๋๋ค.
์ด์ธ๋ฆฌ๋ ๊ณณ: ๋ถ์ ์ด๋ฒคํธ, ๋ก๊ทธ, ์์๊ฐ ์ค์ํ ๋ช
๋ น.
๋จผ์ ๋ก์ปฌ DB์ ๊ธฐ๋กํ๊ณ ํ๋ฉด์ ์ฆ์ ๋ฐ์ํ ๋ค, ๋์ค์ ์๋ฒ์ ๋๊ธฐํํฉ๋๋ค.
์ด์ธ๋ฆฌ๋ ๊ณณ: ๋ฉ๋ชจ, ํ ์ผ, ์ฆ๊ฒจ์ฐพ๊ธฐ, ์ฅ๋ฐ๊ตฌ๋ ๋ฑ ๋๋ถ๋ถ์ ์ฌ์ฉ์ ๋ฐ์ดํฐ.
โก ์ง์ฐ ์ฐ๊ธฐ ๊ตฌํ: ๋๊ด์ ์ ๋ฐ์ดํธ
์ฌ์ฉ์๊ฐ ํ ์ผ์ ์ฒดํฌํ๋ ์๊ฐ, ์๋ฒ ์๋ต์ ๊ธฐ๋ค๋ฆฌ์ง ์๊ณ ๋ฐ๋ก ์ฒดํฌ๋ ๋ชจ์ต์ ๋ณด์ฌ์ฃผ๋ ๊ฒ์ ๋๊ด์ ์ ๋ฐ์ดํธ(Optimistic Update)๋ผ๊ณ ํฉ๋๋ค. ์คํ๋ผ์ธ ํผ์คํธ์์๋ ๋ก์ปฌ DB์ ์ฐ๊ธฐ๋ง ํ๋ฉด Flow๊ฐ ์์์ ํ๋ฉด์ ๋ฐ๊พธ๋, ๋๊ด์ ์ ๋ฐ์ดํธ๊ฐ ๊ฑฐ์ ๊ณต์ง๋ก ๋ฐ๋ผ์ต๋๋ค.
override suspend fun toggleDone(id: String, done: Boolean) {
val current = taskDao.getById(id) ?: return
taskDao.upsert(
current.copy(
done = done,
updatedAt = System.currentTimeMillis(),
syncState = if (current.syncState == SyncState.PENDING_CREATE)
SyncState.PENDING_CREATE // ์์ง ์๋ฒ์ ์๋ ํญ๋ชฉ์ด๋ฉด ์์ฑ ๋๊ธฐ ์ ์ง
else SyncState.PENDING_UPDATE
)
)
syncScheduler.requestSync() // WorkManager์ ๋๊ธฐํ ์์ฝ
}
override suspend fun delete(id: String) {
val current = taskDao.getById(id) ?: return
if (current.syncState == SyncState.PENDING_CREATE) {
taskDao.hardDelete(id) // ์๋ฒ์ ๊ฐ ์ ์์ผ๋ ๊ทธ๋ฅ ์ญ์
} else {
taskDao.upsert(current.copy(isDeleted = true,
syncState = SyncState.PENDING_DELETE,
updatedAt = System.currentTimeMillis()))
syncScheduler.requestSync()
}
}
๐ก ์ํ ์ ์ด ๋ํ
์ผ์ด ํ์ง์ ๊ฐ๋ฅธ๋ค
"์์ฑ ๋๊ธฐ" ์ํ์ธ ํญ๋ชฉ์ ์์ ํ๋ฉด ์ฌ์ ํ "์์ฑ ๋๊ธฐ"์ฌ์ผ ํ๊ณ , "์์ฑ ๋๊ธฐ" ํญ๋ชฉ์ ์ญ์ ํ๋ฉด ์๋ฒ์ ์๋ฆด ํ์ ์์ด ๋ก์ปฌ์์ ์ง์ฐ๋ฉด ๋ฉ๋๋ค. ์ด๋ฐ ์ ์ด ๊ท์น์ ๋์น๋ฉด ์๋ฒ์ ์กด์ฌํ์ง ์๋ ํญ๋ชฉ์ ์
๋ฐ์ดํธ ์์ฒญ์ ๋ณด๋ด 404๊ฐ ๋๋ ๋ฒ๊ทธ๊ฐ ์๊น๋๋ค.
๐ฎ ์์๋ฐ์ค(Outbox) ํจํด: ๋ณ๊ฒฝ ์ฌํญ์ ๋ณ๋ ํ ์ด๋ธ๋ก
์ํฐํฐ์ ๋๊ธฐํ ์ํ ์ปฌ๋ผ์ ๋๋ ๋ฐฉ์ ์ธ์, ๋ณ๊ฒฝ ๋ช ๋ น ์์ฒด๋ฅผ ๋ณ๋ ํ ์ด๋ธ์ ์๋ ์์๋ฐ์ค ํจํด๋ ๋ง์ด ์๋๋ค. ํนํ "์ข์์ ๋๋ฅด๊ธฐ โ ์ทจ์ โ ๋ค์ ๋๋ฅด๊ธฐ"์ฒ๋ผ ๋์์ ์์์ ์๋ฏธ๊ฐ ์ค์ํ ๊ฒฝ์ฐ์ ์ ๋ฆฌํฉ๋๋ค.
@Entity(tableName = "outbox")
data class OutboxEntity(
@PrimaryKey(autoGenerate = true) val seq: Long = 0,
val entityId: String,
val operation: String, // "CREATE" | "UPDATE" | "DELETE"
val payloadJson: String,
val createdAt: Long,
val attempt: Int = 0
)
์ค์ํ ์ ์ ๋ฐ์ดํฐ ๋ณ๊ฒฝ๊ณผ ์์๋ฐ์ค ๊ธฐ๋ก์ ํ๋์ ํธ๋์ญ์
์ผ๋ก ๋ฌถ๋ ๊ฒ์
๋๋ค. Room์ withTransaction ๋ธ๋ก(room-ktx)์ ์ฐ๋ฉด "๋ฐ์ดํฐ๋ ๋ฐ๋์๋๋ฐ ์์๋ฐ์ค ๊ธฐ๋ก์ ๋น ์ก๋ค" ๊ฐ์ ๋ฐ์ชฝ ์ํ๋ฅผ ๋ง์ ์ ์์ด์.
suspend fun addTask(title: String) {
val entity = TaskEntity(UUID.randomUUID().toString(), title, false,
System.currentTimeMillis(), syncState = SyncState.PENDING_CREATE)
db.withTransaction {
db.taskDao().upsert(entity)
db.outboxDao().insert(OutboxEntity(entityId = entity.id,
operation = "CREATE", payloadJson = json.encodeToString(entity.asNetwork()),
createdAt = System.currentTimeMillis()))
}
syncScheduler.requestSync()
}
| ๋น๊ต | ์ํ ์ปฌ๋ผ ๋ฐฉ์ | ์์๋ฐ์ค ๋ฐฉ์ |
|---|---|---|
| ๊ตฌํ ๋์ด๋ | ๋ฎ์ | ์ค๊ฐ |
| ์ ์ก ๋จ์ | ์ต์ข ์ํ(์ค๋ ์ท) | ๊ฐ๋ณ ๋ช ๋ น(์ด๋ฒคํธ) |
| ์ค๊ฐ ๋ณ๊ฒฝ ์ด๋ ฅ | ๋ณํฉ๋์ด ์ฌ๋ผ์ง | ์์๋๋ก ๋ณด์กด |
| ์ ๋ง๋ ์๋ฒ API | REST์ PUT/PATCH ๋ฆฌ์์ค ๊ฐฑ์ | ๋ช ๋ นํ API, ์ด๋ฒคํธ ์ฒ๋ฆฌ |
๐ 6. ๋๊ธฐํ ์์ง: WorkManager๋ก '์ ๋ ํฌ๊ธฐํ์ง ์๋' ์ ์ก
๋ก์ปฌ์ ์์ธ ๋ณ๊ฒฝ ์ฌํญ์ ์๋ฒ๋ก ๋ณด๋ด๊ณ , ์๋ฒ์ ์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ค๋ ์์ ์ด ๋๊ธฐํ์ ๋๋ค. ์ด ์ผ์ ์ฑ์ด ๋ฐฑ๊ทธ๋ผ์ด๋๋ก ๊ฐ๊ฑฐ๋ ํ๋ก์ธ์ค๊ฐ ์ข ๋ฃ๋ผ๋ ์ด์ด์ ธ์ผ ํ๋ฏ๋ก, Android Jetpack์ WorkManager๊ฐ ๊ถ์ฅ ๋๊ตฌ์ ๋๋ค. WorkManager๋ ์ฑ์ด ์ข ๋ฃ๋๊ฑฐ๋ ๊ธฐ๊ธฐ๊ฐ ์ฌ๋ถํ ๋์ด๋ ์์ฝ๋ ์์ ์ด ์คํ๋๋๋ก ๋ณด์ฅํ๋ ์ง์ํ(persistent) ๋ฐฑ๊ทธ๋ผ์ด๋ ์์ API์์.
โ๏ธ ๋๊ธฐํ ์์ปค ๋ง๋ค๊ธฐ
@HiltWorker
class SyncWorker @AssistedInject constructor(
@Assisted context: Context,
@Assisted params: WorkerParameters,
private val syncer: TaskSyncer,
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result = try {
syncer.pushLocalChanges() // 1) ๋ก์ปฌ ๋ณ๊ฒฝ ์ฌํญ ์
๋ก๋
syncer.pullRemoteChanges() // 2) ์๋ฒ ๋ณ๊ฒฝ ์ฌํญ ๋ค์ด๋ก๋
Result.success()
} catch (e: IOException) {
Result.retry() // ๋คํธ์ํฌ ์ค๋ฅ: ๋ฐฑ์คํ ํ ์ฌ์๋
} catch (e: HttpException) {
if (e.code() in 500..599) Result.retry() else Result.failure()
}
}
์ค๋ฅ ์ข ๋ฅ๋ฅผ ๊ตฌ๋ถํ๋ ๊ฒ์ด ์ค์ํฉ๋๋ค. ๋คํธ์ํฌ ๋จ์ ์ด๋ ์๋ฒ 5xx ์ค๋ฅ๋ ์ผ์์ ์ด๋ ์ฌ์๋ํ ๊ฐ์น๊ฐ ์์ง๋ง, 400(์๋ชป๋ ์์ฒญ) ๊ฐ์ ์๊ตฌ์ ์ค๋ฅ๋ฅผ ๋ฌดํ ์ฌ์๋ํ๋ฉด ๋ฐฐํฐ๋ฆฌ๋ง ๋ น์ ๋๋ค. ์๊ตฌ ์ค๋ฅ ํญ๋ชฉ์ ๋ฐ๋ก ํ์ํด์ ์ฌ์ฉ์์๊ฒ ์๋ฆฌ๊ฑฐ๋ ๊ฒฉ๋ฆฌํ์ธ์.
๐ ๋๊ธฐํ ์์ฝ: ์ ์ฝ ์กฐ๊ฑด๊ณผ ๊ณ ์ ์์
class SyncScheduler @Inject constructor(
@ApplicationContext private val context: Context
) {
private val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
fun requestSync() {
val request = OneTimeWorkRequestBuilder<SyncWorker>()
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL,
WorkRequest.MIN_BACKOFF_MILLIS, TimeUnit.MILLISECONDS)
.build()
WorkManager.getInstance(context).enqueueUniqueWork(
"task-sync", ExistingWorkPolicy.KEEP, request)
}
fun schedulePeriodicSync() {
val request = PeriodicWorkRequestBuilder<SyncWorker>(6, TimeUnit.HOURS)
.setConstraints(constraints)
.build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"task-periodic-sync", ExistingPeriodicWorkPolicy.KEEP, request)
}
}
๋คํธ์ํฌ๊ฐ ์ฐ๊ฒฐ๋ ์ํ์์๋ง ์คํ๋ฉ๋๋ค. ์คํ๋ผ์ธ ์ค์ ์์ธ ์์
์ ์ฐ๊ฒฐ์ด ๋์์ค๋ฉด ์์คํ
์ด ์์์ ๊นจ์ ์ค๋๋ค. Wi-Fi์์๋ง ๋์ฉ๋ ์
๋ก๋๋ฅผ ํ๊ณ ์ถ๋ค๋ฉด UNMETERED๋ฅผ ์ธ ์ ์์ด์.
์ฌ์ฉ์๊ฐ ์ฒดํฌ๋ฐ์ค๋ฅผ 10๋ฒ ๋๋ฌ๋ ๋๊ธฐํ ์์ ์ด 10๊ฐ ์์ด์ง ์๋๋ก ์ด๋ฆ์ผ๋ก ๋ฌถ์ต๋๋ค. ์ ์ฑ (KEEP, REPLACE, APPEND ๋ฑ)์ ์๊ตฌ์ฌํญ์ ๋ง๊ฒ ๊ณ ๋ฆ ๋๋ค.
PeriodicWorkRequest์ ์ต์ ๋ฐ๋ณต ๊ฐ๊ฒฉ์ 15๋ถ์ ๋๋ค. ๊ทธ๋ณด๋ค ์ฆ์ ์ค์๊ฐ์ฑ์ด ํ์ํ๋ฉด ํธ์ ๊ธฐ๋ฐ ๋๊ธฐํ๋ฅผ ํจ๊ป ์จ์ผ ํฉ๋๋ค.
โ ๏ธ KEEP ์ ์ฑ
์ ํจ์
๋๊ธฐํ๊ฐ ์ด๋ฏธ ์คํ ์ค์ผ ๋ ์ ๋ณ๊ฒฝ์ด ์๊ธฐ๋ฉด, KEEP ์ ์ฑ
์ ์ ์์ฒญ์ ๋ฌด์ํฉ๋๋ค. ์คํ ์ค์ธ ์์ปค๊ฐ ์ด๋ฏธ ๋๊ธฐ ๋ชฉ๋ก์ ์ฝ์ ๋ค์๋ค๋ฉด ์ ๋ณ๊ฒฝ์ ๋ค์ ๋๊ธฐํ๊น์ง ๋ฐ๋ฆด ์ ์์ด์. ํด๊ฒฐ์ฑ
์ผ๋ก๋ ์์ปค๊ฐ ๋๋๊ธฐ ์ง์ ์ ๋จ์ ๋๊ธฐ ํญ๋ชฉ์ ๋ค์ ํ์ธํด ๋ฐ๋ณต ์ฒ๋ฆฌํ๊ฑฐ๋, APPEND_OR_REPLACE ์ ์ฑ
์ผ๋ก ํ์ ์์
์ ์ด์ด ๋ถ์ด๋ ๋ฐฉ๋ฒ์ด ์์ต๋๋ค.
๐ ์ฑ ์์ ์ ๋๊ธฐํ
Now in Android ์ํ์ ์ฑ ์์ ์ ๋๊ธฐํ ์์
์ ๋ฑ๋กํ๋ ๊ตฌ์กฐ๋ฅผ ๋ณด์ฌ์ค๋๋ค. ์ฑ ์์ ์์ ์ ๊ณ ์ ์์
์ผ๋ก ๋๊ธฐํ๋ฅผ ํ ๋ฒ ๊ฑธ์ด ๋๋ฉด, ์ฌ์ฉ์๊ฐ ํ๋ฉด์ ๋ณด๋ ๋์ ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์ต์ ๋ฐ์ดํฐ๊ฐ ์ฑ์์ง๋๋ค. ์ฆ์ ์คํ์ด ์ค์ํ๋ค๋ฉด setExpedited()๋ก ๊ธด๊ธ ์์
์ ์์ฒญํ ์ ์๋๋ฐ, ๊ธด๊ธ ์์
์ ์์คํ
ํ ๋น๋์ ์ ์ฝ์ ๋ฐ์ผ๋ฏ๋ก ํ ๋น๋ ์ด๊ณผ ์์ ์ ์ฑ
(OutOfQuotaPolicy)๋ ํจ๊ป ์ง์ ํด์ผ ํฉ๋๋ค.
๐ก 7. Pull vs Push: ์๋ฒ์ ๋ณํ๋ฅผ ์ด๋ป๊ฒ ์์์ฑ๊น
๋ก์ปฌ ๋ณ๊ฒฝ์ ์๋ฒ๋ก ์ฌ๋ฆฌ๋ ๊ฑด ์ฑ์ด ์ฃผ๋ํ์ง๋ง, ์๋ฒ์์ ์๊ธด ๋ณํ๋ฅผ ์๋ ๋ฐฉ๋ฒ์ ์ค๊ณ ์ ํ์ด ํ์ํฉ๋๋ค. ๊ณต์ ๊ฐ์ด๋๋ ์ด๋ฅผ ํ(Pull) ๊ธฐ๋ฐ๊ณผ ํธ์(Push) ๊ธฐ๋ฐ ๋๊ธฐํ๋ก ๊ตฌ๋ถํฉ๋๋ค.
| ํญ๋ชฉ | ๐งฒ Pull ๊ธฐ๋ฐ | ๐ฃ Push ๊ธฐ๋ฐ |
|---|---|---|
| ๋์ | ์ฑ์ด ํ์ํ ๋(ํ๋ฉด ์ง์ , ์ฃผ๊ธฐ์ ์ผ๋ก) ์๋ฒ์ ๋ฌผ์ด๋ด | ์๋ฒ๊ฐ ๋ณ๊ฒฝ ์ ์๋ฆผ(์: FCM)์ ๋ณด๋ด๊ณ , ์ฑ์ด ๊ทธ๋ ๊ฐ์ ธ์ด |
| ์ฅ์ | ๊ตฌํ์ด ๋จ์, ์๋ฒ ๋ถ๋ด ์์ธก ์ฌ์ | ๋ณ๊ฒฝ์ ๋น ๋ฅด๊ฒ ๋ฐ์, ๋ถํ์ํ ์กฐํ ๊ฐ์ |
| ๋จ์ | ๋ณ๊ฒฝ์ด ์์ด๋ ์กฐํ, ์ต์ ์ฑ ์ง์ฐ | ํธ์ ์ธํ๋ผ ํ์, ์๋ฆผ ๋๋ฝ ๋๋น ํ์ |
| ์ ํฉํ ์ฑ | ๋ด์ค, ์นดํ๋ก๊ทธ, ์ผ์ ๋ณ๊ฒฝ์ด ๋๋ฌธ ๋ฐ์ดํฐ | ๋ฉ์ ์ , ํ์ ๋๊ตฌ, ์ฌ๋ฌ ๊ธฐ๊ธฐ ๋์ ์ฌ์ฉ |
์ค๋ฌด์์๋ ํ์ด๋ธ๋ฆฌ๋๊ฐ ํํฉ๋๋ค. Now in Android ์ํ์ FCM ๊ฐ์ ๋ฉ์์ง๋ก "๋ณ๊ฒฝ์ด ์๋ค"๋ ์ ํธ๋ฅผ ๋ฐ์ผ๋ฉด ๋๊ธฐํ๋ฅผ ์คํํ๋ ๋ฐฉ์์ ์ค๋ช ํ๋๋ฐ, ์ด๋ ๋ฉ์์ง์๋ ์ค์ ๋ฐ์ดํฐ ๋์ "๋ฌด์์ด ๋ฐ๋์๋์ง"๋ง ๋ด๊ธด ๊ฐ๋ฒผ์ด ์ ํธ๋ฅผ ๋ณด๋ด๊ณ , ์ฑ์ด ๊ทธ ์ ํธ๋ฅผ ๋ฐ์ผ๋ฉด WorkManager๋ก ์ค์ ๋ฐ์ดํฐ๋ฅผ ๋ด๋ ค๋ฐ๋ ๊ตฌ์กฐ๊ฐ ์ผ๋ฐ์ ์ ๋๋ค. FCM ๋ฉ์์ง๋ ํ์ด๋ก๋ ํฌ๊ธฐ ์ ํ(๋ฐ์ดํฐ ๋ฉ์์ง ๊ธฐ์ค ์ต๋ 4KB)์ด ์๊ณ ์ ๋ฌ ์์ ๋ ๋ณด์ฅ๋์ง ์์ผ๋, ํธ์ ๋๋ฝ์ ๋๋นํด ์ฃผ๊ธฐ์ Pull๋ ํจ๊ป ๋๋ ๊ฒ์ด ์์ ํฉ๋๋ค.
๐ฆ ๋ณ๊ฒฝ๋ถ๋ง ๊ฐ์ ธ์ค๊ธฐ: ๋ธํ ๋๊ธฐํ
๋งค๋ฒ ์ ์ฒด ๋ชฉ๋ก์ ๋ด๋ ค๋ฐ๋ ๊ฑด ๋ฐ์ดํฐ๊ฐ ์ ์ ๋ ๊ด์ฐฎ์ง๋ง, ์์ฒ ๊ฑด์ด ๋์ด๊ฐ๋ฉด ๊ธ์ธ ๋ถ๋ด์ด ๋ฉ๋๋ค. ๊ทธ๋์ ๋ณ๊ฒฝ๋ถ(Delta)๋ง ์ฃผ๊ณ ๋ฐ๋ ๋๊ธฐํ๋ฅผ ์ค๊ณํฉ๋๋ค. ๋ํ์ ์ธ ๋ฐฉ๋ฒ์ ๋ ๊ฐ์ง์์.
"๋ง์ง๋ง ๋๊ธฐํ ์ดํ ๋ฐ๋ ๊ฒ๋ง ์ค"(?since=1718000000000). ๋จ์ํ์ง๋ง ์๋ฒยทํด๋ผ์ด์ธํธ ์๊ณ ์ฐจ์ด, ๊ฐ์ ๋ฐ๋ฆฌ์ด์ ์๊ธด ์ฌ๋ฌ ๋ณ๊ฒฝ ์ฒ๋ฆฌ์ ์ฃผ์ํด์ผ ํฉ๋๋ค. ์ปค์๋ ์๋ฒ ์๊ฐ ๊ธฐ์ค์ผ๋ก ๋ฐ์ ์ ์ฅํ๋ ํธ์ด ์์ ํฉ๋๋ค.
์๋ฒ๊ฐ ๋ชจ๋ ๋ณ๊ฒฝ์ ๋จ์กฐ ์ฆ๊ฐํ๋ ๋ฒ์ ๋ฒํธ๋ฅผ ๋ถ์ฌํ๊ณ , ์ฑ์ ๋ง์ง๋ง์ผ๋ก ๋ฐ์ ๋ฒ์ ์ดํ๋ฅผ ์์ฒญํฉ๋๋ค. Now in Android ์ํ๋ ์ํฐํฐ ์ข ๋ฅ๋ณ '๋ณ๊ฒฝ ๋ชฉ๋ก ๋ฒ์ '์ ๊ธฐ๊ธฐ์ ์ ์ฅํ๊ณ ๊ทธ ์ดํ ๋ณ๊ฒฝ๋ง ๋ฐ๋ ๋ฐฉ์์ ์ฌ์ฉํฉ๋๋ค.
๐ก ์ญ์ ๋ ๋ณ๊ฒฝ์ด๋ค
๋ธํ ๋๊ธฐํ์์ ๊ฐ์ฅ ์์ฃผ ๋น ๋จ๋ฆฌ๋ ๊ฒ์ด ์๋ฒ ์ธก ์ญ์ ์
๋๋ค. "๋ฐ๋ ๋ชฉ๋ก"๋ง ์ฃผ๋ฉด ์ญ์ ๋ ํญ๋ชฉ์ ์๋ต์ ์์ ๋ํ๋์ง ์์, ๊ธฐ๊ธฐ์ ์ ๋ น ๋ฐ์ดํฐ๊ฐ ๋จ์ต๋๋ค. ์๋ฒ๋ ์ญ์ ๋ฅผ ํฐ์คํค(์ญ์ ํ์)์ผ๋ก ๋จ๊ฒจ ๋ณ๊ฒฝ ๋ชฉ๋ก์ ํฌํจํด์ผ ํฉ๋๋ค.
suspend fun pullRemoteChanges() {
val since = syncPrefs.lastVersion() // DataStore์ ์ ์ฅ๋ ์ปค์
val changes = api.getTaskChanges(since) // { items, deletedIds, latestVersion }
db.withTransaction {
// ์์ง ์๋ฒ์ ์ ๋ณด๋ธ ๋ก์ปฌ ๋ณ๊ฒฝ์ ๋ฎ์ด์ฐ์ง ์๋๋ก ๋ณดํธ
val pendingIds = taskDao.getPendingTasks().map { it.id }.toSet()
taskDao.upsertAll(changes.items
.filter { it.id !in pendingIds }
.map { it.asEntity() })
changes.deletedIds.filter { it !in pendingIds }.forEach { taskDao.hardDelete(it) }
}
syncPrefs.saveLastVersion(changes.latestVersion)
}
์ ์ฝ๋์ pendingIds ํํฐ๊ฐ ๋ฐ๋ก ๋ค์ ์ฅ์์ ๋ค๋ฃฐ ์ถฉ๋์ ๊ฐ์ฅ ๋จ์ํ ๋ฐฉ์ด์ ์
๋๋ค. ์ฌ๊ธฐ์๋ ๋ก์ปฌ์ ๋๊ธฐ ์ค์ธ ๋ณ๊ฒฝ์ ๋ณดํธํ์ง๋ง, ์ด๊ฑด ํ๋์ ์ ์ฑ
์ผ ๋ฟ์ด์์. ์ด๋ค ์ ์ฑ
์ด ๋ง๋์ง๋ ๋ฐ์ดํฐ ์ฑ๊ฒฉ์ ๋ฐ๋ผ ๋ฌ๋ผ์ง๋๋ค.
โ๏ธ 8. ์ถฉ๋ ํด๊ฒฐ: ์คํ๋ผ์ธ ํผ์คํธ์ ์ต์ข ๋ณด์ค
ํด๋ํฐ๊ณผ ํ๋ธ๋ฆฟ์ผ๋ก ๊ฐ์ ๋ฉ๋ชจ๋ฅผ ๊ฐ๊ฐ ์คํ๋ผ์ธ์์ ์์ ํ๋ค๊ณ ์์ํด ๋ณด์ธ์. ๋ ๋ค ์ฐ๊ฒฐ์ด ๋์์ค๋ฉด ์๋ฒ๋ ๋๊ตฌ ๋ง์ ๋ค์ด์ผ ํ ๊น์? ์ด๊ฒ์ด ๋๊ธฐํ ์ถฉ๋์ ๋๋ค. ์คํ๋ผ์ธ ํผ์คํธ์ ๋์ด๋๋ ์ฌ์ค์ ์ด ๋ฌธ์ ๋ฅผ ์ผ๋ง๋ ์ ๊ตํ๊ฒ ๋ค๋ฃจ๋๋๋ก ๊ฒฐ์ ๋ผ์.
๐ง ์ถฉ๋ ํด๊ฒฐ ์ ๋ต ์ง๋
| ์ ๋ต | ๋ฐฉ์ | ์ฅ์ | ํ๊ณ |
|---|---|---|---|
| ๋ง์ง๋ง ์ฐ๊ธฐ ์น๋ฆฌ(LWW) | ํ์์คํฌํ๊ฐ ๊ฐ์ฅ ๋ฆ์ ์ฐ๊ธฐ๋ฅผ ์ฑํ | ๊ตฌํ์ด ๊ฐ์ฅ ์ฌ์ | ๋จผ์ ์ด ์ฌ์ฉ์์ ๋ณ๊ฒฝ์ด ์กฐ์ฉํ ์ฌ๋ผ์ง. ๊ธฐ๊ธฐ ์๊ณ๊ฐ ํ๋ฆฌ๋ฉด ์๋ฑํ ์ชฝ์ด ์ด๊น |
| ๋ฒ์ ๊ธฐ๋ฐ ๊ฑฐ๋ถ(๋๊ด์ ์ ๊ธ) | "v1์ ๊ธฐ๋ฐ์ผ๋ก ์์ "์ด๋ผ๊ณ ๋ณด๋ด๊ณ , ์๋ฒ ๋ฒ์ ์ด ๋ค๋ฅด๋ฉด ๊ฑฐ๋ถ(์: HTTP 409/412) | ๋ฐ์ดํฐ ์ ์ค์ ๋ง๊ณ ์ถฉ๋์ ๋ช ์์ ์ผ๋ก ๋๋ฌ๋ | ํด๋ผ์ด์ธํธ๊ฐ ์ถฉ๋ ์ฒ๋ฆฌ UIยท๋ก์ง์ ๊ฐ์ ธ์ผ ํจ |
| ํ๋ ๋จ์ ๋ณํฉ | ์๋ก ๋ค๋ฅธ ํ๋๋ฅผ ๋ฐ๊ฟจ๋ค๋ฉด ๋ ๋ค ๋ฐ์ | ์ถฉ๋ ๋น๋๋ฅผ ํฌ๊ฒ ์ค์ | ํ๋๋ณ ๋ณ๊ฒฝ ์ถ์ ํ์. ๊ฐ์ ํ๋ ์ถฉ๋์ ์ฌ์ ํ ๋จ์ |
| ์ฌ์ฉ์ ์ ํ | "๋ด ๋ฒ์ / ์๋ฒ ๋ฒ์ " ์ค ๊ณ ๋ฅด๊ฒ ํจ | ์ค์ํ ๋ฌธ์์ ๊ฐ์ฅ ์ ํ | ์ฌ์ฉ์์๊ฒ ๋ถ๋ด, ๋จ๋ฐํ๋ฉด ํผ๋ก๊ฐ |
| CRDT | ๋ณํฉ์ด ์ํ์ ์ผ๋ก ํญ์ ์๋ ดํ๋๋ก ์ค๊ณ๋ ์๋ฃ๊ตฌ์กฐ | ํ์ ํธ์ง์ ๊ฐ๋ ฅ, ์ค์ ํ์ ์์ด๋ ์๋ ด | ์ค๊ณยท์ ์ฅ ๋น์ฉ์ด ํฌ๊ณ ํ์ต ๊ณก์ ์ด ๊ฐํ๋ฆ |
๊ณต์ ๊ฐ์ด๋๋ ๋ง์ ์ฑ์์ ๋ง์ง๋ง ์ฐ๊ธฐ ์น๋ฆฌ(Last Write Wins)๋ฅผ ํฉ๋ฆฌ์ ์ธ ๊ธฐ๋ณธ๊ฐ์ผ๋ก ์๊ฐํ๋ฉด์, ์ด๋ฅผ ์ํด ๊ธฐ๊ธฐ ์ชฝ ๋ฐ์ดํฐ์ ์ฐ๊ธฐ ์๊ฐ ๊ฐ์ ๋ฉํ๋ฐ์ดํฐ๋ฅผ ํจ๊ป ์ ์ฅํ๋ผ๊ณ ์๋ดํฉ๋๋ค. ๋จ, LWW๋ฅผ ์ธ ๋๋ ๊ธฐ๊ธฐ ์๊ณ๋ ์ฌ์ฉ์๊ฐ ๋ฐ๊ฟ ์ ์๊ณ ํ๋ฆด ์ ์๋ค๋ ์ ์ ๊ธฐ์ตํ์ธ์. ํ์ ๊ธฐ์ค์ ์๋ฒ ์์ ์๊ฐ์ผ๋ก ๋๊ฑฐ๋, ์๋ฒ๊ฐ ๋ฐ๊ธํ ๋ฒ์ ๋ฒํธ๋ฅผ ํจ๊ป ์ฐ๋ ์์ผ๋ก ๋ณด์ํ๋ ๊ฒฝ์ฐ๊ฐ ๋ง์ต๋๋ค.
๐ก๏ธ ๋ฒ์ ๊ธฐ๋ฐ ์ถฉ๋ ์ฒ๋ฆฌ ์์
suspend fun pushLocalChanges() {
for (task in taskDao.getPendingTasks()) {
try {
when (task.syncState) {
SyncState.PENDING_CREATE -> {
val saved = api.createTask(task.asNetwork())
taskDao.upsert(task.copy(serverVersion = saved.version,
syncState = SyncState.SYNCED))
}
SyncState.PENDING_UPDATE -> {
val saved = api.updateTask(task.id, task.asNetwork(),
ifMatchVersion = task.serverVersion)
taskDao.upsert(task.copy(serverVersion = saved.version,
syncState = SyncState.SYNCED))
}
SyncState.PENDING_DELETE -> {
api.deleteTask(task.id)
taskDao.hardDelete(task.id)
}
SyncState.SYNCED -> Unit
}
} catch (e: HttpException) {
if (e.code() == 409) resolveConflict(task) else throw e
}
}
}
์ฌ๊ธฐ์ resolveConflict()๋ ์ฑ์ ์ ์ฑ
์ ๋ด๋ ์๋ฆฌ์
๋๋ค. ์๋ฒ ์ต์ ๋ณธ์ ๋ฐ์ ํ๋ ๋จ์๋ก ๋ณํฉํ ์๋, ์ฌ์ฉ์์๊ฒ ์ ํ์ ๋งก๊ธธ ์๋, ๋จ์ํ ์๋ฒ ์ชฝ์ ์ฑํํ ์๋ ์์ด์. ์ค์ํ ๊ฑด ์ถฉ๋์ด '์กฐ์ฉํ' ์ฒ๋ฆฌ๋์ง ์๋๋ก ์ ์ฑ
์ ๋ช
์์ ์ผ๋ก ์ ํ๋ ๊ฒ์
๋๋ค.
๐ ๋ฉฑ๋ฑ์ฑ: ๊ฐ์ ์์ฒญ์ด ๋ ๋ฒ ๊ฐ๋ ๊ด์ฐฎ๊ฒ
๋ชจ๋ฐ์ผ ๋คํธ์ํฌ์์๋ "์์ฒญ์ ์๋ฒ์ ๋์ฐฉํด ์ฒ๋ฆฌ๋๋๋ฐ, ์๋ต์ด ์ค๋ ๊ธธ์ ์ฐ๊ฒฐ์ด ๋๊ธฐ๋" ์ผ์ด ์ค์ ๋ก ์ผ์ด๋ฉ๋๋ค. ์ฑ์ ์คํจ๋ก ํ๋จํ๊ณ ์ฌ์๋ํ๊ฒ ์ฃ . ๊ทธ๋ผ ๊ฐ์ ํ ์ผ์ด ๋ ๊ฐ ์๊ธธ ์ ์์ต๋๋ค. ๐ฑ
โ
ํด๊ฒฐ์ฑ
: ๋ฉฑ๋ฑ(Idempotent) ์ค๊ณ
ยท ์์ฑ ์์ฒญ์ ํด๋ผ์ด์ธํธ ์์ฑ UUID๋ฅผ ๋ด์ ๋ณด๋ด๋ฉด, ์๋ฒ๋ ๊ฐ์ ID๊ฐ ์ด๋ฏธ ์์ ๋ ์๋ก ๋ง๋ค์ง ์๊ณ ๊ธฐ์กด ๊ฒฐ๊ณผ๋ฅผ ๋๋ ค์ค ์ ์์ต๋๋ค.
ยท ๊ฒฐ์ ์ฒ๋ผ ๋ฏผ๊ฐํ ์์ฒญ์ ๋ฉฑ๋ฑ์ฑ ํค(Idempotency-Key) ํค๋๋ฅผ ๋ถ์ฌ, ์๋ฒ๊ฐ ๊ฐ์ ํค์ ์ค๋ณต ์์ฒญ์ ํ ๋ฒ๋ง ์ฒ๋ฆฌํ๊ฒ ํฉ๋๋ค.
ยท PUT(์ ์ฒด ๊ต์ฒด)์ ๋ณธ์ง์ ์ผ๋ก ๋ฉฑ๋ฑ์ด์ง๋ง, "์๋ +1" ๊ฐ์ ์ฆ๋ถ ๋ช
๋ น์ ๋ฉฑ๋ฑ์ด ์๋๋ฏ๋ก ๋ช
๋ น ID๋ก ์ค๋ณต์ ๊ฑธ๋ฌ์ผ ํฉ๋๋ค.
๋ฉฑ๋ฑ์ฑ์ ์๋ฒ API ์ค๊ณ์ ๋ง๋ฌผ๋ ค ์์ด์, ์ฑ ๊ฐ๋ฐ์ ํผ์ ํด๊ฒฐํ ์ ์๋ ๊ฒฝ์ฐ๊ฐ ๋ง์ต๋๋ค. ์คํ๋ผ์ธ ํผ์คํธ๋ฅผ ๋์ ํ๊ธฐ ์ ์ ๋ฐฑ์๋ ํ๊ณผ ID ์์ฑ ์ฃผ์ฒด, ๋ฒ์ ๊ท์น, ์ญ์ ํํ ๋ฐฉ์, ์ถฉ๋ ์๋ต ์ฝ๋๋ฅผ ๋จผ์ ํฉ์ํด ๋๋ฉด ์ํ์ฐฉ์ค๊ฐ ํฌ๊ฒ ์ค์ด์.
๐ถ 9. ๋คํธ์ํฌ ์ํ ๊ฐ์ง: ์๋, ์์กดํ์ง ๋ง ๊ฒ
์ฌ์ฉ์์๊ฒ "์คํ๋ผ์ธ์
๋๋ค" ๋ฐฐ๋๋ฅผ ๋ณด์ฌ์ฃผ๋ ค๋ฉด ์ฐ๊ฒฐ ์ํ๋ฅผ ์์์ผ ํฉ๋๋ค. ์๋๋ก์ด๋์์๋ ConnectivityManager์ NetworkCallback์ผ๋ก ์ํ ๋ณํ๋ฅผ ๊ฐ์งํ ์ ์์ด์. ์์ ์ CONNECTIVITY_ACTION ๋ธ๋ก๋์บ์คํธ ๋ฐฉ์์ API 28์์ ์ง์ ์ค๋จ(deprecated)๋์์ผ๋ ์ฝ๋ฐฑ ๋ฐฉ์์ ์ฐ๋ ๊ฒ์ด ์ข์ต๋๋ค.
class ConnectivityNetworkMonitor @Inject constructor(
@ApplicationContext private val context: Context
) : NetworkMonitor {
override val isOnline: Flow<Boolean> = callbackFlow {
val cm = context.getSystemService(ConnectivityManager::class.java)
val callback = object : ConnectivityManager.NetworkCallback() {
private val networks = mutableSetOf<Network>()
override fun onAvailable(network: Network) {
networks += network; trySend(true)
}
override fun onLost(network: Network) {
networks -= network; trySend(networks.isNotEmpty())
}
}
val request = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build()
cm.registerNetworkCallback(request, callback)
trySend(cm.activeNetwork
?.let { cm.getNetworkCapabilities(it) }
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) == true)
awaitClose { cm.unregisterNetworkCallback(callback) }
}.conflate()
}
์ด ์ฝ๋๋ ACCESS_NETWORK_STATE ๊ถํ์ด ํ์ํฉ๋๋ค. ๊ทธ๋ฆฌ๊ณ ๊ผญ ๊ธฐ์ตํ ์ ์ด ์์ด์.
โ ๏ธ "์ฐ๊ฒฐ๋จ" โ "์ธํฐ๋ท ๋จ"
NET_CAPABILITY_INTERNET์ "์ธํฐ๋ท ์ฐ๊ฒฐ์ด ๊ฐ๋ฅํ๋๋ก ์ค์ ๋ ๋คํธ์ํฌ"๋ผ๋ ๋ป์ด์ง, ์ค์ ๋ก ์๋ฒ์ ๋ฟ๋๋ค๋ ๋ณด์ฅ์ ์๋๋๋ค. ๋ก๊ทธ์ธ์ด ํ์ํ ๊ณต์ฉ Wi-Fi(์บกํฐ๋ธ ํฌํธ)๋ ํผ์กํ ์
์์๋ ์ฐ๊ฒฐ์ ๋๋๋ฐ ์์ฒญ์ด ์คํจํฉ๋๋ค. ์์คํ
์ด ์ค์ ์ธํฐ๋ท ์ ๊ทผ์ ํ์ธํ ๋คํธ์ํฌ์ธ์ง ๋ณด๋ ค๋ฉด NET_CAPABILITY_VALIDATED๋ ์ฐธ๊ณ ํ ์ ์์ด์.
๊ฒฐ๋ก ์ ์ผ๋ก ๋คํธ์ํฌ ์ํ๋ UI ํํธ๋ก๋ง ์ฐ๊ณ , ์ค์ ๋๊ธฐํ ๋ก์ง์ '์์ฒญ์ ์๋ํ๊ณ ์คํจํ๋ฉด ์ฌ์๋'ํ๋ ๊ตฌ์กฐ๋ก ๋ง๋ค์ด์ผ ํฉ๋๋ค. ์์ ๋ณธ WorkManager์ ์ ์ฝ ์กฐ๊ฑด๊ณผ ๋ฐฑ์คํ๊ฐ ๋ฐ๋ก ๊ทธ ์ญํ ์ด์์.
๐งช 10. ํ ์คํธ: ๋นํ๊ธฐ ๋ชจ๋ ๋ฒํผ๋ง ๋๋ฅด๋ฉด ๋์ผ๊น?
์คํ๋ผ์ธ ํผ์คํธ ์ฑ์ ๋ฒ๊ทธ๋ ๋๋ถ๋ถ ํ์ด๋ฐ๊ณผ ์ํ ์ ์ด์์ ๋์ต๋๋ค. "์คํ๋ผ์ธ์์ ์์ฑ โ ์์ โ ์ญ์ โ ์จ๋ผ์ธ" ๊ฐ์ ์๋๋ฆฌ์ค๋ ์์ผ๋ก ์ฌํํ๊ธฐ ์ด๋ ต๊ธฐ ๋๋ฌธ์ ์๋ํ ํ ์คํธ์ ๊ฐ์น๊ฐ ํนํ ํฝ๋๋ค.
DAO์ API๋ฅผ ๊ฐ์ง(Fake) ๊ตฌํ์ผ๋ก ๋ฐ๊ฟ, "์ฐ๊ธฐ ํ syncState๊ฐ PENDING_UPDATE๊ฐ ๋๋๊ฐ", "์์ฑ ๋๊ธฐ ํญ๋ชฉ ์ญ์ ์ ์๋ฒ ํธ์ถ์ด ์๋๊ฐ"๋ฅผ ๊ฒ์ฆํฉ๋๋ค. Now in Android ์ํ๋ ํ ์คํธ์ฉ ๊ฐ์ง ๋ฐ์ดํฐ ์์ค๋ฅผ ์ ๊ทน ํ์ฉํฉ๋๋ค.
Room.inMemoryDatabaseBuilder()๋ก ์ค์ SQL ๋์์ ๊ฒ์ฆํฉ๋๋ค. Flow๊ฐ ๋ณ๊ฒฝ ์ ์ ๊ฐ์ ๋ด๋ณด๋ด๋์ง๋ Turbine ๊ฐ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก ๊น๋ํ๊ฒ ํ
์คํธํ ์ ์์ด์.
work-testing ์ํฐํฉํธ์ TestListenableWorkerBuilder๋ก ์์ปค๋ฅผ ์ง์ ์คํํ๊ณ , ์์ธ ์ํฉ์์ Result.retry()๋ฅผ ๋๋ ค์ฃผ๋์ง ํ์ธํฉ๋๋ค. WorkManagerTestInitHelper๋ก ์ ์ฝ ์กฐ๊ฑด ์ถฉ์กฑ์ ํ๋ด ๋ผ ์๋ ์์ต๋๋ค.
OkHttp์ MockWebServer๋ก ์ง์ฐ, ์ฐ๊ฒฐ ๋๊น, 409ยท500 ์๋ต์ ์ฌํํฉ๋๋ค. "์์ฒญ์ ์ฑ๊ณตํ์ง๋ง ์๋ต ์ ์ค" ์๋๋ฆฌ์ค๋ก ๋ฉฑ๋ฑ์ฑ๋ ๊ผญ ๊ฒ์ฆํ์ธ์.
@Test
fun ์คํ๋ผ์ธ_์์ ์_๋๊ธฐ์ํ๋ก_์ ์ฅ๋๋ค() = runTest {
val dao = FakeTaskDao(listOf(task(id = "1", state = SyncState.SYNCED)))
val repo = OfflineFirstTaskRepository(dao, FailingTaskApi(), FakeSyncScheduler())
repo.toggleDone("1", done = true)
val saved = dao.getById("1")!!
assertEquals(true, saved.done)
assertEquals(SyncState.PENDING_UPDATE, saved.syncState)
}
๐งณ ๋ง์ด๊ทธ๋ ์ด์ ํ ์คํธ๋ ์์ง ๋ง ๊ฒ
์คํ๋ผ์ธ ํผ์คํธ ์ฑ์ ๋ก์ปฌ DB์๋ ์์ง ์๋ฒ์ ๊ฐ์ง ์์ ์ฌ์ฉ์ ๋ฐ์ดํฐ๊ฐ ๋ค์ด ์์ต๋๋ค. ์ฑ ์
๋ฐ์ดํธ ์ค ์คํค๋ง ๋ง์ด๊ทธ๋ ์ด์
์ด ์คํจํด DB๋ฅผ ๋ ๋ฆฌ๋ฉด, ๊ทธ ๋ฐ์ดํฐ๋ ์์ํ ์ฌ๋ผ์ง๋๋ค. ๊ทธ๋์ fallbackToDestructiveMigration()์ ์คํ๋ผ์ธ ํผ์คํธ ์ฑ์์ ํนํ ์ ์คํ๊ฒ ์จ์ผ ํ๊ณ , Room์ MigrationTestHelper๋ก ๋ง์ด๊ทธ๋ ์ด์
์ ํ
์คํธํ๋ ์ต๊ด์ด ์ค์ํฉ๋๋ค. Room 2.4๋ถํฐ ์ ๊ณต๋๋ ์๋ ๋ง์ด๊ทธ๋ ์ด์
(AutoMigration)์ ์ปฌ๋ผ ์ถ๊ฐ ๊ฐ์ ๋จ์ ๋ณ๊ฒฝ์ ํธํ๊ฒ ์ฒ๋ฆฌํด ์ฃผ์ง๋ง, ํ
์ด๋ธยท์ปฌ๋ผ ์ด๋ฆ ๋ณ๊ฒฝ ๊ฐ์ ๋ชจํธํ ๊ฒฝ์ฐ์๋ ๋ช
์ธ(Spec)๋ฅผ ์ง์ ์ง์ ํด์ผ ํฉ๋๋ค.
๐จ 11. UX ๋์์ธ: ์ฌ์ฉ์๊ฐ '์์ฌ'ํ๊ฒ ๋ง๋๋ ์ ํธ๋ค
๊ธฐ์ ์ ์ผ๋ก ์๋ฒฝํ ๋๊ธฐํ๋, ์ฌ์ฉ์๊ฐ ์ดํดํ์ง ๋ชปํ๋ฉด ๋ถ์์ ์์ธ์ด ๋ฉ๋๋ค. "๋ด๊ฐ ์ด ๊ฑฐ ์ ์ฅ๋ ๊ฑฐ ๋ง์?"๋ผ๋ ์ง๋ฌธ์ ํ๋ฉด์ด ๋ตํด ์ค์ผ ํด์.
์ํ ๋ฐฐ๋๋ ์์ํ๊ฒ
์คํ๋ผ์ธ ์ํ๋ฅผ ๋นจ๊ฐ ๊ฒฝ๊ณ ์ฐฝ์ผ๋ก ๋์ฐ๋ฉด ์ฌ์ฉ์๋ '๊ณ ์ฅ'์ผ๋ก ๋ฐ์๋ค์
๋๋ค. ์๋จ์ ์์ ํ์ ๋ฐฐ๋๋ก "์คํ๋ผ์ธ ยท ๋ณ๊ฒฝ ์ฌํญ์ ์ฐ๊ฒฐ ์ ์๋ ์ ์ฅ๋ผ์" ์ ๋๋ฉด ์ถฉ๋ถํฉ๋๋ค.
ํญ๋ชฉ๋ณ ๋๊ธฐํ ํ์
์์ง ์๋ฒ์ ๊ฐ์ง ์์ ํญ๋ชฉ์ ์์ ์๊ณ ์์ด์ฝ์ด๋ "์ ์ก ๋๊ธฐ" ๋ผ๋ฒจ์ ๋ถ์ด๋ฉด, ์ฌ์ฉ์๋ ๋ฐ์ดํฐ๊ฐ ์ฌ๋ผ์ง์ง ์์๋ค๋ ๊ฑธ ์๋๋ค. ๋ฉ์ ์ ์ฑ์ '์ ์ก ์ค / ์ ์ก๋จ' ํ์๊ฐ ๋ํ์ ์ธ ์์ฃ .
๋ง์ง๋ง ๋๊ธฐํ ์๊ฐ
์ฌ๊ณ ยท๊ฐ๊ฒฉยท๋ฐฐ์ฐจ์ฒ๋ผ ์ต์ ์ฑ์ด ์ค์ํ ๋ฐ์ดํฐ๋ "3๋ถ ์ ์
๋ฐ์ดํธ" ๊ฐ์ ์ ๋ณด๋ฅผ ํจ๊ป ๋ณด์ฌ์ฃผ๋ฉด ์ ๋ขฐ๊ฐ ์ฌ๋ผ๊ฐ๋๋ค.
์จ๋ผ์ธ ์ ์ฉ ๊ธฐ๋ฅ์ ๋ฏธ๋ฆฌ ์๋ ค์ฃผ๊ธฐ
๊ฒฐ์ ๋ฒํผ์ ๋๋ฌ ๋๊ณ ์คํจ์ํค๋ ๊ฒ๋ณด๋ค, ์คํ๋ผ์ธ์ผ ๋ ๋ฒํผ์ ๋นํ์ฑํํ๊ณ "๊ฒฐ์ ๋ ์ฐ๊ฒฐ์ด ํ์ํด์"๋ผ๊ณ ์๋ดํ๋ ํธ์ด ์น์ ํฉ๋๋ค.
์คํจ๋ ์จ๊ธฐ์ง ๋ง๊ณ , ํด๊ฒฐ ๊ฒฝ๋ก๋ฅผ ์ฃผ๊ธฐ
์๊ตฌ ์คํจ(์๋ฒ ๊ฒ์ฆ ์ค๋ฅ ๋ฑ) ํญ๋ชฉ์ "๋ค์ ์๋ / ์์ / ์ญ์ " ์ ํ์ง๋ฅผ ์ค์ผ ํฉ๋๋ค. ์กฐ์ฉํ ๋ฌดํ ์ฌ์๋ํ๋ ๊ฒ์ด ์ต์
์
๋๋ค.
"์คํ๋ผ์ธ ํผ์คํธ์ ๋ชฉํ๋ ์ฌ์ฉ์๊ฐ ๋คํธ์ํฌ์ ์กด์ฌ๋ฅผ ์๊ฒ ๋ง๋๋ ๊ฒ์ด๋ค. ๋จ, ๋คํธ์ํฌ๊ฐ ์ ๋ง ํ์ํ ์๊ฐ์๋ ์ ์งํ๊ฒ ์๋ ค์ฃผ๋ฉด์."
๐ 12. ๋ณด์ยท์ ์ฅ ๊ณต๊ฐยท๋ฐฐํฐ๋ฆฌ: ์จ์ ์ด์ ์ด์
๐ ๊ธฐ๊ธฐ์ ๋จ๋ ๋ฐ์ดํฐ์ ๋ณด์
์คํ๋ผ์ธ ํผ์คํธ๋ ์ ์์ ๋ ๋ง์ ๋ฐ์ดํฐ๋ฅผ ๋ ์ค๋ ๊ธฐ๊ธฐ์ ์ ์ฅํฉ๋๋ค. ๊ธฐ๋ณธ์ ์ผ๋ก ์ฑ ์ ์ฉ ๋ด๋ถ ์ ์ฅ์์ ๋ฐ์ดํฐ๋ ๋ค๋ฅธ ์ฑ์ด ์ ๊ทผํ ์ ์๋๋ก ์๋๋ฐ์ค๋ก ๋ณดํธ๋๊ณ , ์ต์ ์๋๋ก์ด๋ ๊ธฐ๊ธฐ๋ ์ ์ฅ์ ์ํธํ๋ฅผ ์ฌ์ฉํฉ๋๋ค. ๊ทธ๋๋ ์๋ฃยท๊ธ์ต์ฒ๋ผ ๋ฏผ๊ฐํ ๋ฐ์ดํฐ๋ผ๋ฉด ์ถ๊ฐ ์กฐ์น๋ฅผ ๊ฒํ ํ์ธ์.
SQLCipher์ฒ๋ผ SQLite ํ์ผ ์์ฒด๋ฅผ ์ํธํํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ Room๊ณผ ํจ๊ป ์ธ ์ ์์ต๋๋ค. ํค๋ Android Keystore๋ก ๋ณดํธํ๋ ๊ฒ์ด ์ผ๋ฐ์ ์ ๋๋ค.
๊ณ์ ์ ํ ์ ์ด์ ์ฌ์ฉ์์ ๋ก์ปฌ ๋ฐ์ดํฐยท๋๊ธฐ ํ๊ฐ ๋จ์ง ์๊ฒ ์ ๋ฆฌํด์ผ ํฉ๋๋ค. ๋จ, ๋ฏธ์ ์ก ๋ฐ์ดํฐ๊ฐ ์๋ค๋ฉด ๊ฒฝ๊ณ ํ ์ฒ๋ฆฌํด์ผ ๋ฐ์ดํฐ ์ ์ค์ ๋ง์ ์ ์์ด์.
Android ์๋ ๋ฐฑ์ ๋์์ ํ ํฐ์ด๋ ๋ฏผ๊ฐํ DB๊ฐ ํฌํจ๋์ง ์๋๋ก ๋ฐฑ์ ๊ท์น ํ์ผ๋ก ๋ฒ์๋ฅผ ์ง์ ํ์ธ์.
๐พ ์ ์ฅ ๊ณต๊ฐ ๊ด๋ฆฌ
๋ก์ปฌ DB๊ฐ ๋์์ด ์ปค์ง๋ฉด ๊ฒฐ๊ตญ ๋ฌธ์ ๊ฐ ๋ฉ๋๋ค. ์ค๋๋ ํผ๋ ๋ฐ์ดํฐ๋ ์ผ์ ๊ธฐ๊ฐยท๊ฐ์ ๊ธฐ์ค์ผ๋ก ์ ๋ฆฌํ๊ณ , ์ด๋ฏธ์ง ์บ์๋ ์ฉ๋ ์ํ์ ๋๋ฉฐ, ๋๊ธฐํ๊ฐ ๋๋ ์์๋ฐ์ค ํ์ ์ญ์ ํฉ๋๋ค. ์ ๋ฆฌ ์์
๋ WorkManager์ ์ฃผ๊ธฐ ์์
์ผ๋ก ๊ธฐ๊ธฐ๊ฐ ์ถฉ์ ์ค์ผ ๋(setRequiresCharging(true)) ๋๋ฆฌ๋ฉด ์ฌ์ฉ์ ๊ฒฝํ์ ํด์น์ง ์์ต๋๋ค.
๐ ๋ฐฐํฐ๋ฆฌ๋ฅผ ์๋ผ๋ ๋๊ธฐํ
์๋๋ก์ด๋๋ ๋์ฆ(Doze) ๋ชจ๋์ ์ฑ ๋๊ธฐ ๋ฒํท(App Standby Buckets) ๊ฐ์ ์ฅ์น๋ก ๋ฐฑ๊ทธ๋ผ์ด๋ ์์ ์ ์ ํํฉ๋๋ค. WorkManager๋ ์ด๋ฐ ์์คํ ์ ์ฑ ์ ์กด์คํ๋ฉด์ ์์ ์ ์คํํ๋๋ก ์ค๊ณ๋์ด ์์ผ๋ฏ๋ก, ์ง์ ์๋์ด๋ ์๋น์ค๋ฅผ ๋๋ฆฌ๊ธฐ๋ณด๋ค WorkManager์ ๋งก๊ธฐ๋ ๊ฒ์ด ๋ฐฐํฐ๋ฆฌ์ ์ ์ฑ ์ค์ ๋ฉด์์ ์ ๋ฆฌํฉ๋๋ค. ๋ ๋ณ๊ฒฝ์ ํ๋ํ๋ ์ฆ์ ๋ณด๋ด๊ธฐ๋ณด๋ค ๋ฌถ์ด์(batch) ๋ณด๋ด๋ ๊ฒ์ด ๋ฌด์ ๋ชจ๋์ด ๊นจ์ด๋๋ ํ์๋ฅผ ์ค์ฌ ๋ฐฐํฐ๋ฆฌ์ ์ด๋กญ์ต๋๋ค.
๐งฐ 13. ์ค์ ์๋๋ฆฌ์ค ์ธ ๊ฐ์ง
์ด๋ก ์ ํ์ฅ์ ๋์ ํด ๋ณผ๊ฒ์. ๊ฐ์ ์คํ๋ผ์ธ ํผ์คํธ๋ผ๋ ์ฑ์ ์ฑ๊ฒฉ์ ๋ฐ๋ผ ์ค๊ณ ํฌ์ธํธ๊ฐ ๋ฌ๋ผ์ง๋๋ค.
์๋๋ฆฌ์ค A
๐ ๊ฐ์ธ ๋ฉ๋ชจยทํ ์ผ ์ฑ
ยท ์ฐ๊ธฐ ์ ๋ต: ๊ฑฐ์ ์ ๋ถ ์ง์ฐ ์ฐ๊ธฐ
ยท ์ถฉ๋: ํ ์ฌ๋์ด ์ฌ๋ฌ ๊ธฐ๊ธฐ๋ฅผ ์ฐ๋ ์ ๋๋ผ ๋น๋ ๋ฎ์ โ LWW + ํ๋ ๋จ์ ๋ณํฉ์ด๋ฉด ๋๋ถ๋ถ ์ถฉ๋ถ
ยท ๋๊ธฐํ: ์ฑ ์์ยทํฌ๊ทธ๋ผ์ด๋ ์ง์
์ Pull + ๋ณ๊ฒฝ ์ ์ฆ์ Push ์์ฝ
ยท ํฌ์ธํธ: ํด๋ผ์ด์ธํธ UUID, ์ํํธ ์ญ์ , ๋๊ธฐํ ๋ฐฐ์ง
์๋๋ฆฌ์ค B
๐ ๋ฐฐ์กยทํ์ฅ ์ ๊ฒ ์
๋ฌด ์ฑ
ยท ์ฐ๊ธฐ ์ ๋ต: ์ ๊ฒ ๊ฒฐ๊ณผยท์๋ช
ยท์ฌ์ง์ ์ง์ฐ ์ฐ๊ธฐ, ๋ฐฐ์ฐจ ํ์ ์ ์จ๋ผ์ธ ์ ์ฉ
ยท ๋๊ธฐํ: ๊ทผ๋ฌด ์์ ์ ๋น์ผ ์
๋ฌด ๋ฐ์ดํฐ ์ผ๊ด ๋ค์ด๋ก๋(ํ๋ฆฌํ์น), ์ฌ์ง์ Wi-Fi ๋๋ ์ถฉ์ ์ ์
๋ก๋ ๊ณ ๋ ค
ยท ์ถฉ๋: ๊ด๋ฆฌ์์ ๊ธฐ์ฌ๊ฐ ๊ฐ์ ๊ฑด์ ์์ ํ ์ ์์ โ ๋ฒ์ ๊ธฐ๋ฐ ๊ฑฐ๋ถ + ์๋ฒ ์ฐ์ ์ ์ฑ
๋ฑ ์
๋ฌด ๊ท์น์ ๋ช
ํํ
ยท ํฌ์ธํธ: ๋์ฉ๋ ์ฒจ๋ถ ํ์ผ ํ, ์
๋ก๋ ์งํ๋ฅ ํ์, ๋ฏธ์ ์ก ๋ฐ์ดํฐ ๋ก๊ทธ์์ ์ฐจ๋จ
์๋๋ฆฌ์ค C
๐ฅ ์ค์๊ฐ ํ์
๋ฌธ์ยท๋ณด๋
ยท ์ฐ๊ธฐ ์ ๋ต: ํธ์ง ์ฐ์ฐ์ ํ ๊ธฐ๋ฐ์ผ๋ก ์์๋๋ก ์ ์ก
ยท ๋๊ธฐํ: ์น์์ผ ๋ฑ ์ค์๊ฐ ์ฑ๋ + ์ฌ์ฐ๊ฒฐ ์ ๋๋ฝ๋ถ ๋ธํ ๋๊ธฐํ
ยท ์ถฉ๋: ๋์ ํธ์ง์ด ์ผ์ โ CRDT ๋๋ ์ด์ ๋ณํ(OT) ๊ณ์ด ์๊ณ ๋ฆฌ์ฆ ๊ฒํ
ยท ํฌ์ธํธ: ๋ณํฉ ๋ก์ง ๊ฒ์ฆ์ ๋ง๋ํ ํ
์คํธ ๋น์ฉ, ๊ธฐ์ฑ ๋๊ธฐํ ์๋ฃจ์
๋์
๋ ํ์ค์ ์ ํ์ง
๐ก ์ง์ ๋ง๋ค๊น, ์๋ฃจ์
์ ์ธ๊น?
Firebase Cloud Firestore๋ ์๋๋ก์ด๋ SDK์์ ์คํ๋ผ์ธ ์ง์์ฑ(๋ก์ปฌ ์บ์)์ ๊ธฐ๋ณธ ํ์ฑํํด ์ ๊ณตํ๊ณ , ์คํ๋ผ์ธ์์ ์ด ๋ด์ฉ์ ์ฐ๊ฒฐ ๋ณต๊ตฌ ์ ์๋ฒ์ ๋ฐ์ํฉ๋๋ค. ์ด ์ธ์๋ ๋๊ธฐํ ๊ธฐ๋ฅ์ ๋ด์ฅํ ์ฌ๋ฌ ๋ฐฑ์๋ยทDB ์๋ฃจ์
์ด ์์ด์. ๋ค๋ง ์ด๋ฐ ๋๊ตฌ๋ ์ถฉ๋ ์ฒ๋ฆฌ ์ ์ฑ
, ์ฟผ๋ฆฌ ์ ์ฝ, ๋น์ฉ ๊ตฌ์กฐ๊ฐ ๊ฐ์ ์ ํด์ ธ ์์ผ๋ ์๊ตฌ์ฌํญ๊ณผ ๋ง๋์ง ๊ผผ๊ผผํ ๋น๊ตํด์ผ ํฉ๋๋ค. ์์ฒด ์๋ฒ์ ๋ณต์กํ ์
๋ฌด ๊ท์น์ด ์๋ค๋ฉด ์ด ๊ธ์ฒ๋ผ Room + WorkManager๋ก ์ง์ ๊ตฌ์ฑํ๋ ๋ฐฉ์์ด ์ ์ฐํฉ๋๋ค.
๐งญ 14. ๋์ ๋ก๋๋งต: ๊ธฐ์กด ์ฑ์ ์คํ๋ผ์ธ ํผ์คํธ๋ก ๋ฐ๊พธ๋ ค๋ฉด
์ฒ์๋ถํฐ ์๋ก ๋ง๋๋ ์ฑ์ด ์๋๋ผ๋ฉด, ํ ๋ฒ์ ๋ชจ๋ ๊ธฐ๋ฅ์ ๋ฐ๊พธ๊ธฐ๋ณด๋ค ๋จ๊ณ์ ์ผ๋ก ์ ํํ๋ ํธ์ด ์์ ํฉ๋๋ค.
ํต์ฌ ๊ธฐ๋ฅ ์ ์ โ "์คํ๋ผ์ธ์์ ์ด๊ฒ๋งํผ์ ๋ผ์ผ ํ๋ค"๋ฅผ ๋จผ์ ์ ํฉ๋๋ค. ๋ชจ๋ ๊ธฐ๋ฅ์ ์คํ๋ผ์ธํํ ํ์๋ ์์ด์.
์ฝ๊ธฐ๋ถํฐ ์ ํ โ Repository๊ฐ API ์๋ต ๋์ Room Flow๋ฅผ ๋ ธ์ถํ๋๋ก ๋ฐ๊ฟ๋๋ค. ์ด ๋จ๊ณ๋ง์ผ๋ก๋ ์ฒด๊ฐ ์๋์ ์คํ๋ผ์ธ ์ด๋์ด ํฌ๊ฒ ๊ฐ์ ๋ฉ๋๋ค.
๋ฐฑ์๋ ๊ณ์ฝ ํฉ์ โ ID ์์ฑ, ๋ฒ์ ํ๋, ๋ธํ API, ์ญ์ ํํ, ์ถฉ๋ ์๋ต ๊ท์น์ ์ ํฉ๋๋ค.
์ฐ๊ธฐ ์ ํ โ ๊ธฐ๋ฅ๋ณ๋ก ์จ๋ผ์ธ ์ ์ฉ/ํ/์ง์ฐ ์ฐ๊ธฐ๋ฅผ ์ ํ๊ณ , ๋๊ธฐํ ์ํ ์ปฌ๋ผ์ด๋ ์์๋ฐ์ค๋ฅผ ๋์ ํฉ๋๋ค.
๋๊ธฐํ ์์ปค์ ๊ด์ธก์ฑ โ WorkManager ์์ปค๋ฅผ ๋ถ์ด๊ณ , ๋๊ธฐํ ์คํจ์จยท๋๊ธฐ ํ ๊ธธ์ดยท์ถฉ๋ ๋ฐ์ ์ ๊ฐ์ ์งํ๋ฅผ ์์งํฉ๋๋ค.
UX ๋ค๋ฌ๊ธฐ โ ์คํ๋ผ์ธ ๋ฐฐ๋, ๋๊ธฐ ๋ฐฐ์ง, ์คํจ ๋ณต๊ตฌ ํ๋ฆ์ ์์ฑํฉ๋๋ค.
์ด ๊ณผ์ ์ ์ฑ ์ค๊ณ, ๋ฐฑ์๋ API, UX ๋์์ธ์ด ํ๊บผ๋ฒ์ ์ฝํ๋ ์์ ์ด๋ผ ํ ๊ตฌ์ฑ์ด ์๋ค๋ฉด ๋ถ๋ด์ด ๋ ์ ์์ด์. ์ด๋ด ๋ ์ฌ๋ฅ๋ท ๊ฐ์ ์ฌ๋ฅ ๊ณต์ ํ๋ซํผ์์ ์๋๋ก์ด๋ ์ํคํ ์ฒ ๊ฒฝํ์ด ์๋ ๊ฐ๋ฐ์๋ ๋ชจ๋ฐ์ผ UX ๋์์ด๋์ ๋์์ ๋ฐ์, ์ค๊ณ ๋ฆฌ๋ทฐ๋ ํน์ ๋จ๊ณ๋ง ๋งก๊ธฐ๋ ๋ฐฉ์๋ ํ์ค์ ์ธ ์ ํ์ง๊ฐ ๋ฉ๋๋ค.
โ 15. ์ถ์ ์ ์ ํ ์ฒดํฌ๋ฆฌ์คํธ
๐งพ ํ๋์ ์ ๋ฆฌ
์์น โ ๋ก์ปฌ DB๊ฐ ๋จ์ผ ์ง์ค ๊ณต๊ธ์, ๋คํธ์ํฌ๋ ๋ก์ปฌ์ ์ต์ ํํ๋ ์ญํ
์ฝ๊ธฐ โ Room DAO์ Flow โ Repository โ ViewModel StateFlow โ UI, ๋์ฉ๋ ๋ชฉ๋ก์ Paging 3 RemoteMediator
์ฐ๊ธฐ โ ์จ๋ผ์ธ ์ ์ฉ / ํ ๊ธฐ๋ฐ / ์ง์ฐ ์ฐ๊ธฐ๋ฅผ ๊ธฐ๋ฅ๋ณ๋ก ์ ํ, ๋๊ธฐํ ์ํ ์ปฌ๋ผ ๋๋ ์์๋ฐ์ค
๋๊ธฐํ โ WorkManager(์ ์ฝ ์กฐ๊ฑดยท๋ฐฑ์คํยท๊ณ ์ ์์
), PullยทPush ํ์ด๋ธ๋ฆฌ๋, ๋ธํ ๋๊ธฐํ์ ์ญ์ ํฐ์คํค
์ถฉ๋ โ LWW๋ ์ฌ์ด ๊ธฐ๋ณธ๊ฐ, ์ค์ ๋ฐ์ดํฐ๋ ๋ฒ์ ๊ธฐ๋ฐ ๊ฑฐ๋ถยท๋ณํฉยท์ฌ์ฉ์ ์ ํ, ํ์
์ CRDT ๊ฒํ
ํ์ง โ ๋ฉฑ๋ฑ์ฑ, ๋คํธ์ํฌ ์ํ๋ UI ํํธ๋ก๋ง, ๋ง์ด๊ทธ๋ ์ด์
ํ
์คํธ, ์ ์งํ ๋๊ธฐํ UX
์คํ๋ผ์ธ ํผ์คํธ๋ ๋จ์ํ '์บ์ ๊ธฐ๋ฅ ์ถ๊ฐ'๊ฐ ์๋๋ผ, ๋ฐ์ดํฐ๊ฐ ์ด๋์ ์ด๊ณ ์ด๋ป๊ฒ ํ๋ฌ๊ฐ๋์ง๋ฅผ ๋ค์ ์ค๊ณํ๋ ์ผ์ ๋๋ค. ์ฒ์์ ๋ฉํ๋ฐ์ดํฐ ์ปฌ๋ผ ํ๋, ์ํ ์ ์ด ๊ท์น ํ๋๊ฐ ๊ท์ฐฎ๊ฒ ๋๊ปด์ง์ง๋ง, ๊ทธ ๋ํ ์ผ์ด ์์ด๋ฉด ์งํ์ฒ ํฐ๋ ์์์๋, ์ฐ๊ผญ๋๊ธฐ์์๋ ๋ฌต๋ฌตํ ์ผํ๋ ์ฑ์ด ์์ฑ๋ฉ๋๋ค. ์ฌ์ฉ์๋ ๊ทธ ๋ ธ๋ ฅ์ ๋์น์ฑ์ง ๋ชปํ๊ฒ ์ง๋ง, ๋ฐ๋ก ๊ทธ๊ฒ ์ฑ๊ณต์ ์ฆ๊ฑฐ์์. ๐โจ
๐ ์ฐธ๊ณ ์๋ฃ
ยท Android Developers โ Build an offline-first app: https://developer.android.com/topic/architecture/data-layer/offline-first
ยท Android Developers โ Guide to app architecture: https://developer.android.com/topic/architecture
ยท Android Developers โ Save data in a local database using Room: https://developer.android.com/training/data-storage/room
ยท Android Developers โ WorkManager (Schedule tasks): https://developer.android.com/develop/background-work/background-tasks/persistent
ยท Android Developers โ Page from network and database (Paging 3): https://developer.android.com/topic/libraries/architecture/paging/v3-network-db
ยท Android Developers โ Monitor connectivity status: https://developer.android.com/training/basics/network-ops/reading-network-state
ยท GitHub โ Now in Android ์ํ ์ฑ: https://github.com/android/nowinandroid
ยท Firebase โ Access data offline (Cloud Firestore): https://firebase.google.com/docs/firestore/manage-data/enable-offline
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.