[Android] App architecture(persistence)

이도연·2024년 2월 4일
0

android studio

목록 보기
25/28

Storing data

메모리에 저장된 데이터를 앱에 표시했다. 기기의 local storage 에 데이터를 저장한다면 더욱 유용할 것이다.

Android app 에서 데이터 저장하기

App-specific storage

앱에서만 사용할 수 있는 파일을 저장한다. ex) 구조화된 데이터 파일(JSON), 일반 텍스트 파일, 미디어 파일

Shared storage (files to be shared with other apps)

다른 앱과 공유하려는 파일을 저장(미디어 또는 문서 등)

Preferences

개인 기본 데이터를 키-값 쌍으로 저장한다. (SharedPreference)

Databases

구조화된 데이터를 앱 전용 DB 에 저장한다.



database

DB(database) : 쉽게 접근하고, 검색하고, 정리할 수 있는 구조화된 데이터의 집합이다. 데이터는 테이블에 저장되고, 각 테이블에는 관련된 정보 필드가 있다.

  • Tables (테이블)
  • Rows (행)
  • Columns (열)

각 테이블은 행을 가질 수 있고, 각 행은 하나의 항목을 나타내며, 열은 각 행에 대해 보유된 데이터의 유형을 설명합니다.
사람 테이블의 하나의 행은 이름, 나이, 이메일 주소를 가진 한 사람을 나타내고, 자동차 테이블의 하나의 행은 제조사, 모델, 연식이 있는 한 대의 자동차를 나타낸다.

각 테이블에는 테이블의 각 행에 대한 고유 식별자를 제공하는 _id 가 있다.

데이터베이스의 구조(표, 행 및 열 포함)가 데이터 간의 관계 및 제약 조건을 설정할 수 있는 기능을 제공하기 때문에 이는 관계형 데이터베이스이다.



Structured Query Language (SQL)

관계형 데이터베이스와 상호 작용하기 위해 DB의 도메인별 언어인 SQL(또는 구조화된 쿼리 언어)을 사용한다.

Query : 데이터베이스에서 데이터를 조회하거나 조작하기 위해 사용되는 명령어의 집합. 주로 SQL이라는 언어를 사용하여 데이터베이스 쿼리를 작성한다.

SQL에는 많은 기능이 있다.

  • 데이터베이스에서 테이블을 생성
  • 데이터 조회
  • 데이터 삽입 및 업데이트
  • 데이터베이스에서 데이터를 삭제
    등등..



SQLite in Android

모바일 기기는 하드웨어와 컴퓨팅 능력에 한계가 있기 때문에 안드로이드는 SQL 표준을 기반으로 하는 SQLite를 사용한다. SQLite는 SQL의 대부분의 기능을 지원한다.

SQLite : Android 에서 SQLite 는 경량의 관계형 DB 관리 시스템(RDBMS). Android Application 에서 내부 데이터 저장 및 관리에 주로 사용된다.
SQLite 는 서버 없이 로컬에서 사용할 수 있으며, Android App 에서는 주로 구조화된 데이터를 저장하고 검색하기 위한 목적으로 사용된다.



Example SQLite commands

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

위는, 색상 이름과 16진수 색상 코드를 보유하는 색상이라는 데이터베이스 테이블에서 작동하기 위한 명령어의 예시이다.
색상 테이블에 새 행을 삽입하고 테이블에서 기존 데이터를 읽고 행을 업데이트할 수 있으며, 주어진 기준과 일치하는 행도 삭제할 수 있다.

이러한 Create-Read-Update-Delete 작업은 CRUD 작업이라고도 하며, 이러한 작업은 데이터베이스와 가장 일반적인 상호 작용이다.

대문자로 표기된 단어는 SQLite 키워드(예: INSERT, UPDATE, DELETE, VALUES, WHERE 등)이므로 이러한 이름을 테이블 이름이나 열 이름에 사용할 수 없다.

SQLite : Android 에서 SQLite 는 경량의 관계형 DB 관리 시스템(RDBMS). Android Application 에서 내부 데이터 저장 및 관리에 주로 사용된다.
SQLite 는 서버 없이 로컬에서 사용할 수 있으며, Android App 에서는 주로 구조화된 데이터를 저장하고 검색하기 위한 목적으로 사용된다.



  1. SELECT 문
  • SELECT 문은 데이터베이스에서 데이터를 조회할 때 사용
  • "users" 테이블에서 모든 사용자의 이름을 선택하는 간단한 쿼리
  • 이 쿼리는 "users" 테이블에서 각 행의 "name" 열의 값을 선택
SELECT name FROM users;
  1. WHERE 절
  • WHERE 절은 특정 조건을 만족하는 행만 선택할 때 사용
  • 나이가 25세 이상인 사용자의 이름을 선택하는 쿼리는 다음과 같다.
SELECT name FROM users WHERE age >= 25;
  1. INSERT 문
  • INSERT 문은 데이터베이스에 새로운 데이터를 추가할 때 사용
  • "users" 테이블에 새로운 사용자를 추가하는 쿼리
  • 이 쿼리는 "users" 테이블에 이름이 'John Doe'이고 나이가 30인 사용자를 추가
INSERT INTO users (name, age) VALUES ('John Doe', 30);
  1. UPDATE 문
  • UPDATE 문은 이미 있는 데이터를 수정할 때 사용됩니다.
  • "users" 테이블에서 이름이 'Alice'인 사용자의 나이를 28로 수정하는 쿼리
UPDATE users SET age = 28 WHERE name = 'Alice';
  1. DELETE 문
  • DELETE 문은 데이터베이스에서 특정 행을 삭제할 때 사용됩니다.
  • "users" 테이블에서 이름이 'Bob'인 사용자를 삭제하는 쿼리는 다음과 같습니다.
DELETE FROM users WHERE name = 'Bob';



Interacting directly with a database

  • 원시의 SQL 쿼리에 대한 컴파일을 확인할 수 없다.
  • SQL 쿼리 <-> 데이터 개체를 변환하기 위해 많은 코드가 필요

안드로이드는 앱에서 직접 SQLite 데이터베이스를 생성하고 관리하는 데 도움이 되는 API 를 제공한다. 데이터베이스와 직접 상호 작용하는 것은 편리하지만, 실수를 방지하기 위해서 주의가 필요하다.
예를 들어, 원래의 SQL 쿼리에 대한 컴파일은 검증할 수 없으며, SQL 쿼리와 데이터 개체 간 변환을 위해 많은 코드를 작성해야 한다.

Android Jetpack은 앱에 있는 데이터베이스와 더 쉽게 상호 작용할 수 있는 Room persistence library 를 제공한다.



Room persistence library

Room persistence library 는 SQLite 위에 추상화 계층을 제공하여 SQLite의 모든 기능을 활용하면서 보다 강력한 데이터베이스 액세스를 가능하게 한다.

Add Gradle depencencies

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"
}

Kotlin 기반 앱의 경우 kapt를 사용하고, kotlin-kapt 플러그인을 추가해야 합니다('kotlin-kapt').

Room

Room 은 데이터베이스의 데이터를 (앱 코드에서 직접 조작할 수 있는) 객체로 변환하는 객체 관계형 매핑 라이브러리(object relational mapping library) 이다.
또한 객체의 데이터를 다시 데이터베이스로 밀어 넣으면서 프로세스를 되돌릴 수도 있다.



Room 에는 크게 세 가지 구성요소가 있다.

Entity : 데이터베이스 내의 테이블
DAO : 데이터베이스 액세스에 사용되는 메서드를 포함
데이터베이스(Database) : 데이터베이스 홀더를 포함, 앱의 지속적인 관계 데이터에 대한 기본적인 연결을 위한 주요 액세스 포인트

Entity 또는 DAO 클래스의 수에는 제한이 없지만 데이터베이스 내에서 고유해야 한다.



ColorValue app

ColorValue app 에서 색상 정보를 저장하려면, 그 정보를 담을 수 있는 표(database table)가 필요하다.
ColorValue app 에서 Color, ColorDao, ColorDatabase 세 가지 클래스가 필요하다.



Color class

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

먼저 색상 이름과 16진수 색상 값을 포함하여 특정 색상에 대한 데이터를 저장할 색상 클래스를 정의한다. 데이터 클래스는 필수 사항이 아니다. 그러나 Room database 에 연결하려면, 해당 클래스에 주석을 달아야한다.



Annotations

Android Room 은 Android 앱에서 SQLite 데이터베이스를 쉽게 사용할 수 있도록 돕는 라이브러리이다.
프로그래밍 언어에서 주석은, 코드에 메타데이터를 첨부하며 컴파일러에 추가 정보를 제공한다.
Room 은 데이터베이스 작업을 처리하는 데 도움을 주는 몇 가지 주요 주석(Annotations)을 제공한다.

  • @Entity : 데이터베이스의 테이블을 나타내는 클래스에 사용. 예를 들어 @Entity(tableName = "colors")는 "colors"라는 이름의 테이블을 나타내는 엔터티 클래스를 정의한다.
  • @DAO : 데이터베이스 엑세스 메서드를 정의한다. 개발자가 직접 코드를 작성하지 않아도 필요한 코드를 자동으로 생성한다.
  • @Database : 데이터베이스를 나타내는 클래스에 사용된다. 이 클래스는 데이터베이스의 버전, 엔터티 클래스 등을 정의하며, 데이터베이스와 관련된 작업을 수행.



Entity

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

클래스에 @Entity 주석을 추가하여 tableName 을 정의한다. 이 주석은, 이 표가 되는 클래스에 달아준다. 즉, 해당 클래스가 database table 을 나타낸다는 걸 알려준다. 'tableName = "colors" 는 이 테이블의 이름을 "colors" 로 정하는 것이다.

@PrimaryKey 를 사용하여, 열을 테이블의 기본 키로 표시한다.
이 주석은 표의 각 행을 고유하게 식별하는 열을 지정한다. '_id' 라는 이름의 정수 열을 사용하고, 이 열에는 각 행마다 자동으로 생성되는 고유한 번호가 들어간다. Room 이 Entity 에 자동 ID 를 할당하려면 @PrimatyKey의 autoGenerate 속성을 true 로 설정한다.

@ColumnInfo 주석을 hex 변수에 추가하여, 원하는 열 이름을 전달한다. 이 열의 이름을 "hex_color"로 설정한다.



Data access object (DAO)

  • DAO에서 데이터베이스 상호 작용을 정의
  • DAO를 인터페이스 또는 추상 클래스로 선언
  • Room은 컴파일 시 DAO 구현을 작성
  • Room은 컴파일 시 모든 DAO 쿼리를 확인

Room persistence library 를 사용하여 앱의 데이터에 액세스하려면 데이터 액세스 개체 또는 DAO를 사용한다. 직접 쿼리를 사용하는 대신 DAO 클래스에서 사용할 데이터베이스 상호 작용을 정의한다.

DAO 를 사용하면, 앱을 컴파일할 때 Dao 클래스에서 모든 쿼리를 확인하므로, 쿼리 중 하나에 문제가 있으면 즉시 알림을 받을 수 있다.



@Dao
interface ColorDao {
	// "colors" 라는 DB 테이블에서 모든 정보 가져오기
    // 색 상자에서 모든 색을 꺼냄.
    @Query("SELECT * FROM colors")
    fun getAll(): Array<Color>
   
    // DB 에 정보 추가
    // 새로운 색을 찾았을 때, 해당 명령어로 그 정보를 DB 에 추가
    @Insert
    fun insert(vararg color: Color)
    
    // 이미 있는 정보를 업데이트
    // 기존 색 이름이나 색 코드를 수정할 때 사용
    @Update
    fun update(color: Color)
    
    // DB 에서 특정 정보 삭제
    // 더 이상 필요없는 색 정보를 지우고 싶을때 사용
    @Delete
    fun delete(color: Color)



Query

@Dao
interface ColorDao {

	// DB 에서 모든 정보 가져오는 명령어
    // colors: 테이블 이름, *: 모든 열 
    @Query("SELECT * FROM colors")
    fun getAll(): Array<Color>

	// 함수에 전달된 이름과 일치하는 색상 정보를 가져오는 명령어
    // name: 함수에 전달된 이름
    @Query("SELECT * FROM colors WHERE name = :name")
    fun getColorByName(name: String): LiveData<Color>

	// 특정 헥스 코드 전달 시 해당 헥스 코드와 일치하는 색상 정보 가져옴
    // hex_color 가 주어진 조건과 일치하는 색상 정보를 가져옴.
    @Query("SELECT * FROM colors WHERE hex_color = :hex")
    fun getColorByHex(hex: String): LiveData<Color>



database

DAO가 선언된 상태에서 Room 데이터베이스를 작성할 수 있다.
@Database 로 클래스에 주석을 달고 엔티티 목록을 포함시킨다. @Database 주석의 엔티티 속성은 이 데이터베이스에 저장될 개체를 선언한다.

이것을 RoomDatabase에서 확장된 추상 클래스(ColorDatabase)로 선언한다.
클래스 내에 인수를 사용하지 않고 @Dao로 주석이 달린 클래스를 반환하는 추상 메서드를 포함한다. Room.DatabaseBuilder 또는 Room.inMemoryDatabaseBuilder를 통해 ColorDatabase 클래스를 구현합니다.



Room database

@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 {
            ...
        }
    }
    ...

각 room database instance 는 상당히 비용이 많이 들고 단일 프로세스에서 여러 인스턴스에 엑세스할 필요가 거의 없다. 그래서, 앱이 단일 프로세스에서 실행되는 경우, database 개체를 인스턴스화할 때 싱글톤 디자인 패턴을 따라야 한다. (= 이 데이터베이스를 사용할 때마다 새로운 데이터베이스를 만들기보다는 하나의 특별한 데이터베이스만 사용하는 것이 효율적)
이를 위해서 ColorDatabase 클래스에서는 특별한 방법을 사용하여 하나의 데이터베이스 인스턴스만을 만들게 되어있다.

이렇게 하면 데이터 무결성(예: 데이터베이스 인스턴스에 대한 변경 사항)이 다른 모든 스레드에서 즉시 확인할 수 있으며, 예를 들어 두 스레드가 캐시에서 동일한 엔티티를 업데이트하는 것을 방지할 수 있습니다.

Volatile 키워드를 사용하면, INSTANCE 변수를 휘발성으로 만드는 이유는 해당 값이 캐시되지 않고 모든 읽기 및 쓰기 작업이 메인 메모리에서 직접 수행된다. 간단히 말하면 모든 일을 메인 메모리에서 바로 처리하도록 할 수 있다. 여러 스레드가 동시에 작업을 할 때, 데이터베이스에 저장된 정보가 서로 꼬이거나, 두 스레드가 캐시에서 동일한 엔티티를 업데이트 하는 등의 혼동을 @Volatile을 사용하여 이를 방지할 수 있다.

즉, 이 코드는 하나의 저장소(데이터베이스)를 만들고, 여러 사람(스레드)이 함께 사용할 때 정보가 꼬이지 않도록 한다.



Database instance

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

Room.databaseBuilder()를 사용하여 응용프로그램 컨텍스트, ColorDatabase 클래스 및 데이터베이스 이름을 사용하여 데이터베이스를 만든다.

synchronized(동기화): 여러 스레드가 잠재적으로 동시에 데이터베이스 인스턴스를 요청할 수 있으므로 하나가 아닌 두 개 이상의 데이터베이스가 생성된다. 데이터베이스가 동기화되도록 코드를 래핑한다는 것은 한 번에 하나의 실행 스레드만 이 코드 블록에 입력할 수 있음을 의미한다. 즉 데이터베이스가 한 번만 초기화되도록 보장한다.

마이그레이션 경로가 없을 때 기존 데이터를 손실해도 상관없을 때가 있다.데이터베이스를 만들 때 fallbackToDestructiveMigration() builder 메서드를 호출한다. 정의된 마이그레이션 경로가 없는 경우 앱의 데이터베이스에서 테이블을 파괴적으로 재생성하도록 Room에 알린다.



Get and use a DAO

데이터베이스에 몇 가지 데이터를 추가한다면..

Get the DAO from the database

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

먼저 getInstance() 메서드를 사용하여 colorDao라는 DAO 인스턴스를 생성하여 싱글톤 인스턴스를 제공한다.


Create new Color and use DAO to insert it into database

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

이제 color 인스턴스 newColor를 추가하고 insert() 메서드를 사용하여 DAO 인스턴스에 추가하면, 데이터베이스에 정의된 newColor가 추가된다.

이제 DAO 개체를 사용하여 데이터베이스를 삽입, 업데이트, 검색 또는 쿼리할 수 있다.



Asynchronous programming

Long-running tasks

  • Download information
  • Sync with a server
  • Write to a file
  • Heavy computation
  • Read from, or write to, a database

위와 같이 오래 실행되는 작업을 수행해야 하는 경우, 메인 스레드에서 작업을 수행할 시 사용자에게 응답하지 않을 수 있다. 이것은 비동기적으로 작업을 수행해야 한다.

Need for async programming

  • Limited time to do tasks and remain responsive
  • Balanced with the need to execute long-running tasks
  • Control over how and where tasks are executed

비동기 프로그래밍의 필요성은 세 가지로 요약된다.

작업을 실행하는 데 사용할 수 있는 시간이 제한되어 있다.
60fps 리프레시 속도를 달성하려면 UI 프레임을 16ms 미만으로 렌더링해야 한다. 앱의 UI 렌더링이 그보다 느리면 시스템은 프레임을 건너뛸 수 밖에 없어 "jank"라고 말더듬을 유발한다.

일부 작업을 완료하는 데 시간이 오래 걸린다.
코드를 최적화할 수 있지만 일부 작업은 처리를 완료하는 데 시간이 오래 걸린다.

작업이 실행되는 방법과 위치를 모두 제어할 수 있어야 한다.
예를 들어, 작업이 오류를 전파하거나 실행을 중단하는 상황을 제어할 수 있어야 한다.



Async programming on Android

  • Threading
  • Callbacks
  • Plus many other options

비동기적으로 작업하는 방법 중 하나는 스레드를 사용하는 것이다.
스레드는, 코드를 메인 스레드에서 실행하기 위해 실행 단위를 생성한다. 그러나 운영 체제가 스레드에 대한 메모리를 생성하고 할당해야 하기 때문에, 새로운 스레드를 회전시키는 데 많은 시간이 소요된다.

레고 블록으로 무언가를 만들 떄, 여러 명의 친구들과 함께 작업하면 더 빨리 끝낼 수 있겠지만, 모든 일을 한 명이 다 하는 것보다는 느리게 될 수도 있다.

이때, "스레드(thread)"라는 개념이 나타난다. (스레드 = 친구1명)
여러 명이 동시에 일을 처리할 수 있겠지만, 새로운 스레드를 만들려면 블록을 더 만들어야 하고, 그 블록을 위한 자원(메모리)를 할당해야 한다. 결론적으로 일이 빨라지긴 하지만, 새로운 블록을 만드는 데 시간이 좀걸리기 때문에 총 소요시간이 똑같다.

스레드의 또 다른 문제는 많은 필요한 작업이 동시에 수행되지만 동시에 수행되지 않거나 병렬적일 수 없다는 것이다. 예를 들어, 데이터베이스에서 주문을 로드해야 하는 경우 필요한 모든 데이터를 가져오려면 3개의 요청이 필요하다. 각각의 스레드를 사용할 수 있지만 요청 2와 3이 요청 1에 종속되어 있다면 의존성은 유지되어야 하며, 이는 서로 차단되어야 한다.

일부 작업은 동시에 할 수 있지만, 다른 일들은 순차적으로 해야 하는 경우가 있다. 예를 들면, 어떤 정보를 얻기 위해 여러 번 요청을 해야 할 때, 첫 번째 요청의 결과를 받기 전에 두 번째와 세 번째 요청을 하면 문제가 생길 수 있다.

해결책인 비동기 프로그래밍(asynchronous programming)은 블록을 만드는 대신, 각각의 작업이 끝날 때마다 알려주는 방식이다. 이렇게 하면 모든 작업이 동시에 일어나지 않더라도 효율적으로 일을 처리할 수 있다. 하지만 이 방법도, 많은 작업이 중첩 시 관리하기 힘들어지고, 에러 처리도 어려워진다.

콜백을 사용할 수도 있다. 하지만, 중첩된 콜백은 관리가 어려워지고 오류 처리가 어려워진다.

이외에 사용할 수 있는 다른 옵션들이 많이 있지만 각각 단점과 어려움이 뒤따른다.



Coroutines

  • Keep your app responsive while managing long-running tasks.
  • Simplify asynchronous code in your Android app.
  • Write code in sequential way
  • Handle exceptions with try/catch block

코루틴을 사용하는 것은 안드로이드 비동기 프로그래밍에 권장되는 솔루션이다.
안드로이드에서 비동기적으로 실행되는 코드를 단순화하기 위해 사용할 수 있는 동시성 디자인 패턴이다.
메인 스레드를 차단하여, 앱이 응답하지 않게 할 수 있는 장시간 실행 작업을 관리하는 데 도움이 된다.
Kotlin 1.3 에 추가되었으며, 다른 언어의 기존 개념을 기반으로 한다.

Benefits

  • Lightweight
  • Fewer memory leaks
  • Built-in cancellation support
  • Jetpack integration



Suspend and Resume

suspend fun insert(word: Word) {
	wordDao.insert(word)
}

suspend modifier 를 사용하여, 해당 함수를 코루틴이 사용할 수 있는 것으로 선언한다. 함수는 코루틴 내에서 또는 다른 정지 함수에서만 호출할 수 있다. (중단 기능은 Gutter 아이콘)

suspend : 현재 코루틴 실행을 일시 중지하고 로컬 변수 저장
resume : 저장 상태를 자동으로 로드하고, 코드가 일시 중단된 시점부터 실행을 계속함.

코루틴이 정상적인 함수 호출처럼 함수가 돌아올 때까지 차단하는 대신, suspend modifier 가 표시된 함수를 호출하면 결과가 준비될 때까지 실행을 중지하고 결과와 함께 중단된 위치에서 다시 시작한다. 결과를 기다리는 동안 중단된 함수는 실행 중인 스레드를 차단하여, 다른 함수나 코루틴이 실행할 수 있도록 한다.

suspend 는, 많은 동시 작업을 지원하면서 메모리를 절약하고, 콜백을 대체하기 위해 작업을 일시 중지했다가 다시 시작한다.



Add suspend modifier to DAO methods

@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)

Room 에는 코루틴이 지원된다. DAO 메서드에서 suspend modifier 를 추가하여 코루틴 내에서만 호출하거나 다른 suspend function 에서 호출할 수 있다.



Control where coroutines run

백그라운드 스레드에서는 suspend 를 사용해서 코루틴을 실행할 수 없다. (메인 스레드에서 실행)
예를 들어, UI 이벤트에 응답하여 메인 스레드에서 코루틴을 실행할 수 있으며, 장기간 실행되는 작업을 수행하지 않으면 UI 가 즉시 업데이트될 수 있다.

Kotlin 에는 수행 중인 작업에 따라 코루틴이 실행되어야 하는 위치를 지정하는 데 사용할 수 있는 여러 dispatcher 가 있다. (Dispatchers.Main, Dispatchers.IO, Dispatchers.Default)



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
}

withContext 키워드를 사용 시, 어떤 dispatcher 에서 실행되어야 하는지 지정할 수 있다.
이 경우 withContext(Dispatchers.IO)는 코루틴의 실행을 I/O(입출력 작업) 스레드로 이동한다. withContext 또한 suspend 함수이므로, 메인 스레드에서 호출되더라도 안전하게 사용할 수 있다. (main-safe 기능이며 필요에 따라 UI 업데이트). 비유하자면, 마치 학교(I/O)에 갔다가 다시 집(메인 스레드)로 돌아오는 것과 비슷하다. withContext 블록이 완료되는 대로 메인 스레드의 코루틴이 재개된다.

간단하게 말하면, withContext 는 현재 하는 일의 환경(dispatcher)를 바꾸는 도구이며, 작업을 안전하게 처리하도록 돕는다.



CoroutineScope

Coroutine 은 CoroutineScope 에서 실행해야 한다.

  • 시작된 모든 코루틴(일시 중단된 코루틴 포함)을 추적
  • Scope 에서 코루틴을 취소하는 방법을 제공
  • 정규 함수와 코루틴 사이의 중개역할 제공

Kotlin 은 CoroutineScope 없이는 새로운 코루틴을 시작할 수 없다.
CoroutineScope 는 모든 코루틴을 추적하고, 그것은 그 안에서 시작된 모든 코루틴을 취소시킬 수 있다.



Start new Coroutines

  • launch : no result needed
  • async : can return a result
fun loadUI() {
    launch {
        fetchDocs()
    }
}

launch 는 주어진 범위에서 값을 반환하지 않는 새로운 코루틴 작업을 생성한다. async 를 사용하면 코루틴을 시작하고 wait 키워드로 값을 반환할 수 있다.

위 예시는, 기본 코루틴 스코프에서 launch 키워드를 통해, 새로운 코루틴을 시작한다. fetchDocs 같은 suspend 함수를 호출할 수 있다.



ViewModelScope

class MyViewModel: ViewModel() {

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

ViewModelScope 는 앱의 각 ViewModel 에 대해 정의되어 있다. ViewModel의 ViewModelScope 속성을 통해 ViewModel의 CoroutineScope에 액세스할 수 있다. ViewModel을 지운 경우 이 범위에서 시작된 모든 Coroutine은 자동으로 취소된다.

-> Coroutine은 ViewModel이 활성화된 경우에만 수행해야 하는 작업이 있을 때 유용하다. 예를 들어 레이아웃에 대한 데이터를 가져오는 경우 ViewModel로 작업을 범위를 지정했을 때, ViewModel을 지운 경우 불필요한 리소스 소비를 방지해야 합니다.

Example viewModelScope

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

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

ViewModel에서는 DAO와 상호 작용할 수 있다.(ViewModel이 DB 에 엑세스하고 데이터를 읽거나 쓸 수 있다).

ViewModel 은 UI 와 관련된 데이터나 로직을 가진다. 이는 activity 나 fragment 같은 UI 컨트롤러에서 분리된 구성요소

DAO 는 DB 와 직접 상호작용하는 객체이다. DB 에서 데이터를 읽고 쓰는 기능 제공

예를 들어, 유저가 앱에서 새로운 데이터를 추가하려고 할 때, ViewModel 은 해당 데이터를 DAO 를 통해 DB 에 추가. UI 와 DB 간 통신이 효율적으로 처리.

ColorViewModel 클래스의 경우, Application context 를 참조하여 lifecycle 인식 방식으로 데이터베이스를 인스턴스화할 수 있는 버전이기 때문에 ViewModel 대신 AndroidViewModel에서 확장한다.

기본 ViewModel 클래스는 UI component 와 분리된 비즈니스 로직을 제공하나, Application context 에 엑세스할 수는 없다.
AndroidViewModel 은 ViewModel 을 확장하면서 동시에 Application 클래스의 context 에 엑세스할 수 있다.

ColorViewModel에는 DAO를 사용하여 데이터베이스에 새 색상을 삽입하는 viewModelScope를 사용하여 새 코루틴을 시작하는 저장 기능이 있다.



Testing database

테스트는 특히 데이터베이스 및 네트워킹에 있어서 모든 앱 개발의 필수적인 부분이다. 데이터베이스 코드에 대한 테스트를 작성하여 예상대로 작동하는지 확인해야한다.



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

AndroidJUNIT4 runner는 Android 특정 코드를 실행하기 위한 적절한 references 및 contexts 를 제공한다. @Before와 @After는 @Test 주석으로 표시된 각 테스트 기능의 직전과 직후에 코드를 실행한다.



Create test class

@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")

    ...

테스트 수업에서 가장 먼저 해야 할 일은 DAO와 데이터베이스 개체를 선언하는 것이다. (Color instance 3개)



Create and close database for each test

In DatabaseTest.kt

@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()

@Before로 주석이 달린 함수에서 데이터베이스와 DAO 인스턴스를 만든다. (클래스의 모든 테스트 케이스 앞에서 실행) 실제 사용자 데이터가 있는 프로덕션 데이터베이스를 깨끗하게 유지하기 위해 인메모리 데이터베이스를 회전시켜 더 이상 사용하지 않을 때 폐기한다. @After로 주석이 달린 closeDb()는 각 테스트가 완료된 후 정리를 수행한다.



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)
    }

이제, 데이터베이스를 테스트할 수 있다. 이 코드에서 세 가지 색상을 DAO를 통해 데이터베이스에 넣고 검색을 시도한다.

0개의 댓글