์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿ“ถ ํ„ฐ๋„ ์†์—์„œ๋„ ๋ฉ€์ฉกํ•œ ์•ฑ ๋งŒ๋“ค๊ธฐ: ์•ˆ๋“œ๋กœ์ด๋“œ ์˜คํ”„๋ผ์ธ ํผ์ŠคํŠธ ์•ฑ ๊ฐœ๋ฐœ ์™„์ „ ์ •๋ณต ๊ฐ€์ด๋“œ ๐Ÿš‡

๐Ÿ“ถ ํ„ฐ๋„ ์†์—์„œ๋„ ๋ฉ€์ฉกํ•œ ์•ฑ ๋งŒ๋“ค๊ธฐ: ์•ˆ๋“œ๋กœ์ด๋“œ ์˜คํ”„๋ผ์ธ ํผ์ŠคํŠธ ์•ฑ ๊ฐœ๋ฐœ ์™„์ „ ์ •๋ณต ๊ฐ€์ด๋“œ ๐Ÿš‡

์ง€ํ•˜์ฒ ์ด ํ•œ๊ฐ• ๋‹ค๋ฆฌ๋ฅผ ๊ฑด๋„ˆ๋Š” ์ˆœ๊ฐ„, ์—˜๋ฆฌ๋ฒ ์ดํ„ฐ ๋ฌธ์ด ๋‹ซํžˆ๋Š” ์ˆœ๊ฐ„, ๋“ฑ์‚ฐ๋กœ ์ •์ƒ ์ง์ „ '์„œ๋น„์Šค ์•ˆ ๋จ' ํ‘œ์‹œ๊ฐ€ ๋œจ๋Š” ์ˆœ๊ฐ„โ€ฆ
๊ทธ๋•Œ ์•ฑ์ด ํ•˜์–€ ํ™”๋ฉด์— ๋ฑ…๊ธ€๋ฑ…๊ธ€ ๋กœ๋”ฉ ์Šคํ”ผ๋„ˆ๋งŒ ๋Œ๋ฆฌ๊ณ  ์žˆ๋‹ค๋ฉด? ์‚ฌ์šฉ์ž๋Š” ์กฐ์šฉํžˆ ์•ฑ์„ ๋‹ซ๊ณ , ๋‹ค์‹œ๋Š” ์—ด์ง€ ์•Š์„์ง€๋„ ๋ชฐ๋ผ์š”. ๐Ÿ˜‡

์˜คํ”„๋ผ์ธ ํผ์ŠคํŠธ(Offline-first)๋Š” "๋„คํŠธ์›Œํฌ๋Š” ์–ธ์ œ๋“  ๋Š๊ธธ ์ˆ˜ ์žˆ๋‹ค"๋ฅผ ๊ธฐ๋ณธ ์ „์ œ๋กœ ๋‘๊ณ , ๊ธฐ๊ธฐ ์•ˆ์˜ ๋กœ์ปฌ ๋ฐ์ดํ„ฐ๋ฅผ ์•ฑ์˜ ๊ธฐ์ค€(Source of Truth)์œผ๋กœ ์‚ผ๋Š” ์„ค๊ณ„ ์ฒ ํ•™์ž…๋‹ˆ๋‹ค. ์ด ๊ธ€์—์„œ๋Š” ์•ˆ๋“œ๋กœ์ด๋“œ ๊ณต์‹ ์•„ํ‚คํ…์ฒ˜ ๊ฐ€์ด๋“œ๊ฐ€ ๊ถŒ์žฅํ•˜๋Š” ๋ฐฉ์‹์„ ๋ผˆ๋Œ€๋กœ, RoomยทFlowยทWorkManager๋ฅผ ํ™œ์šฉํ•œ ์‹ค์ „ ๊ตฌํ˜„, ๋™๊ธฐํ™” ์ „๋žต, ์ถฉ๋Œ ํ•ด๊ฒฐ, ํ…Œ์ŠคํŠธ๊นŒ์ง€ ํ•œ ๋ฒˆ์— ์ •๋ฆฌํ•ด ๋ณผ๊ฒŒ์š”.

์˜คํ”„๋ผ์ธ ํผ์ŠคํŠธ์˜ ํ•ต์‹ฌ ๋„คํŠธ์›Œํฌ๊ฐ€ ๋Š๊ฒจ๋„ ์•ฑ์€ ๋กœ์ปฌ DB๋กœ ๊ณ„์† ์ผํ•œ๋‹ค ์„œ๋ฒ„ ํ•  ์ผ ๋ชฉ๋ก ์ •์ƒ ์ž‘๋™ ์ค‘ Room ๋กœ์ปฌ DB ๋Œ€๊ธฐ ์ค‘์ธ ๋ณ€๊ฒฝ ์‚ฌํ•ญ โœ” ์ฝ๊ธฐ: ๋กœ์ปฌ์—์„œ ์ฆ‰์‹œ โณ ์“ฐ๊ธฐ: ํ์— ์Œ“์•„ ๋‘๊ธฐ ๐Ÿ”„ ์—ฐ๊ฒฐ ๋ณต๊ตฌ ์‹œ ๋™๊ธฐํ™”

๐Ÿงญ 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)์—๋Š” "๋กœ์ปฌ ๊ธฐ์ค€์˜ ๋ฐ์ดํ„ฐ ์ŠคํŠธ๋ฆผ"๋งŒ ๋…ธ์ถœํ•˜๋Š” ๊ตฌ์กฐ์ฃ .

์˜คํ”„๋ผ์ธ ํผ์ŠคํŠธ ๋ฐ์ดํ„ฐ ํ๋ฆ„ UI (Compose ํ™”๋ฉด) ViewModel ยท StateFlow Repository ๋กœ์ปฌ์„ ๋…ธ์ถœ, ์›๊ฒฉ์€ ๋™๊ธฐํ™”์—๋งŒ ๋กœ์ปฌ: Room DB ๋‹จ์ผ ์ง„์‹ค ๊ณต๊ธ‰์› ์›๊ฒฉ: Retrofit API ๋Š๊ธธ ์ˆ˜ ์žˆ์Œ WorkManager ๋™๊ธฐํ™” Flow ๊ด€์ฐฐ ์ €์žฅ ์š”์ฒญ

๐Ÿ“Œ ํ•ต์‹ฌ ์›์น™ ์„ธ ๊ฐ€์ง€

1

๋กœ์ปฌ ๋ฐ์ดํ„ฐ ์†Œ์Šค๊ฐ€ ๋‹จ์ผ ์ง„์‹ค ๊ณต๊ธ‰์›(SSOT)
์•ฑ ์ƒ์œ„ ๋ ˆ์ด์–ด๊ฐ€ ์ฝ๋Š” ๋ฐ์ดํ„ฐ๋Š” ์˜ค์ง ๋กœ์ปฌ DB์—์„œ ๋‚˜์˜ต๋‹ˆ๋‹ค. ๋„คํŠธ์›Œํฌ ์‘๋‹ต์„ ViewModel์— ์ง์ ‘ ๋„˜๊ธฐ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

2

์ฝ๊ธฐ๋Š” ๊ด€์ฐฐ ๊ฐ€๋Šฅํ•œ ์ŠคํŠธ๋ฆผ์œผ๋กœ
Room DAO๊ฐ€ Flow๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉด, ๋™๊ธฐํ™” ์ž‘์—…์ด DB๋ฅผ ๊ฐฑ์‹ ํ•˜๋Š” ์ฆ‰์‹œ ํ™”๋ฉด๋„ ์ž๋™์œผ๋กœ ๋ฐ”๋€๋‹ˆ๋‹ค. "์ƒˆ๋กœ๊ณ ์นจ ๋ฒ„ํŠผ ๋ˆ„๋ฅด๋ฉด ๋‹ค์‹œ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ" ๊ฐ™์€ ์ˆ˜๋™ ํ๋ฆ„์ด ์‚ฌ๋ผ์ง‘๋‹ˆ๋‹ค.

3

๋„คํŠธ์›Œํฌ ์ž‘์—…์€ ์•ฑ ์ƒ๋ช…์ฃผ๊ธฐ์™€ ๋ถ„๋ฆฌ
์‚ฌ์šฉ์ž๊ฐ€ ํ™”๋ฉด์„ ๋‹ซ๊ฑฐ๋‚˜ ์•ฑ์ด ์ข…๋ฃŒ๋ผ๋„ ๋™๊ธฐํ™”๋Š” ์ด์–ด์ ธ์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด ์—ญํ• ์€ WorkManager๊ฐ€ ๋งก์Šต๋‹ˆ๋‹ค.

๐Ÿ—‚๏ธ ๋ชจ๋ธ์„ ์„ธ ๊ฒน์œผ๋กœ ๋‚˜๋ˆ„๋Š” ์ด์œ 

์‹ค๋ฌด์—์„œ๋Š” ๊ฐ™์€ 'ํ•  ์ผ' ๋ฐ์ดํ„ฐ๋ผ๋„ ๋ชจ๋ธ์„ ๋ ˆ์ด์–ด๋ณ„๋กœ ๋ถ„๋ฆฌํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์Šต๋‹ˆ๋‹ค. ๊ตฌ๊ธ€์˜ ๊ณต์‹ ์ƒ˜ํ”Œ ์•ฑ Now in Android๋„ ๋„คํŠธ์›Œํฌ ๋ชจ๋ธ, ์—”ํ‹ฐํ‹ฐ(DB) ๋ชจ๋ธ, ์™ธ๋ถ€ ๋…ธ์ถœ์šฉ ๋ชจ๋ธ์„ ๋ถ„๋ฆฌํ•˜๋Š” ๊ตฌ์กฐ๋ฅผ ๋ณด์—ฌ ์ค๋‹ˆ๋‹ค.

๋ชจ๋ธ์˜ˆ์‹œ ์ด๋ฆ„์—ญํ• 
๋„คํŠธ์›Œํฌ ๋ชจ๋ธNetworkTask์„œ๋ฒ„ JSON ๊ตฌ์กฐ ๊ทธ๋Œ€๋กœ. ์„œ๋ฒ„ ์ŠคํŽ™ ๋ณ€๊ฒฝ์˜ ์ถฉ๊ฒฉ์„ ์—ฌ๊ธฐ์„œ ํก์ˆ˜
์—”ํ‹ฐํ‹ฐ ๋ชจ๋ธTaskEntityRoom ํ…Œ์ด๋ธ” ๊ตฌ์กฐ. ๋™๊ธฐํ™” ์ƒํƒœยท๋กœ์ปฌ ์ˆ˜์ • ์‹œ๊ฐ ๋“ฑ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํฌํ•จ
์™ธ๋ถ€ ๋ชจ๋ธTaskUIยท๋„๋ฉ”์ธ์ด ์“ฐ๋Š” ๊น”๋”ํ•œ ๋ชจ๋ธ. ๋™๊ธฐํ™” ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋Š” ์ˆจ๊น€

์ด๋ ‡๊ฒŒ ๋‚˜๋ˆ„๋ฉด ์„œ๋ฒ„๊ฐ€ ํ•„๋“œ๋ช…์„ ๋ฐ”๊ฟ”๋„ 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

์„œ๋ฒ„ ์ž๋™ ์ฆ๊ฐ€ 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๊ฐ€ ์˜คํ”„๋ผ์ธ ํผ์ŠคํŠธ ๋ฐฉ์‹์˜ ํŽ˜์ด์ง€๋„ค์ด์…˜์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. ๊ตฌ์กฐ๋Š” ์ด๋ ‡์Šต๋‹ˆ๋‹ค.

1

UI๋Š” Room์ด ๋งŒ๋“  PagingSource๋งŒ ๊ตฌ๋…ํ•ฉ๋‹ˆ๋‹ค.

2

๋กœ์ปฌ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ฐ”๋‹ฅ๋‚˜๋ฉด Paging์ด RemoteMediator.load()๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค(์ƒˆ๋กœ๊ณ ์นจยท์•žยท๋’ค ๋ฐฉํ–ฅ์„ LoadType์œผ๋กœ ๊ตฌ๋ถ„).

3

RemoteMediator๋Š” ๋„คํŠธ์›Œํฌ์—์„œ ๋‹ค์Œ ํŽ˜์ด์ง€๋ฅผ ๋ฐ›์•„ DB์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. ๋‹ค์Œ ํŽ˜์ด์ง€ ํ‚ค๋Š” ๋ณ„๋„ '๋ฆฌ๋ชจํŠธ ํ‚ค' ํ…Œ์ด๋ธ”์— ๋ณด๊ด€ํ•˜๋Š” ๊ฒƒ์ด ์ผ๋ฐ˜์ ์ž…๋‹ˆ๋‹ค.

4

DB๊ฐ€ ๋ฐ”๋€Œ๋ฉด PagingSource๊ฐ€ ๋ฌดํšจํ™”๋˜๊ณ , ์ƒˆ ๋ฐ์ดํ„ฐ๊ฐ€ ํ™”๋ฉด์— ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์ด์–ด ๋ถ™์Šต๋‹ˆ๋‹ค.

@OptIn(ExperimentalPagingApi::class)
val pager = Pager(
    config = PagingConfig(pageSize = 30),
    remoteMediator = ArticleRemoteMediator(api, db),
    pagingSourceFactory = { db.articleDao().pagingSource() }
).flow

์˜คํ”„๋ผ์ธ์ผ ๋•Œ ์‚ฌ์šฉ์ž๋Š” ์ด๋ฏธ ๋ฐ›์•„ ๋‘” ํŽ˜์ด์ง€๊นŒ์ง€ ์Šคํฌ๋กคํ•  ์ˆ˜ ์žˆ๊ณ , ์—ฐ๊ฒฐ์ด ๋Œ์•„์˜ค๋ฉด ์ด์–ด์„œ ๋กœ๋“œ๋ฉ๋‹ˆ๋‹ค. ๊ตฌ์กฐ ์ž์ฒด๊ฐ€ ์˜คํ”„๋ผ์ธ ํผ์ŠคํŠธ์˜ˆ์š”.


โœ๏ธ 5. ์“ฐ๊ธฐ ์ „๋žต: ์„ธ ๊ฐ€์ง€ ์„ ํƒ์ง€

์ฝ๊ธฐ๋Š” ๋น„๊ต์  ์‰ฝ์Šต๋‹ˆ๋‹ค. ์ง„์งœ ์–ด๋ ค์šด ๊ฑด ์“ฐ๊ธฐ์ฃ . ๊ณต์‹ ๊ฐ€์ด๋“œ๋Š” ์˜คํ”„๋ผ์ธ ํผ์ŠคํŠธ ์•ฑ์˜ ์“ฐ๊ธฐ๋ฅผ ํฌ๊ฒŒ ์„ธ ๊ฐ€์ง€ ์ „๋žต์œผ๋กœ ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ๋Šฅ๋งˆ๋‹ค ๋‹ค๋ฅธ ์ „๋žต์„ ์„ž์–ด ์“ฐ๋Š” ๊ฒƒ์ด ์ •์ƒ์ž…๋‹ˆ๋‹ค.

์˜จ๋ผ์ธ ์ „์šฉ ์“ฐ๊ธฐ

๋„คํŠธ์›Œํฌ๋กœ๋งŒ ์“ฐ๊ธฐ๋ฅผ ์‹œ๋„ํ•˜๊ณ , ์„ฑ๊ณตํ•˜๋ฉด ๋กœ์ปฌ์„ ๊ฐฑ์‹ ํ•ฉ๋‹ˆ๋‹ค. ์‹คํŒจํ•˜๋ฉด ์‚ฌ์šฉ์ž์—๊ฒŒ ์•Œ๋ฆฌ๊ฑฐ๋‚˜ ์ฆ‰์‹œ ์žฌ์‹œ๋„.
์–ด์šธ๋ฆฌ๋Š” ๊ณณ: ๊ฒฐ์ œ, ์†ก๊ธˆ, ์ขŒ์„ ์˜ˆ์•ฝ์ฒ˜๋Ÿผ ์„œ๋ฒ„์˜ ์ฆ‰๊ฐ ํ™•์ •์ด ํ•„์š”ํ•œ ๋™์ž‘.

ํ ๊ธฐ๋ฐ˜ ์“ฐ๊ธฐ

์“ฐ๊ธฐ ์š”์ฒญ์„ ๋กœ์ปฌ ํ(๋˜๋Š” DB)์— ๋„ฃ๊ณ , ๋„คํŠธ์›Œํฌ๊ฐ€ ๊ฐ€๋Šฅํ•  ๋•Œ ์ˆœ์„œ๋Œ€๋กœ ์ „์†กํ•ฉ๋‹ˆ๋‹ค.
์–ด์šธ๋ฆฌ๋Š” ๊ณณ: ๋ถ„์„ ์ด๋ฒคํŠธ, ๋กœ๊ทธ, ์ˆœ์„œ๊ฐ€ ์ค‘์š”ํ•œ ๋ช…๋ น.

์ง€์—ฐ(Lazy) ์“ฐ๊ธฐ

๋จผ์ € ๋กœ์ปฌ 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()
}
๋น„๊ต์ƒํƒœ ์ปฌ๋Ÿผ ๋ฐฉ์‹์•„์›ƒ๋ฐ•์Šค ๋ฐฉ์‹
๊ตฌํ˜„ ๋‚œ์ด๋„๋‚ฎ์Œ์ค‘๊ฐ„
์ „์†ก ๋‹จ์œ„์ตœ์ข… ์ƒํƒœ(์Šค๋ƒ…์ƒท)๊ฐœ๋ณ„ ๋ช…๋ น(์ด๋ฒคํŠธ)
์ค‘๊ฐ„ ๋ณ€๊ฒฝ ์ด๋ ฅ๋ณ‘ํ•ฉ๋˜์–ด ์‚ฌ๋ผ์ง์ˆœ์„œ๋Œ€๋กœ ๋ณด์กด
์ž˜ ๋งž๋Š” ์„œ๋ฒ„ APIREST์˜ 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)
    }
}
๐Ÿ”’ NetworkType.CONNECTED

๋„คํŠธ์›Œํฌ๊ฐ€ ์—ฐ๊ฒฐ๋œ ์ƒํƒœ์—์„œ๋งŒ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ์˜คํ”„๋ผ์ธ ์ค‘์— ์Œ“์ธ ์ž‘์—…์€ ์—ฐ๊ฒฐ์ด ๋Œ์•„์˜ค๋ฉด ์‹œ์Šคํ…œ์ด ์•Œ์•„์„œ ๊นจ์›Œ ์ค๋‹ˆ๋‹ค. Wi-Fi์—์„œ๋งŒ ๋Œ€์šฉ๋Ÿ‰ ์—…๋กœ๋“œ๋ฅผ ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด UNMETERED๋ฅผ ์“ธ ์ˆ˜ ์žˆ์–ด์š”.

๐Ÿงท ๊ณ ์œ  ์ž‘์—…(Unique Work)

์‚ฌ์šฉ์ž๊ฐ€ ์ฒดํฌ๋ฐ•์Šค๋ฅผ 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. ์ถฉ๋Œ ํ•ด๊ฒฐ: ์˜คํ”„๋ผ์ธ ํผ์ŠคํŠธ์˜ ์ตœ์ข… ๋ณด์Šค

ํœด๋Œ€ํฐ๊ณผ ํƒœ๋ธ”๋ฆฟ์œผ๋กœ ๊ฐ™์€ ๋ฉ”๋ชจ๋ฅผ ๊ฐ๊ฐ ์˜คํ”„๋ผ์ธ์—์„œ ์ˆ˜์ •ํ–ˆ๋‹ค๊ณ  ์ƒ์ƒํ•ด ๋ณด์„ธ์š”. ๋‘˜ ๋‹ค ์—ฐ๊ฒฐ์ด ๋Œ์•„์˜ค๋ฉด ์„œ๋ฒ„๋Š” ๋ˆ„๊ตฌ ๋ง์„ ๋“ค์–ด์•ผ ํ• ๊นŒ์š”? ์ด๊ฒƒ์ด ๋™๊ธฐํ™” ์ถฉ๋Œ์ž…๋‹ˆ๋‹ค. ์˜คํ”„๋ผ์ธ ํผ์ŠคํŠธ์˜ ๋‚œ์ด๋„๋Š” ์‚ฌ์‹ค์ƒ ์ด ๋ฌธ์ œ๋ฅผ ์–ผ๋งˆ๋‚˜ ์ •๊ตํ•˜๊ฒŒ ๋‹ค๋ฃจ๋А๋ƒ๋กœ ๊ฒฐ์ •๋ผ์š”.

๋‘ ๊ธฐ๊ธฐ์˜ ์˜คํ”„๋ผ์ธ ์ˆ˜์ • ์ถฉ๋Œ ๐Ÿ“ฑ ํœด๋Œ€ํฐ ๐Ÿ’ป ํƒœ๋ธ”๋ฆฟ โ˜๏ธ ์„œ๋ฒ„ v1 "์šฐ์œ " "์šฐ์œ  2๊ฐœ" "๋‘์œ " v2 ์ˆ˜๋ฝ ! v1 ๊ธฐ๋ฐ˜ ์ˆ˜์ • โ†’ ์ถฉ๋Œ ๊ฐ์ง€ ์˜คํ”„๋ผ์ธ ๊ตฌ๊ฐ„

๐Ÿง  ์ถฉ๋Œ ํ•ด๊ฒฐ ์ „๋žต ์ง€๋„

์ „๋žต๋ฐฉ์‹์žฅ์ ํ•œ๊ณ„
๋งˆ์ง€๋ง‰ ์“ฐ๊ธฐ ์Šน๋ฆฌ(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. ํ…Œ์ŠคํŠธ: ๋น„ํ–‰๊ธฐ ๋ชจ๋“œ ๋ฒ„ํŠผ๋งŒ ๋ˆ„๋ฅด๋ฉด ๋์ผ๊นŒ?

์˜คํ”„๋ผ์ธ ํผ์ŠคํŠธ ์•ฑ์˜ ๋ฒ„๊ทธ๋Š” ๋Œ€๋ถ€๋ถ„ ํƒ€์ด๋ฐ๊ณผ ์ƒํƒœ ์ „์ด์—์„œ ๋‚˜์˜ต๋‹ˆ๋‹ค. "์˜คํ”„๋ผ์ธ์—์„œ ์ƒ์„ฑ โ†’ ์ˆ˜์ • โ†’ ์‚ญ์ œ โ†’ ์˜จ๋ผ์ธ" ๊ฐ™์€ ์‹œ๋‚˜๋ฆฌ์˜ค๋Š” ์†์œผ๋กœ ์žฌํ˜„ํ•˜๊ธฐ ์–ด๋ ต๊ธฐ ๋•Œ๋ฌธ์— ์ž๋™ํ™” ํ…Œ์ŠคํŠธ์˜ ๊ฐ€์น˜๊ฐ€ ํŠนํžˆ ํฝ๋‹ˆ๋‹ค.

๐Ÿงฉ Repository ๋‹จ์œ„ ํ…Œ์ŠคํŠธ

DAO์™€ API๋ฅผ ๊ฐ€์งœ(Fake) ๊ตฌํ˜„์œผ๋กœ ๋ฐ”๊ฟ”, "์“ฐ๊ธฐ ํ›„ syncState๊ฐ€ PENDING_UPDATE๊ฐ€ ๋˜๋Š”๊ฐ€", "์ƒ์„ฑ ๋Œ€๊ธฐ ํ•ญ๋ชฉ ์‚ญ์ œ ์‹œ ์„œ๋ฒ„ ํ˜ธ์ถœ์ด ์—†๋Š”๊ฐ€"๋ฅผ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. Now in Android ์ƒ˜ํ”Œ๋„ ํ…Œ์ŠคํŠธ์šฉ ๊ฐ€์งœ ๋ฐ์ดํ„ฐ ์†Œ์Šค๋ฅผ ์ ๊ทน ํ™œ์šฉํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ—„๏ธ Room ์ธ๋ฉ”๋ชจ๋ฆฌ DB ํ…Œ์ŠคํŠธ

Room.inMemoryDatabaseBuilder()๋กœ ์‹ค์ œ SQL ๋™์ž‘์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. Flow๊ฐ€ ๋ณ€๊ฒฝ ์‹œ ์ƒˆ ๊ฐ’์„ ๋‚ด๋ณด๋‚ด๋Š”์ง€๋Š” Turbine ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ ๊น”๋”ํ•˜๊ฒŒ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์–ด์š”.

๐Ÿ‘ท WorkManager ํ…Œ์ŠคํŠธ

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. ๋ณด์•ˆยท์ €์žฅ ๊ณต๊ฐ„ยท๋ฐฐํ„ฐ๋ฆฌ: ์ˆจ์€ ์šด์˜ ์ด์Šˆ

๐Ÿ”’ ๊ธฐ๊ธฐ์— ๋‚จ๋Š” ๋ฐ์ดํ„ฐ์˜ ๋ณด์•ˆ

์˜คํ”„๋ผ์ธ ํผ์ŠคํŠธ๋Š” ์ •์˜์ƒ ๋” ๋งŽ์€ ๋ฐ์ดํ„ฐ๋ฅผ ๋” ์˜ค๋ž˜ ๊ธฐ๊ธฐ์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ๋ณธ์ ์œผ๋กœ ์•ฑ ์ „์šฉ ๋‚ด๋ถ€ ์ €์žฅ์†Œ์˜ ๋ฐ์ดํ„ฐ๋Š” ๋‹ค๋ฅธ ์•ฑ์ด ์ ‘๊ทผํ•  ์ˆ˜ ์—†๋„๋ก ์ƒŒ๋“œ๋ฐ•์Šค๋กœ ๋ณดํ˜ธ๋˜๊ณ , ์ตœ์‹  ์•ˆ๋“œ๋กœ์ด๋“œ ๊ธฐ๊ธฐ๋Š” ์ €์žฅ์†Œ ์•”ํ˜ธํ™”๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ž˜๋„ ์˜๋ฃŒยท๊ธˆ์œต์ฒ˜๋Ÿผ ๋ฏผ๊ฐํ•œ ๋ฐ์ดํ„ฐ๋ผ๋ฉด ์ถ”๊ฐ€ ์กฐ์น˜๋ฅผ ๊ฒ€ํ† ํ•˜์„ธ์š”.

๐Ÿ—๏ธ DB ์•”ํ˜ธํ™”

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. ๋„์ž… ๋กœ๋“œ๋งต: ๊ธฐ์กด ์•ฑ์„ ์˜คํ”„๋ผ์ธ ํผ์ŠคํŠธ๋กœ ๋ฐ”๊พธ๋ ค๋ฉด

์ฒ˜์Œ๋ถ€ํ„ฐ ์ƒˆ๋กœ ๋งŒ๋“œ๋Š” ์•ฑ์ด ์•„๋‹ˆ๋ผ๋ฉด, ํ•œ ๋ฒˆ์— ๋ชจ๋“  ๊ธฐ๋Šฅ์„ ๋ฐ”๊พธ๊ธฐ๋ณด๋‹ค ๋‹จ๊ณ„์ ์œผ๋กœ ์ „ํ™˜ํ•˜๋Š” ํŽธ์ด ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค.

1

ํ•ต์‹ฌ ๊ธฐ๋Šฅ ์„ ์ • โ€” "์˜คํ”„๋ผ์ธ์—์„œ ์ด๊ฒƒ๋งŒํผ์€ ๋ผ์•ผ ํ•œ๋‹ค"๋ฅผ ๋จผ์ € ์ •ํ•ฉ๋‹ˆ๋‹ค. ๋ชจ๋“  ๊ธฐ๋Šฅ์„ ์˜คํ”„๋ผ์ธํ™”ํ•  ํ•„์š”๋Š” ์—†์–ด์š”.

2

์ฝ๊ธฐ๋ถ€ํ„ฐ ์ „ํ™˜ โ€” Repository๊ฐ€ API ์‘๋‹ต ๋Œ€์‹  Room Flow๋ฅผ ๋…ธ์ถœํ•˜๋„๋ก ๋ฐ”๊ฟ‰๋‹ˆ๋‹ค. ์ด ๋‹จ๊ณ„๋งŒ์œผ๋กœ๋„ ์ฒด๊ฐ ์†๋„์™€ ์˜คํ”„๋ผ์ธ ์—ด๋žŒ์ด ํฌ๊ฒŒ ๊ฐœ์„ ๋ฉ๋‹ˆ๋‹ค.

3

๋ฐฑ์—”๋“œ ๊ณ„์•ฝ ํ•ฉ์˜ โ€” ID ์ƒ์„ฑ, ๋ฒ„์ „ ํ•„๋“œ, ๋ธํƒ€ API, ์‚ญ์ œ ํ‘œํ˜„, ์ถฉ๋Œ ์‘๋‹ต ๊ทœ์น™์„ ์ •ํ•ฉ๋‹ˆ๋‹ค.

4

์“ฐ๊ธฐ ์ „ํ™˜ โ€” ๊ธฐ๋Šฅ๋ณ„๋กœ ์˜จ๋ผ์ธ ์ „์šฉ/ํ/์ง€์—ฐ ์“ฐ๊ธฐ๋ฅผ ์ •ํ•˜๊ณ , ๋™๊ธฐํ™” ์ƒํƒœ ์ปฌ๋Ÿผ์ด๋‚˜ ์•„์›ƒ๋ฐ•์Šค๋ฅผ ๋„์ž…ํ•ฉ๋‹ˆ๋‹ค.

5

๋™๊ธฐํ™” ์›Œ์ปค์™€ ๊ด€์ธก์„ฑ โ€” WorkManager ์›Œ์ปค๋ฅผ ๋ถ™์ด๊ณ , ๋™๊ธฐํ™” ์‹คํŒจ์œจยท๋Œ€๊ธฐ ํ ๊ธธ์ดยท์ถฉ๋Œ ๋ฐœ์ƒ ์ˆ˜ ๊ฐ™์€ ์ง€ํ‘œ๋ฅผ ์ˆ˜์ง‘ํ•ฉ๋‹ˆ๋‹ค.

6

UX ๋‹ค๋“ฌ๊ธฐ โ€” ์˜คํ”„๋ผ์ธ ๋ฐฐ๋„ˆ, ๋Œ€๊ธฐ ๋ฐฐ์ง€, ์‹คํŒจ ๋ณต๊ตฌ ํ๋ฆ„์„ ์™„์„ฑํ•ฉ๋‹ˆ๋‹ค.

์ด ๊ณผ์ •์€ ์•ฑ ์„ค๊ณ„, ๋ฐฑ์—”๋“œ API, UX ๋””์ž์ธ์ด ํ•œ๊บผ๋ฒˆ์— ์–ฝํžˆ๋Š” ์ž‘์—…์ด๋ผ ํŒ€ ๊ตฌ์„ฑ์ด ์ž‘๋‹ค๋ฉด ๋ถ€๋‹ด์ด ๋  ์ˆ˜ ์žˆ์–ด์š”. ์ด๋Ÿด ๋• ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ์žฌ๋Šฅ ๊ณต์œ  ํ”Œ๋žซํผ์—์„œ ์•ˆ๋“œ๋กœ์ด๋“œ ์•„ํ‚คํ…์ฒ˜ ๊ฒฝํ—˜์ด ์žˆ๋Š” ๊ฐœ๋ฐœ์ž๋‚˜ ๋ชจ๋ฐ”์ผ UX ๋””์ž์ด๋„ˆ์˜ ๋„์›€์„ ๋ฐ›์•„, ์„ค๊ณ„ ๋ฆฌ๋ทฐ๋‚˜ ํŠน์ • ๋‹จ๊ณ„๋งŒ ๋งก๊ธฐ๋Š” ๋ฐฉ์‹๋„ ํ˜„์‹ค์ ์ธ ์„ ํƒ์ง€๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.


โœ… 15. ์ถœ์‹œ ์ „ ์…€ํ”„ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

โ˜‘๏ธ ํ™”๋ฉด์€ ๋„คํŠธ์›Œํฌ ์‘๋‹ต์ด ์•„๋‹ˆ๋ผ ๋กœ์ปฌ DB์˜ Flow๋ฅผ ๊ตฌ๋…ํ•˜๋Š”๊ฐ€?
โ˜‘๏ธ ์ตœ์ดˆ ์‹คํ–‰ + ์˜คํ”„๋ผ์ธ ์ƒํ™ฉ์—์„œ ๊ฑฐ์ง“ ๋นˆ ํ™”๋ฉด์„ ๋ณด์—ฌ์ฃผ์ง€ ์•Š๋Š”๊ฐ€?
โ˜‘๏ธ ์˜คํ”„๋ผ์ธ์—์„œ ์ƒ์„ฑํ•œ ํ•ญ๋ชฉ์— ํด๋ผ์ด์–ธํŠธ ์ƒ์„ฑ ID๊ฐ€ ๋ถ€์—ฌ๋˜๋Š”๊ฐ€?
โ˜‘๏ธ ์‚ญ์ œ๊ฐ€ ์†Œํ”„ํŠธ ์‚ญ์ œ โ†’ ์„œ๋ฒ„ ๋ฐ˜์˜ โ†’ ์‹ค์ œ ์‚ญ์ œ ์ˆœ์„œ๋กœ ์ฒ˜๋ฆฌ๋˜๋Š”๊ฐ€?
โ˜‘๏ธ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ๊ณผ ๋™๊ธฐํ™” ๋Œ€๊ธฐ ๊ธฐ๋ก์ด ๊ฐ™์€ ํŠธ๋žœ์žญ์…˜์— ๋ฌถ์—ฌ ์žˆ๋Š”๊ฐ€?
โ˜‘๏ธ ๋™๊ธฐํ™”๊ฐ€ WorkManager ๊ณ ์œ  ์ž‘์—…์œผ๋กœ, ๋„คํŠธ์›Œํฌ ์ œ์•ฝ๊ณผ ๋ฐฑ์˜คํ”„๋ฅผ ๊ฐ–๊ณ  ์‹คํ–‰๋˜๋Š”๊ฐ€?
โ˜‘๏ธ ์ผ์‹œ์  ์˜ค๋ฅ˜๋Š” ์žฌ์‹œ๋„ํ•˜๊ณ , ์˜๊ตฌ ์˜ค๋ฅ˜๋Š” ๊ฒฉ๋ฆฌยท์•ˆ๋‚ดํ•˜๋Š”๊ฐ€?
โ˜‘๏ธ ์‘๋‹ต ์œ ์‹ค ํ›„ ์žฌ์‹œ๋„์—๋„ ์ค‘๋ณต ์ƒ์„ฑ์ด ์—†๋Š”๊ฐ€(๋ฉฑ๋“ฑ์„ฑ)?
โ˜‘๏ธ ์„œ๋ฒ„ Pull์ด ๋ฏธ์ „์†ก ๋กœ์ปฌ ๋ณ€๊ฒฝ์„ ๋ฎ์–ด์“ฐ์ง€ ์•Š๋Š”๊ฐ€?
โ˜‘๏ธ ์ถฉ๋Œ ์ •์ฑ…(LWW, ๋ฒ„์ „ ๊ฑฐ๋ถ€, ๋ณ‘ํ•ฉ, ์‚ฌ์šฉ์ž ์„ ํƒ)์ด ๊ธฐ๋Šฅ๋ณ„๋กœ ๋ช…์‹œ๋˜์–ด ์žˆ๋Š”๊ฐ€?
โ˜‘๏ธ DB ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์ด ํ…Œ์ŠคํŠธ๋˜์—ˆ๊ณ , ํŒŒ๊ดด์  ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ํ”ผํ–ˆ๋Š”๊ฐ€?
โ˜‘๏ธ ๋กœ๊ทธ์•„์›ƒยท๊ณ„์ • ์ „ํ™˜ ์‹œ ๋กœ์ปฌ ๋ฐ์ดํ„ฐ ์ •๋ฆฌ ์ •์ฑ…์ด ์žˆ๋Š”๊ฐ€?

๐Ÿงพ ํ•œ๋ˆˆ์— ์ •๋ฆฌ

์›์น™ โ€” ๋กœ์ปฌ 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
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.