Search
Duplicate

Coroutines + Legacy =

Tags
Engineering
Android
Kotlin Coroutines๋Š” ํ˜„์žฌ Android ๊ฐœ๋ฐœ ์ƒํƒœ๊ณ„์—์„œ ๊ฐ€์žฅ ์ฃผ๋ชฉ๋ฐ›๋Š” ๊ธฐ์ˆ  ์ค‘ ํ•˜๋‚˜์ผ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๊ธ€์„ ์ฝ๋Š” ์—ฌ๋Ÿฌ๋ถ„๋“ค์€ ์ด๋ฏธ ๊ฐ์ž์˜ ์„œ๋น„์Šค์— ์ด ๊ธฐ์ˆ ์„ ์ ์šฉํ–ˆ๊ฑฐ๋‚˜ ๊ฒ€ํ† ํ•˜๋Š” ์ค‘์ด๋ผ ์ƒ๊ฐํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๊ธ€์€ Cupist์˜ Java-MVP to Kotlin-MVVM ์ „ํ™˜ ์ž‘์—…์ด ํ•œ์ฐฝ์ด๋˜ 2019๋…„ ์ƒ๋ฐ˜๊ธฐ์˜ ๊ธฐ๋ก์˜ ์ผ๋ถ€์ด๋ฉฐ ์ฝ๋Š” ๋ถ„๋“ค์—๊ฒŒ ์กฐ๊ธˆ์ด๋‚˜๋งˆ ๋„์›€์ด ๋˜์—ˆ์œผ๋ฉด ํ•˜๋Š” ๋งˆ์Œ์œผ๋กœ ์ธ์ƒ์ ์œผ๋กœ ๋Š๊ผˆ๋˜ ์ด์Šˆ์™€ ๊ทธ ์›์ธ์— ๋Œ€ํ•œ ์ด์•ผ๊ธฐ๋ฅผ ๊ณต์œ ํ•˜๊ณ ์ž ํ•ฉ๋‹ˆ๋‹ค. ๋ฌผ๋ก  ๊ฐ์ž์˜ ์„œ๋น„์Šค๋งˆ๋‹ค ๋งŒ๋‚  ์ˆ˜ ์žˆ๋Š” ์ด์Šˆ๋Š” ์กฐ๊ธˆ ๋” ๋‹ค์–‘ํ•  ๊ฒƒ์ด๊ณ  "๋ญ ์ด๋Ÿฐ ์‹ค์ˆ˜๋ฅผ ํ•ด?"๋ผ๋Š” ์ƒ๊ฐ์„ ๊ฐ€์ง€์‹ค ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค๋งŒ, ์ €๋„ ๋งŒ๋‚˜๊ธฐ ์ „๊นŒ์ง€ ์ƒ๊ฐ ๋ชป ํ–ˆ์—ˆ๊ฑฐ๋“ ์š”. ์—ฌ๋Ÿฌ๋ถ„๋“ค์€ ๊ฐ™์€ ์‹ค์ˆ˜๋ฅผ ํ•˜์ง€ ๋งˆ์‹œ๊ธธ ๋ฐ”๋ž๋‹ˆ๋‹ค.

์ฝ”๋ฃจํ‹ด์ด๋ผ๊ตฌ์š”?

๋„ค, ์ฝ”๋ฃจํ‹ด์ด์š”. ์–ด์ฐจํ”ผ ์ •์‹ ๋ฆด๋ฆฌ์ฆˆ ๋œ ๊ฑฐ ๋ฐ”๋กœ ์ง„ํ–‰ ํ•ด๋ณด์ž ๋ผ๋Š” ๋งˆ์Œ์œผ๋กœ ๋„์ž…์„ ์‹œ์ž‘ํ•˜์˜€์Šต๋‹ˆ๋‹ค. ์ด๋ฏธ ๋งŽ์€ ๊ธ€์—์„œ ์ด ๊ธฐ์ˆ ์ด ์ „ํ†ต์ ์ธ ์•ˆ๋“œ๋กœ์ด๋“œ ๊ฐœ๋ฐœ๊ณผ ๋‹ค๋ฅธ ๊ธฐ์ˆ ์ด ๊ฐ€์ง€์ง€ ๋ชปํ•˜๋Š” ์ด์ ์— ๋Œ€ํ•ด ๋‹ค๋ฃจ๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋”ฐ๋กœ ์„ค๋ช…ํ•˜์ง€ ์•Š์œผ๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค. Cupist์˜ ์•ˆ๋“œ๋กœ์ด๋“œ ๊ฐœ๋ฐœ์ž๋“ค ๋˜ํ•œ ์ด๋Ÿฌํ•œ ๊ธ€์„ ์ฐธ๊ณ ํ•˜๋ฉฐ ์ด ๊ธฐ์ˆ ์ด ์šฐ๋ฆฌ์—๊ฒŒ ์ฃผ๋Š” ์ด์ ์— ๋Œ€ํ•ด ๊ธฐ๋Œ€๋ฅผ ํ•˜์˜€๊ณ  ๊ฐœ๋ฐœ์„ ์ง„ํ–‰ํ•จ๊ณผ ๋™์‹œ์— ๊ฐํƒ„ํ•˜์˜€๊ณ  ์žฅ๋ฏธ๋น› ๋ฏธ๋ž˜๋ฅผ ์ƒ๊ฐํ•˜๋ฉฐ ์ƒˆ๋กœ์šด ์•ˆ๋“œ๋กœ์ด๋“œ ์•ฑ ๋ฒ„์ „์„ ๋ฆด๋ฆฌ์ฆˆํ•˜์˜€์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  Fabric์˜ ๋ฆฌํฌํŠธ์— ๋ถˆ๊ฝƒ๊ฐ™์ด ์˜ฌ๋ผ์˜ค๋Š” Crash๋ฅผ ๋ณด๋ฉฐ ๋ฌด์–ธ๊ฐ€ ์ž˜๋ชป๋˜์—ˆ๋‹ค๋Š” ๊ฒƒ์„ ๋Š๊ผˆ์ฃ ...
android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5) ################################################################# Error Code : 5 (SQLITE_BUSY) Caused By : The database file is locked. (database is locked (code 5): , while compiling: PRAGMA cache_size) #################################################################
Plain Text
์นœ๊ตฌ์•ผ ์˜ค๋žœ๋งŒ์ด์•ผ!
์นœ๊ตฌ์•ผ ๋‚˜๋„ ์˜ค๋žœ๋งŒ์ด์•ผ! ๊ทธ๋Ÿฐ๋ฐ ๋„ˆ ์ซŒ ๋งŽ์ด ๋ฐ”์˜๊ตฌ๋‚˜?

Legacy Code์˜ ๋Šช

์œ„์—์„œ ์–ธ๊ธ‰ํ•œ ๋ฐ”์™€ ๊ฐ™์ด ์ด ์ž‘์—…์€ Java + MVP๋กœ ๊ตฌ์„ฑ๋œ ์•ฑ ๋ฉ”์ธ ์ฝ”๋“œ๋ฅผ Kotlin + MVVM์œผ๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋ฐ ๋ฆฌํŒฉํ† ๋ง ์ž‘์—…์˜ ์ผ๋ถ€์˜€์Šต๋‹ˆ๋‹ค. ๊ธฐ์กด์— Callback ๋ฐฉ์‹ ๋“ฑ์œผ๋กœ ๊ตฌํ˜„๋˜์—ˆ๋˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ฝ”๋ฃจํ‹ด์„ ์ด์šฉํ•˜์—ฌ ๋ฆฌํŒฉํ† ๋งํ•˜๋Š” ๊ณผ์ •์—์„œ ์ผ๋ถ€ SQLite ์ ‘๊ทผ ๋กœ์ง๋“ค์ด IO Dispatcher๋ฅผ ํ†ตํ•ด ์‹คํ–‰๋˜๋„๋ก ์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  IO Thread์—์„œ DB ์“ฐ๊ธฐ ์ž‘์—…์„ ํ•˜๋Š” ์ค‘ ์ƒ๊ฐํ•˜์ง€๋„ ์•Š์€ ๋‹ค๋ฅธ Activity์˜ Main Thread์—์„œ DB ์ฝ๊ธฐ๋ฅผ ์‹œ๋„ํ•˜์˜€๊ณ  ์•ฑ์ด ์ฃฝ์–ด ๋‚˜๊ฐ€๊ธฐ ์‹œ์ž‘ํ–ˆ์Šต๋‹ˆ๋‹ค.
์›์ธ๋„ ์–ด๋Š ์ •๋„ ํŒŒ์•…๋˜์—ˆ์œผ๋‹ˆ ๋น ๋ฅด๊ฒŒ ๊ณ ์ณ์•ผ๊ฒ ๋‹ค๋Š” ๋งˆ์Œ์— IO Dispatcher์—์„œ ๋™์ž‘ํ•˜๋Š” SQLite์˜ Transaction Mode๋ฅผ IMMEDIATE๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ํ•ซํ”ฝ์Šค ํ•˜๊ณ  "์–ดํœด.. ํฐ์ผ ๋‚ ๋ป”ํ–ˆ๋‹ค.."๋ผ ์ƒ๊ฐํ•˜์˜€์Šต๋‹ˆ๋‹ค. (์•„๋‹ˆ ์ด๋ฏธ ๋‚œ๊ฑด๋ฐ..?) ์—…๋ฐ์ดํŠธ ํ›„ ์ง„์ •๋œ Crash Report๋ฅผ ๋ณด๋ฉฐ ์•ˆ๋„ํ•จ์„ ๋Š๋ผ๋ฉฐ Fabric์˜ Non-Fatals Report๋ฅผ ํ™•์ธํ•˜๋Š” ์ˆœ๊ฐ„ ์ด ์ด์Šˆ์˜ ์ˆจ๊ฒจ์ง„ ์ด๋ฉด์„ ์•Œ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
java.lang.IllegalStateException: attempt to re-open an already-closed object
Plain Text
์นœ๊ตฌ์•ผ ๋‚˜๋„ ์˜ค๋žœ๋งŒ์ด์•ผ~
์•„...๊ทธ๋ž˜...๊ทธ๋ƒฅ ๋ญ”๊ฐ€ ์—„์ฒญ ์ž˜๋ชป ๋งŒ๋“  ๊ฑฐ์˜€๊ตฌ๋‚˜!!!
์—ญ์‹œ๋‚˜ ๋ฒ”์ธ์€ Dispatchers.IO๋ฅผ ์ด์šฉํ•˜์—ฌ ์ƒ์„ฑ๋œ CoroutinScope์˜ Thread๋“ค ์ด์—ˆ์Šต๋‹ˆ๋‹ค. IO Dispatcher๋กœ ์‹คํ–‰๋œ ๋กœ์ง๋“ค์˜ ๋™์ž‘ Thread๋Š” ๋‹ค๋ฅผ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์„œ๋กœ ๋ถˆ๊ฝƒ ๊ฐ™์€ ์ž์› ์Ÿํƒˆ์ „์„ ๋ฒŒ์ด๋ฉฐ ์ด์Šˆ๊ฐ€ ๋ฐœ์ƒํ•˜๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. (๋ฌธ์„œ๋ฅผ ์ž˜ ์ฝ์œผ๋ผ๊ณ  ์นœ๊ตฌ )

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์งค ์‹œ๊ฐ„

๊ฐ€์žฅ ๋น ๋ฅด๊ณ  ํ™•์‹คํ•˜๊ฒŒ ํ•ด๊ฒฐ ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์€ SQLite ๊ด€๋ จ ๋™์ž‘์„ ์ „๋ถ€ Main์œผ๋กœ ๋Œ๋ฆฌ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ํ›„๋ณด๋กœ๋Š” newSingleThreadContext๋ฅผ ์ด์šฉํ•œ ๋ณ„๋„์˜ ThreadContext๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๊ฒƒ ์ •๋„๊ฐ€ ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค. (์ด๋ฆ„์€ Dispatchers.Database๋กœ ํ•˜๋Š” ๊ฒŒ ์ข‹๊ฒ ๊ตฐ! ) ํ•ด๊ฒฐ๋œ๋‹ค๋Š” ๊ฒƒ์€ ํ™•์‹คํ•˜์ง€๋งŒ ํ™•์ธ์ฐจ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.
์šฐ์„  ์ด์Šˆ ์žฌํ˜„ ๋ฐ ํ•ด๊ฒฐ์„ ์œ„ํ•˜์—ฌ ํ…Œ์ŠคํŠธ์šฉ SQLite Wrapping Class์ธ TestDatabase๋ฅผ ์ƒ์„ฑํ•˜์˜€์Šต๋‹ˆ๋‹ค. ๊ฐ์ฒด๋ฅผ ํ•˜๋‚˜์”ฉ ์ €์žฅํ•˜๊ณ  ์ง€์šธ ์ˆ˜ ์žˆ๋Š” ๊ฐ„๋‹จํ•œ ๊ธฐ๋Šฅ์ด ์žˆ์ง€๋งŒ, ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•˜์—ฌ Insert ํ•  ๋•Œ๋งˆ๋‹ค 2์ดˆ์”ฉ sleep ํ•œ๋‹ค๋Š” ๊ฒฐ์ ์„ ๋ถ€์—ฌํ•˜์˜€์Šต๋‹ˆ๋‹ค. ์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด ๋ด…๋‹ˆ๋‹ค. ํ•ด๋‹น ์ด์Šˆ์˜ ํ…Œ์ŠคํŠธ๋Š” SQLite์™€ Coroutines๋ฅผ ์ง€์›ํ•ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ Robolectric๊ณผ kotlinx-coroutines-test(1.1.1 ๋ฒ„์ „ ์‚ฌ์šฉ ๋จ)๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
@ExperimentalCoroutinesApi @RunWith(RobolectricTestRunner::class) class DatabaseTest { private val mainThreadSurrogate = newSingleThreadContext("UI thread") lateinit var database: TestDatabase val testName = "TEST_NAME" @Before fun setup() { Dispatchers.setMain(mainThreadSurrogate) database = TestDatabase(ApplicationProvider.getApplicationContext()) } @After fun tearDown() { Dispatchers.resetMain() mainThreadSurrogate.close() } @Test fun whenInsertTestData() { database.insert(testName) val users = database.getAll() val user = users.find { it.name == testName } assertNotNull(user) assertEquals(1, users.size) } }
Kotlin
๊ธฐ๋ณธ์ ์ธ ์„ธํŒ…์ด ๋œ ๋ชจ์Šต์œผ๋กœ SQLite ๋ฐ ์ฝ”๋ฃจํ‹ด์„ ์ดˆ๊ธฐํ™”ํ•˜๊ณ  SQLite๊ฐ€ ์ •์ƒ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๊ฐ„๋‹จํ•œ ํ…Œ์ŠคํŠธ๊ฐ€ ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด์ œ Fabric์—์„œ ์งˆ๋ฆฌ๋„๋ก ๋ณธ ์ด์Šˆ๋ฅผ ์žฌํ˜„ํ•ด๋ณด๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.
์ž‘์„ฑ๋  ์ผ€์ด์Šค๋Š” ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.
โ€ข
IO Thread(์ •ํ™•ํžˆ๋Š” DefaultDispatcher์˜ Worker ์ค‘ ํ•˜๋‚˜์ด๊ฒ ์ง€๋งŒ ํŽธ์˜์ƒ IO Thread๋ผ ์นญํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.)์—์„œ ์“ฐ๊ธฐ ์ž‘์—… ์‹œ๋„ ์ค‘ ๋Œ€๋žต 0.5์ดˆ ํ›„ ๋˜ ๋‹ค๋ฅธ IO Thread์—์„œ ์“ฐ๊ธฐ ์‹œ๋„
โ€ข
Main Thread์—์„œ ์“ฐ๊ธฐ ์ž‘์—… ์‹œ๋„ ์ค‘ ๋Œ€๋žต 0.5์ดˆ ํ›„ IO Thread์—์„œ ์“ฐ๊ธฐ ์‹œ๋„
@ExperimentalCoroutinesApi @RunWith(RobolectricTestRunner::class) class DatabaseTest { ... @Test(expected = IllegalStateException::class) fun givenWhileInsertingOnIO_whenInsertOnIO_thenException() = runBlocking(Dispatchers.Default) { launch(Dispatchers.IO) { database.insert(testName) } delay(500) launch(Dispatchers.IO) { database.insert(testName) assertTrue(false) } Unit } @Test(expected = IllegalStateException::class) fun givenWhileInsertingOnMain_whenInsertOnIO_thenException() = runBlocking(Dispatchers.Default) { launch(Dispatchers.Main) { database.insert(testName) } delay(500) launch(Dispatchers.IO) { database.insert(testName) assertTrue(false) } Unit } }
Kotlin
... ๋ญ”๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์€ ๋ชจ์–‘์œผ๋กœ ๋ณด์ž…๋‹ˆ๋‹ค. ์ตœ๋Œ€ํ•œ ์‹œ๋‚˜๋ฆฌ์˜ค๋Œ€๋กœ ๋™์ž‘ํ•˜๋„๋ก ์˜๋„ํ•˜์˜€์ง€๋งŒ ์ด๋Ÿฐ ํƒ€์ž…์˜ ์ด์Šˆ(ํƒ€์ด๋ฐ์ด๋ผ ๋ถˆ๋ฆฌ๋Š”)๋ฅผ ์œ„ํ•œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ๋งŽ์ด ์ž‘์„ฑํ•˜์ง€ ์•Š์•„ ์ต์ˆ™ํ•˜์ง€ ์•Š๋„ค์š”. ๊ทธ๋ž˜๋„ ๊ธฐ๋Œ€ํ•œ ๋Œ€๋กœ Exception์ด ๋ฐœ์ƒํ•˜๋ฉฐ ํ…Œ์ŠคํŠธ๋Š” ์„ฑ๊ณตํ•˜๋Š” ๊ฒƒ์œผ๋กœ ๋ณด์ž…๋‹ˆ๋‹ค. ์ด์ œ ์ด ์ฝ”๋“œ๋ฅผ ๋ฒ ์ด์Šค๋กœ Main๊ณผ ์ง์ ‘ ์ƒ์„ฑํ•œ ThreadContext๋ฅผ ์ด์šฉํ•ด์„œ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค.
์ž‘์„ฑ๋  ์ผ€์ด์Šค๋Š” ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.
โ€ข
Main Thread์—์„œ ์“ฐ๊ธฐ ์ž‘์—… ์‹œ๋„ ์ค‘ ๋Œ€๋žต 0.5์ดˆ ํ›„ Main Thread์—์„œ ์“ฐ๊ธฐ ์‹œ๋„
โ€ข
Database Thread์—์„œ ์“ฐ๊ธฐ ์ž‘์—… ์‹œ๋„ ์ค‘ ๋Œ€๋žต 0.5์ดˆ ํ›„ Database Thread์—์„œ ์“ฐ๊ธฐ ์‹œ๋„
์œ„ ์ผ€์ด์Šค์—์„œ ๋‘ ๋ฒˆ์งธ Insert๋Š” ์ฒซ ๋ฒˆ์งธ Insert๊ฐ€ ์™„์ „ํžˆ ์ข…๋ฃŒ๋œ ํ›„ ๋™์ž‘ํ•˜๊ฒŒ ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค.
@ExperimentalCoroutinesApi @RunWith(RobolectricTestRunner::class) class DatabaseTest { private val databaseContext = newSingleThreadContext("Database thread") ... @Test fun givenWhileInsertingOnMain_whenInsertOnMain_thenInserted() = runBlocking(Dispatchers.Default) { launch(Dispatchers.Main) { database.insert(testName) } delay(500) launch(Dispatchers.Main) { database.insert(testName) }.join() assertEquals(2, database.getAll().size) } @Test fun givenWhileInsertingOnDatabase_whenInsertOnDatabase_thenInserted() = runBlocking(Dispatchers.Default) { launch(databaseContext) { database.insert(testName) } delay(500) launch(databaseContext) { database.insert(testName) }.join() assertEquals(2, database.getAll().size) } }
Kotlin
ํ…Œ์ŠคํŠธ๋ฅผ ๋™์ž‘์‹œํ‚ค๋ฉด ์˜ˆ์ƒ๋Œ€๋กœ ๋‘ ํ…Œ์ŠคํŠธ ๋ชจ๋‘ ํ†ต๊ณผํ•˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿผ ์ด์ œ ํ•ซํ”ฝ์Šค๋ฅผ ์œ„ํ•œ ์„ ํƒ์˜ ์‹œ๊ฐ„์ž…๋‹ˆ๋‹ค.

์—ํ•„๋กœ๊ทธ

newSingleThreadContext์˜ ๊ฐ’๋น„์‹ผ ๋น„์šฉ์— ๋น„ํ•˜์—ฌ Glam App์˜ SQLite๋Š” ๊ทธ๋ฆฌ ๋ฌด๊ฒ๊ฒŒ ์‚ฌ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ฒฐ๊ตญ, ์ด๋Ÿฌํ•œ ๋น„์šฉ ๋ฌธ์ œ์™€ ๋ ˆ๊ฑฐ์‹œ ์ฝ”๋“œ์™€์˜ ์ด์Šˆ ๋ณด์žฅ ๋“ฑ์˜ ์ด์œ ๋กœ ์šฐ์„  SQLite๋Š” Main์—์„œ ์ž‘์—…ํ•˜๋Š” ๊ฒƒ์œผ๋กœ ๊ฒฐ์ •๋˜์–ด ํ•ซํ”ฝ์Šค ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด์ƒ์ ์ด์ง€ ์•Š๊ณ  ์™„๋ฒฝ๊ณผ๋Š” ๊ฑฐ๋ฆฌ๊ฐ€ ๋จผ ๋ฐฉ๋ฒ•์ด์ง€๋งŒ ํšจ๊ณผ๋Š” ํ™•์‹คํ•˜์—ฌ ๋น ๋ฅด๊ฒŒ ์ด์Šˆ๊ฐ€ ์ง„์ •๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•˜์˜€์Šต๋‹ˆ๋‹ค. ์ƒ๊ฐํ•ด๋ณด๋ฉด ๋ ˆ๊ฑฐ์‹œ ์ฝ”๋“œ๋ฅผ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ๋ฐ”๋กœ Room์œผ๋กœ Migration ํ–ˆ๋‹ค๋ฉด ๋งŒ๋‚˜์ง€ ์•Š์•˜์„ ์ด์Šˆ์˜€์„์ง€๋„ ๋ชจ๋ฆ…๋‹ˆ๋‹ค. (์•„๋‹ˆ, ๋‹ค๋ฅธ ์ด์Šˆ๋ฅผ ๋งŒ๋‚ฌ์„ ๊บผ์•ผ! ) ์ง€๊ธˆ์ด๋ผ๋„ ๋Šฆ์ง€ ์•Š์•˜๊ธฐ ๋•Œ๋ฌธ์— ์ด์   Jetpack์„ ํƒ€๋Ÿฌ ๊ฐ‘๋‹ˆ๋‹ค. ์•ˆ๋…€์—‰~!