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์ ํ๋ฌ ๊ฐ๋๋ค. ์๋
์~!