Lesson 9: App architecture (persistence)

HanbinΒ·2021λ…„ 9μ›” 1일
0

Teach Android Development

λͺ©λ‘ 보기
9/13
post-thumbnail

πŸ’‘ Teach Android Development

κ΅¬κΈ€μ—μ„œ μ œκ³΅ν•˜λŠ” ꡐ윑자료λ₯Ό μ •λ¦¬ν•˜κΈ° μœ„ν•œ ν¬μŠ€νŠΈμž…λ‹ˆλ‹€.

Android Development Resources for Educators

Storing data

Ways to store data in an Android app

  • App-specific storage
    • μ•±μ „μš© 파일
  • Shared storage (files to be shared with other apps)
    • λ‹€λ₯Έ μ•±κ³Ό κ³΅μœ ν•˜λ €λŠ” νŒŒμΌμ„ μ €μž₯
  • Preferences
    • key-value 쌍으둜 μ €μž₯ (SharedPreferences)
  • Databases
    • μ•± μ „μš© λ°μ΄ν„°λ² μ΄μŠ€μ— μ €μž₯

What is a database?

μ‰½κ²Œ μ—‘μ„ΈμŠ€, 검색 및 ꡬ성할 수 μžˆλŠ” κ΅¬μ‘°ν™”λœ 데이터 λͺ¨μŒ. λ‹€μŒκ³Ό 같이 κ΅¬μ„±λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€.

  • Tables (person, car)
  • Rows (Columnsκ°€ μ €μž₯된 ν–‰)
  • Columns (_id, name, make)

Structured Query Language (SQL)

SQL을 μ‚¬μš©ν•˜μ—¬ κ΄€κ³„ν˜• λ°μ΄ν„°λ² μ΄μŠ€μ— μ•‘μ„ΈμŠ€ν•˜κ³  μˆ˜μ •ν•©λ‹ˆλ‹€.

  • Create new tables
  • Query for data
  • Insert new data
  • Update data
  • Delete data

SQLite in Android

λͺ¨λ°”일 κΈ°κΈ°λŠ” ν•˜λ“œμ›¨μ–΄μ™€ 컴퓨터 λŠ₯λ ₯이 μ œν•œμ μ΄κΈ° λ•Œλ¬Έμ— μ•ˆλ“œλ‘œμ΄λ“œμ—μ„œλŠ” SQL ν‘œμ€€μ„ κΈ°λ°˜μœΌλ‘œν•˜λŠ” SQLiteλ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€. SQLiteλŠ” SQL의 λŒ€λΆ€λΆ„μ˜ κΈ°λŠ₯을 μ§€μ›ν•©λ‹ˆλ‹€.

Example SQLite commands

  • CREATE : INSERT INTO colors VALUES ("red", "#FF0000");
  • SELECT : SELECT * from colors;
  • UPDATE : UPDATE colors SET hex="#DD0000" WHERE name="red";
  • DELETE : DELETE FROM colors WHERE name = "red";

Interacting directly with a database

  • μ›μ‹œ SQL 쿼리의 컴파일 νƒ€μž„ 검증을 받지 λͺ»ν•©λ‹ˆλ‹€.
  • λ³€ν™˜ν•˜λ €λ©΄ λ§Žμ€ μ‚¬μš©κ΅¬ μ½”λ“œκ°€ ν•„μš”ν•©λ‹ˆλ‹€.
    • SQL 쿼리 <-> data 객체

Room persistence library

SQLite의 λͺ¨λ“ κΈ°λŠ₯을 ν™œμš©ν•©λ‹ˆλ‹€.

Add Gradle dependencies

dependencies {
  implementation "androidx.room:room-runtime:$room_version"
  kapt "androidx.room:room-compiler:$room_version"

  // Kotlin Extensions and Coroutines support for Room
  implementation "androidx.room:room-ktx:$room_version"

  // Test helpers
  testImplementation "androidx.room:room-testing:$room_version"
}

ROOM

λ°μ΄ν„°λ² μ΄μŠ€μ˜ 데이터 ν‘œν˜„μ„ μ•±μ—μ„œ 직접 μ‚¬μš©ν•  수 μžˆλŠ” 객체둜 λ³€ν™˜ν•˜λŠ” 객체 κ΄€κ³„ν˜• 맀핑 λΌμ΄λΈŒλŸ¬λ¦¬μž…λ‹ˆλ‹€.

Three major components in Room

  • Entitiy : λ°μ΄ν„°λ² μ΄μŠ€ λ‚΄μ˜ ν…Œμ΄λΈ”μ„ λ‚˜νƒ€λƒ…λ‹ˆλ‹€.(ex: Color)
  • DAO : λ°μ΄ν„°λ² μ΄μŠ€μ— μ—‘μ„ΈμŠ€ν•˜λŠ”λ° μ‚¬μš©λ˜λŠ” λ©”μ„œλ“œλ₯Ό ν¬ν•¨ν•©λ‹ˆλ‹€.(ex: ColorDao)
  • Database : 앱에 λ°μ΄ν„°λ² μ΄μŠ€μ— λŒ€ν•œ 연결을 μœ„ν•œ κΈ°λ³Έ μ—‘μ„ΈμŠ€ μ§€μ μž…λ‹ˆλ‹€. (ex: ColoeDatabase)

ColorClass

Color μ €μž₯을 μœ„ν•΄ ν•„μš”ν•œ 클래슀λ₯Ό μ •μ˜ν•©λ‹ˆλ‹€. Room databaseμ—μ„œ μ‚¬μš©ν•˜λ €λ©΄ annotation이 ν•„μš”ν•©λ‹ˆλ‹€.

data class Color {
    val hex: String,
    val name: String
}

Annotations

  • μ»΄νŒŒμΌλŸ¬μ— μΆ”κ°€ 정보λ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€.
    • @Entity, @DAO, @Database
  • λ§€κ°œλ³€μˆ˜λ₯Ό μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
    • @Entity(tableName = "colors")
  • μ–΄λ…Έν…Œμ΄μ…˜μ„ μ •λ³΄λ‘œ μ‚¬μš©ν•˜μ—¬ μžλ™μœΌλ‘œ μ½”λ“œλ₯Ό μƒμ„±ν•©λ‹ˆλ‹€.

Entity

SQLite λ°μ΄ν„°λ² μ΄μŠ€ ν…Œμ΄λΈ”μ— λ§€ν•‘λ˜λŠ” 클래슀.

  • @Entity : ν•΄λ‹Ή ν΄λž˜μŠ€κ°€ μ—”ν‹°ν‹°μž„μ„ μ•Œλ¦½λ‹ˆλ‹€.
  • @PrimaryKey : ν…Œμ΄λΈ”μ˜ κΈ°λ³Έν‚€λ₯Ό μ§€μ •ν•©λ‹ˆλ‹€.
  • @ColumnInfo : 기본적으둜 Property 이름을 μ‚¬μš©ν•˜μ—¬ column이 λ§Œλ“€μ–΄ μ§€μ§€λ§Œ ν•΄λ‹Ή μ–΄λ…Έν…Œμ΄μ…˜μ„ μ‚¬μš©ν•˜μ—¬ λ³€κ²½ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

Example entity

@Entity(tableName = "colors")
data class Color {
    @PrimaryKey(autoGenerate = true) val _id: Int,
    @ColumnInfo(name = "hex_color") val hex: String,
    val name: String
}

Data access object (DAO)

λ°μ΄ν„°λ² μ΄μŠ€μ— μ•‘μ„ΈμŠ€ ν•˜κΈ° μœ„ν•œ 클래슀 μž‘μ—…

  • DAOμ—μ„œ λ°μ΄ν„°λ² μ΄μŠ€ μƒν˜Έμž‘μš©μ„ μ •μ˜ν•©λ‹ˆλ‹€.
  • DAOλ₯Ό μΈν„°νŽ˜μ΄μŠ€, μΆ”μƒν΄λž˜μŠ€λ‘œ μ„ μ–Έν•©λ‹ˆλ‹€.
  • 컴파일 νƒ€μž„μ— DAO κ΅¬ν˜„μ²΄λ₯Ό μƒμ„±ν•©λ‹ˆλ‹€.
  • 컴파일 νƒ€μž„μ— λͺ¨λ“  DAO 쿼리λ₯Ό ν™•μΈν•©λ‹ˆλ‹€.

Example DAO

@Dao
interface ColorDao {

    @Query("SELECT * FROM colors")
    fun getAll(): Array<Color>
    @Insert
    fun insert(vararg color: Color)
    @Update
    fun update(color: Color)
    @Delete
    fun delete(color: Color)

Create a Room database

  • @Database κ³Ό ν•¨κ»˜ μ—”ν‹°ν‹° 리슀트λ₯Ό ν¬ν•¨ν•©λ‹ˆλ‹€.
    @Database(entities = [Color::class], version = 1)
  • 좔상 클래슀인 RoomDatabaseλ₯Ό ν™•μž₯ν•©λ‹ˆλ‹€.
    abstract class ColorDatabase : RoomDatabase()
    • DAOλ₯Ό λ°˜ν™˜ν•˜λŠ” 좔상 λ©”μ„œλ“œλ₯Ό μ„ μ—…ν•©λ‹ˆλ‹€.
      abstract fun colorDao(): ColorDao

Example Room database

RoomDatabase μΈμŠ€ν„΄μŠ€λŠ” 무겁고 단일 ν”„λ‘œμ„ΈμŠ€ λ‚΄μ—μ„œ μ—¬λŸ¬ μΈμŠ€ν„΄μŠ€μ— μ•‘μ„ΈμŠ€ν•  ν•„μš”κ°€ 거의 μ—†κΈ° λ•Œλ¬Έμ— 싱글톀 νŒ¨ν„΄μ„ λ”°λΌμ•Όν•©λ‹ˆλ‹€.

@Database(entities = [Color::class], version = 1)
abstract class ColorDatabase : RoomDatabase() {
    abstract fun colorDao(): ColorDao
    companion object {
        @Volatile
        private var INSTANCE: ColorDatabase? = null
        fun getInstance(context: Context): ColorDatabase {
            ...
        }
    }
    ...

Create database instance

Room.databaseBuilder()λ₯Ό μ‚¬μš©ν•˜μ—¬ λ°μ΄ν„°λ² μ΄μŠ€λ₯Ό λ§Œλ“­λ‹ˆλ‹€.

synchronized : ν•˜λ‚˜μ˜ μ‹€ν–‰ μŠ€λ ˆλ“œλ§Œ 이 μ½”λ“œ 블둝을 μˆ˜ν–‰ν•˜λ―€λ‘œ λ°μ΄ν„°λ² μ΄μŠ€κ°€ ν•œ 번만 μ΄ˆκΈ°ν™”λ©λ‹ˆλ‹€.

fallbackToDestructiveMigration() : λ§ˆμ΄κ·Έλ ˆμ΄μ…˜ κ²½λ‘œκ°€ λˆ„λ½λ˜μ—ˆμ„ λ•Œ κΈ°μ‘΄ 데이터가 μ†μ‹€λ˜λŠ” 것을 ν—ˆμš©ν•  경우.

fun getInstance(context: Context): ColorDatabase {
    return INSTANCE ?: synchronized(this) {
        INSTANCE ?: Room.databaseBuilder(
            context.applicationContext,
            ColorDatabase::class.java, "color_database"
        )
        .fallbackToDestructiveMigration()
        .build()
        .also { INSTANCE = it }
    }
}

Get and use a DAO

λ°μ΄ν„°λ² μ΄μŠ€μ—μ„œ DAOλ₯Ό κ°€μ Έμ˜΅λ‹ˆλ‹€.

val colorDao = ColorDatabase.getInstance(application).colorDao()

μƒˆλ‘œμš΄ Colorλ₯Ό λ§Œλ“€κ³  DAOλ₯Ό μ‚¬μš©ν•˜μ—¬ λ°μ΄ν„°λ² μ΄μŠ€μ— μž…λ ₯ν•©λ‹ˆλ‹€.

val newColor = Color(hex = "#6200EE", name = "purple")
colorDao.insert(newColor)

Asynchronous programming

Long-running tasks

  • 정보 λ‹€μš΄λ‘œλ“œ
  • μ„œλ²„μ™€ 동기화
  • νŒŒμΌμ— μ“°κΈ°
  • 무거운 계산
  • λ°μ΄ν„°λ² μ΄μŠ€μ—μ„œ μ½κ±°λ‚˜ μ“°κΈ°

μž₯κΈ° μž‘μ—…μ„ μˆ˜ν–‰ν•˜λŠ” 경우 κΈ°λ³Έ μŠ€λ ˆλ“œμ—μ„œ μˆ˜ν–‰ν•˜λ©΄ μ•ˆλ©λ‹ˆλ‹€. κΈ°λ³Έ μŠ€λ ˆλ“œμ—μ„œ μˆ˜ν–‰ν•  경우 앱이 μ‚¬μš©μžμ—κ²Œ μ‘λ‹΅ν•˜μ§€ μ•Šμ„ 수 μžˆμŠ΅λ‹ˆλ‹€.

Need for async programming

  • μž‘μ—…μ„ μˆ˜ν–‰ν•˜κ³  응닡을 μœ μ§€ν•˜λŠ”λ° μ œν•œλœ μ‹œκ°„.
  • μž₯κΈ° μž‘μ—…μ„ μ‹€ν–‰ν•΄μ•Ό ν•˜λŠ” ν•„μš”μ„±κ³Ό κ· ν˜•.
  • μž‘μ—… μ‹€ν–‰ 방법 및 μœ„μΉ˜ μ œμ–΄.

Async programming on Android

  • Threading
  • Callbacks
  • κ·Έμ™Έ λ§Žμ€ μ˜΅μ…˜

Coroutines

μ•ˆλ“œλ‘œμ΄λ“œ 비동기 ν”„λ‘œκ·Έλž˜λ°μ— ꢌμž₯λ˜λŠ” λ°©λ²•μž…λ‹ˆλ‹€.

  • μž₯κΈ° μ‹€ν–‰ μž‘μ—…μ„ κ΄€λ¦¬ν•˜λŠ” λ™μ•ˆ μ•±μ˜ 응닡성을 μœ μ§€ν•©λ‹ˆλ‹€.
  • μ•ˆλ“œλ‘œμ΄λ“œ μ•±μ—μ„œ 비동기 μ½”λ“œλ₯Ό λ‹¨μˆœν™”ν•©λ‹ˆλ‹€.
  • μ½”λ“œλ₯Ό 순차적으둜 μž‘μ„±ν•©λ‹ˆλ‹€.
  • try/catch λΈ”λ‘μœΌλ‘œ μ˜ˆμ™Έ 처리λ₯Ό μ§„ν–‰ν•©λ‹ˆλ‹€.

Benefits of coroutines

  • κ²½λŸ‰
  • λ©”λͺ¨λ¦¬ λˆ„μˆ˜ κ°μ†Œ
  • cancellation λ‚΄μž₯ 지원
  • Jetpack integration
    • Jetpack λΌμ΄λΈŒλŸ¬λ¦¬μ—λŠ” 전체 μ½”νˆ¬ν‹΄μ„ μ§€μ›ν•˜λŠ” ν™•μž₯ κΈ°λŠ₯이 ν¬ν•¨λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€.

Suspend functions

  • suspend modifier μΆ”κ°€
  • λ‹€λ₯Έ suspend ν•¨μˆ˜ λ˜λŠ” coroutines에 μ˜ν•΄ ν˜ΈμΆœλ˜μ–΄μ•Ό ν•©λ‹ˆλ‹€.
suspend fun insert(word: Word) {
    wordDao.insert(word)
}

Suspend and resume

  • suspend : ν˜„μž¬ μ½”λ£¨ν‹΄μ˜ 싀행을 μΌμ‹œ μ€‘μ§€ν•˜κ³  지역 λ³€μˆ˜λ₯Ό μ €μž₯ν•©λ‹ˆλ‹€.

  • resume : μ €μž₯된 μƒνƒœλ₯Ό μžλ™μœΌλ‘œ λ‘œλ“œν•˜κ³  μΌμ‹œ μ€‘λ‹¨λœ 지점뢀터 μ‹€ν–‰ν•©λ‹ˆλ‹€.

코루틴이 suspend ν•¨μˆ˜λ₯Ό ν˜ΈμΆœν•˜λ©΄ ν•΄λ‹Ή ν•¨μˆ˜κ°€ 일반 ν•¨μˆ˜ 호좜처럼 λ°˜ν™˜λ  λ•ŒκΉŒμ§€ μ°¨λ‹¨ν•˜λŠ” 것이 μ•„λ‹ˆλΌ κ²°κ³Όκ°€ 쀀비될 λ•ŒκΉŒμ§€ 싀행을 μΌμ‹œ μ€‘λ‹¨ν•œ λ‹€μŒ 결과와 ν•¨κ»˜ μ€‘λ‹¨λœ κ³³μ—μ„œ λ‹€μ‹œ μ‹œμž‘ν•©λ‹ˆλ‹€.

κ²°κ³Όλ₯Ό κΈ°λ‹€λ¦¬λŠ” λ™μ•ˆ μŠ€λ ˆλ“œμ˜ 차단을 ν•΄μ œν•˜μ—¬ λ‹€λ₯Έ κΈ°λŠ₯μ΄λ‚˜ 코루틴을 μ‹€ν–‰ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

suspend 와 resume μž‘μ—…μ„ ν•¨κ»˜ μ‚¬μš©ν•˜μ—¬ μ½œλ°±μ„ λŒ€μ²΄ν•©λ‹ˆλ‹€.

Add suspend modifier to DAO methods

Room은 코루틴을 μ§€μ›ν•©λ‹ˆλ‹€. suspend μˆ˜μ •μžλ₯Ό μΆ”κ°€ ν•˜μ—¬, μ½”λ£¨ν‹΄μ΄λ‚˜ λ‹€λ₯Έ suspend ν•¨μˆ˜μ—μ„œ ν˜ΈμΆœν•  수 있게 ν•©λ‹ˆλ‹€.

@Dao
interface ColorDao {

    @Query("SELECT * FROM colors")
    suspend fun getAll(): Array<Color>
    @Insert
    suspend fun insert(vararg color: Color)
    @Update
    suspend fun update(color: Color)
    @Delete
    suspend fun delete(color: Color)

Control where coroutines run

withContext

withContextλ₯Ό μ‚¬μš©ν•˜μ—¬ 감싸진 μ½”λ“œλ₯Ό μ‹€ν–‰ν•  λ””μŠ€νŒ¨μ³λ₯Ό 지정할 수 μžˆμŠ΅λ‹ˆλ‹€.

suspend fun get(url: String) {

	// Start on Dispatchers.Main

    withContext(Dispatchers.IO) {
        // Switches to Dispatchers.IO
        // Perform blocking network IO here
    }

    // Returns to Dispatchers.Main
}

CoroutineScope

코루틴은 CoroutineScopeμ—μ„œ μ‹€ν–‰λ˜μ–΄μ•Ό ν•©λ‹ˆλ‹€.

  • κ·Έ μ•ˆμ—μ„œ μ‹€ν–‰λœ λͺ¨λ“  코루틴을 μΆ”μ ν•©λ‹ˆλ‹€.
  • μŠ€μ½”ν”„μ—μ„œ 코루틴을 μ·¨μ†Œν•˜λŠ” 방법을 μ œκ³΅ν•©λ‹ˆλ‹€
  • 일반 ν•¨μˆ˜μ™€ 코루틴 μ‚¬μ΄μ˜ bridgeλ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€.

Examples:

  • GlobalScope
  • viewModelScope
  • lifecycleScope

Start new coroutines

  • launch : κ²°κ³Όκ°€ ν•„μš”μ—†μ„ 경우
fun loadUI() {
    launch {
        fetchDocs()
    }
}
  • async : κ²°κ³Όκ°€ ν•„μš”ν•  경우
    • asyncλ₯Ό μ‚¬μš©ν•˜λ©΄ 코루틴을 μ‹œμž‘ν•˜κ³  await ν‚€μ›Œλ“œλ‘œ 값을 λ°˜ν™˜ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

ViewModelScope

  • ViewModel이 μ§€μ›Œμ§€λ©΄ μžλ™μœΌλ‘œ μ·¨μ†Œλ©λ‹ˆλ‹€.
  • ViewModel이 ν™œμ„±ν™”λœ 경우 μˆ˜ν–‰ν•΄μ•Ό ν•˜λŠ” μž‘μ—…μ΄ μžˆμ„ λ•Œ μœ μš©ν•©λ‹ˆλ‹€.
class MyViewModel: ViewModel() {

    init {
        viewModelScope.launch {
            // Coroutine that will be canceled
            // when the ViewModel is cleared
        }
    }
    ...

Example viewModelScope

class ColorViewModel(val dao: ColorDao, application: Application)
    : AndroidViewModel(application) {

fun save(color: Color) {
    viewModelScope.launch {
        colorDao.insert(color)
    }
}
 
...

Testing databases

Add Gradle dependencies

android {
    defaultConfig {
        ...
        testInstrumentationRunner "androidx.test.runner
         .AndroidJUnitRunner"
        testInstrumentationRunnerArguments clearPackageData: 'true'
    }
}
dependencies {
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
}

Testing Android code

  • @RunWith(AndroidJUnit4::class)
  • @Before
  • @After
  • @Test

Create test class

ν…ŒμŠ€νŠΈμ— ν•„μš”ν•œ DAO와 λ°μ΄ν„°λ² μ΄μŠ€λ₯Ό μ •μ˜ν•©λ‹ˆλ‹€.
λ‚˜μ€‘μ— μ‚¬μš©ν•  Color μΈμŠ€ν„΄μŠ€λ₯Ό 생성해 μ€λ‹ˆλ‹€.

@RunWith(AndroidJUnit4::class)
class DatabaseTest {

    private lateinit val colorDao: ColorDao
    private lateinit val db: ColorDatabase

    private val red = Color(hex = "#FF0000", name = "red")
    private val green = Color(hex = "#00FF00", name = "green")
    private val blue = Color(hex = "#0000FF", name = "blue")

    ...

Create and close database for each test

@Before : ν…ŒμŠ€νŠΈ 이전에 μˆ˜ν–‰ν•  ν–‰λ™μœΌλ‘œ λ°μ΄ν„°λ² μ΄μŠ€μ™€ DAO μΈμŠ€ν„΄μŠ€λ₯Ό μƒμ„±ν•©λ‹ˆλ‹€.
(μ‹€μ œ μ‚¬μš©μž 데이터λ₯Ό μœ μ§€ν•˜κΈ° μœ„ν•΄ 더 이상 μ‚¬μš©ν•˜μ§€ μ•Šμ„ λ•Œ νκΈ°λ˜λŠ” ν…ŒμŠ€νŠΈ λͺ©μ μœΌλ‘œ inMemoryDatabaseBuilderλ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€.)

@After : ν…ŒμŠ€νŠΈ μ™„λ£Œλœ ν›„ μˆ˜ν–‰λ©λ‹ˆλ‹€.

@Before
fun createDb() {
    val context: Context = ApplicationProvider.getApplicationContext()
    db = Room.inMemoryDatabaseBuilder(context, ColorDatabase::class.java)
        .allowMainThreadQueries()
        .build()
    colorDao = db.colorDao()
}
@After
@Throws(IOException::class)
fun closeDb() = db.close()

Test insert and retrieve from a database

    @Test
    @Throws(Exception::class)
    fun insertAndRetrieve() {
        colorDao.insert(red, green, blue)
        val colors = colorDao.getAll()
        assert(colors.size == 3)
    }

0개의 λŒ“κΈ€