안드로이드 Room과 코루틴

김성준·2022년 2월 27일
0

안드로이드

목록 보기
11/16

Room

Room?

Room은 로컬 데이터베이스에 데이터를 저장하기 위해 사용하는 라이브러리이다.
Room은 SQLite를 완벽히 활용하면서 원활한 데이터베이스 액세스가 가능하도록 SQLite에 추상화 계층을 제공합니다.

Room 사용 시 이점(SQLite API를 바로 사용했을 때에 비해)

  • SQL 쿼리의 컴파일 시간 확인 (잘못된 쿼리문을 컴파일 타임에 잡아줌)
  • 반복적이고 오류가 발생하기 쉬운 상용구 코드를 최소화하는 편의 주석(annotation)
  • 데이터베이스 마이그레이션이 용이함.

gradle 설정

앱에서 Room을 사용하려면 앱의 build.gradle 파일에 다음 종속 항목을 추가합니다.

dependencies {
    val roomVersion = "2.4.1"

    implementation("androidx.room:room-runtime:$roomVersion")
    annotationProcessor("androidx.room:room-compiler:$roomVersion")

    // To use Kotlin annotation processing tool (kapt)
    kapt("androidx.room:room-compiler:$roomVersion")
    // To use Kotlin Symbolic Processing (KSP)
    ksp("androidx.room:room-compiler:$roomVersion")

    // optional - Kotlin Extensions and Coroutines support for Room
    implementation("androidx.room:room-ktx:$roomVersion")

    // optional - RxJava2 support for Room
    implementation("androidx.room:room-rxjava2:$roomVersion")

    // optional - RxJava3 support for Room
    implementation("androidx.room:room-rxjava3:$roomVersion")

    // optional - Guava support for Room, including Optional and ListenableFuture
    implementation("androidx.room:room-guava:$roomVersion")

    // optional - Test helpers
    testImplementation("androidx.room:room-testing:$roomVersion")

    // optional - Paging 3 Integration
    implementation("androidx.room:room-paging:2.4.1")
}

m1 맥에서 오류 발생시
kapt 'org.xerial:sqlite-jdbc:3.34.0'를 더 추가해준다.

Room의 구성요소

Database 클래스

데이터베이스를 보유하고 앱의 영구 데이터와의 기본 연결을 위한 기본 액세스 포인트 역할을 합니다.

  • 클래스에는 데이터베이스와 연결된 데이터 항목을 모두 나열하는 entities 배열이 포함된 @Database 주석이 달려야 합니다.
  • 클래스는 RoomDatabase를 확장하는 추상 클래스여야 합니다.
  • 데이터베이스와 연결된 각 DAO 클래스에서 데이터베이스 클래스는 인수가 0개이고 DAO 클래스의 인스턴스를 반환하는 추상 메서드를 정의해야 합니다.
@Database(entities = [SleepNight::class], version = 1, exportSchema = false)
abstract class SleepDatabase: RoomDatabase() {
    abstract val sleepDatabaseDao: SleepDatabaseDao

    companion object {
        @Volatile 
// 변수를 휘발성으로 만듭니다. 모든 스레드가 동일한 값을 참조하게 합니다.
        private var INSTANCE: SleepDatabase? = null
        fun getInstance(context: Context): SleepDatabase {
        //synchronized 블럭은 이 구역에 오직 하나의 스레드만이 접근할 수 있음을 보장합니다. 여러 스레드가 동시에 이 구역에 접근해서 데이터 베이스가 여러개 생기는것을 막아줍니다.
            synchronized(this) {
                var instance = INSTANCE
                if (instance == null) {
                    instance = Room.databaseBuilder(
                        context.applicationContext,
                        SleepDatabase::class.java,
                        "sleep_history_database").fallbackToDestructiveMigration().build()
                    INSTANCE = instance
                }
                return instance
            }
        }
    }
}

DAO(데이터 액세스 객체)

앱이 데이터베이스의 데이터를 쿼리, 업데이트, 삽입, 삭제하는 데 사용할 수 있는 메서드를 제공합니다.

DAO는 클래스가 아니라 인터페이스로 작성한다.
삽입, 추가, 삭제는 @Insert, @Update, @Delete 어노테이션을 붙여주면 자동으로 구현된다.

이외의 행동들은 @Query("sql문")을 사용해서 직접 만들어줘야한다.
아래의 예제는 suspend를 사용한 비동기 DAO이다.
(일반적으로, DB작업은 메인스레드가 아닌 다른 스레드에서 실행되야하므로 비동기로 작성하는게 좋다)

@Dao
interface SleepDatabaseDao {
    @Insert
    suspend fun insert(night: SleepNight)
    @Update
    suspend fun update(night: SleepNight)
    @Query("SELECT * from daily_sleep_quality_table WHERE nightId = :key")
    suspend fun get(key: Long): SleepNight?
    @Query("DELETE FROM daily_sleep_quality_table")
    suspend fun clear()
    @Query("SELECT * FROM daily_sleep_quality_table ORDER BY nightId DESC LIMIT 1")
    suspend fun getTonight(): SleepNight?
    @Query("SELECT * FROM daily_sleep_quality_table ORDER BY nightId DESC")
    fun getAllNights(): LiveData<List<SleepNight>>
}

Data Entity

앱 데이터베이스의 테이블을 나타냅니다.

  • @Entitiy: 어노테이션으로 이 데이터 클래스가 엔티티임을 밝혀준다.
    괄호안에 테이블 명을 적을 수 있다. 안적으면 클래스의 이름을 사용한다.
    (테이블 이름을 적어주는걸 권장한다고 한다.)
  • @PrimaryKey: primaryKey는 키 값이기 때문에 유일한(Unique) 값이어야 한다. 직접 지정해도 되지만 autoGenerate를 true로 주면 자동으로 값을 생성한다.
  • @ColumnInfo: 각 열의 이름을 나타냅니다.
@Entity(tableName = "daily_sleep_quality_table")
data class SleepNight (
    @PrimaryKey(autoGenerate = true)
    var nightId: Long = 0L,
    @ColumnInfo(name = "start_time_milli")
    val startTimeMilli: Long = System.currentTimeMillis(),
    @ColumnInfo(name = "end_time_milli")
    var endTimeMilli: Long = startTimeMilli,
    @ColumnInfo(name = "quality_rating")
    var sleepQuality: Int = -1
        }

코루틴

코루틴?

코루틴은 비동기적으로 실행되는 코드를 간소화하기 위해 Android에서 사용할 수 있는 동시 실행 설계 패턴입니다.

코루틴의 장점

  • 경량: 코루틴을 실행 중인 스레드를 차단하지 않는 정지를 지원하므로 단일 스레드에서 많은 코루틴을 실행할 수 있습니다. 정지는 많은 동시 작업을 지원하면서도 차단보다 메모리를 절약합니다.

  • 메모리 누수 감소: 구조화된 동시 실행을 사용하여 범위 내에서 작업을 실행합니다.

  • 기본으로 제공되는 취소 지원: 실행 중인 코루틴 계층 구조를 통해 자동으로 취소가 전달됩니다.

  • Jetpack 통합: 많은 Jetpack 라이브러리에 코루틴을 완전히 지원하는 확장 프로그램이 포함되어 있습니다. 일부 라이브러리는 구조화된 동시 실행에 사용할 수 있는 자체 코루틴 범위도 제공합니다.

코루틴의 작업(job)

모든 코루틴은 job을 가집니다. 이 job은 계층을 이루고 있습니다. 모든 코루틴은 부모 자식 관계를 갖습니다. 부모의 job이 취소되면 자식 job은 모두 취소됩니다.

fun coroutineFunc() {
    val parent = CoroutineScope(Dispatchers.IO).launch {
        for (i in 1..10) {
            Log.i("coroutine", i.toString())
        }
        val child = CoroutineScope(Dispatchers.IO).launch {
            for (i in 11..20) {
                Log.i("coroutine", i.toString())
            }
        }
        child.cancel()
    }
    //parent.cancel()
}
출력
coroutineprac I/coroutine: 1
coroutineprac I/coroutine: 2
coroutineprac I/coroutine: 3
coroutineprac I/coroutine: 4
coroutineprac I/coroutine: 5
coroutineprac I/coroutine: 6
coroutineprac I/coroutine: 7
coroutineprac I/coroutine: 8
coroutineprac I/coroutine: 9
coroutineprac I/coroutine: 10
fun coroutineFunc() {
    val parent = CoroutineScope(Dispatchers.IO).launch {
        for (i in 1..10) {
            Log.i("coroutine", i.toString())
        }
        val child = CoroutineScope(Dispatchers.IO).launch {
            for (i in 11..20) {
                Log.i("coroutine", i.toString())
            }
        }
        //child.cancel()
    }
    parent.cancel()
}
출력
(없음)

예제에서 첫번째 예제는 부모-자식 관계에서 자식 job을 취소했습니다. 그래서 자식 job만 취소되어 출력은 1~10까지의 로그가 찍혔습니다.
두번째 예제는 부모-자식관계에서 부모 job을 취소했습니다. 따라서 부모 job과 자식 job 모두 취소되어 로그가 아무것도 찍히지 않았습니다.

코루틴 디스패처(Dispatcher)

디스패처는 코루틴이 어떤 스레드에서 실행될지를 결정해줍니다.

코루틴 디스패처의 종류

  • Main : Android 메인 스레드에서 코루틴을 실행하는 디스패처입니다. 이 디스패처는 UI와 상호작용을 하기 위한 목적으로만 사용되어야 합니다.

  • IO : 디스크 또는 네트워크 I/O작업을 수행하기 위한 디스패처입니다.

  • Default : CPU를 많이 사용하는 작업을 기본스레드 외부에서 실행하도록 최적화 되어있는 디스패처입니다. 정렬 작업이나 JSON파싱 작업에 최적화 되어있습니다.

  • Unconfined : 중도에 코루틴이 실행되는 스레드가 변경되는 디스패처(아직 잘 모르겠음) 더 자세한 설명은 이곳을 참고하세요. (안드로이드 개발에서는 사용하지 않는것을 권고한다고 합니다.)

코루틴의 범위(Scope)

  • 글로벌 스코프 : 앱의 생명주기와 함께 동작하기 때문에 실행 도중에 별도 생명 주기 관리가 필요없다.
GlobalScope.launch {
	//do something...
}
  • 코루틴 스코프 : 필요할 때 열고 닫는 용도로 적합하다. 코루틴 스코프에는 디스패처를 지정할 수 있는데 이는 코루틴이 동작하게 될 스레드를 결정하는 역할을 한다.
CoroutineScope(Dispatchers.[종류]).launch {
	//do something...
}
  • 뷰모델 스코프 : 제트팩 라이브러리의 뷰모델을 사용할 때, 뷰모델 인스턴스에서 사용하기 위해 제공된다. 뷰모델이 소멸할 때 자동으로 취소된다.
ViewModelScope.launch {
	//do something...
}

코루틴 빌더(Builder)

코루틴의 확장함수로써 코루틴을 시작하기위해 사용합니다.

  • launch
    • 새로운 코루틴을 생성합니다.
    • job인스턴스를 반환합니다. 그 인스턴스는 코루틴에 대한 참조로 활용됩니다.
    • 리턴값이 없는 코루틴을 활용할 때, 사용합니다.
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val co = CoroutineScope(Dispatchers.Default).launch {
            for (i in 1..10) {
                Log.i("coroutine", i.toString())
            }
        }
        //co.cancel() 등의 활용 가능
    }
}
출력
coroutineprac I/coroutine: 1
coroutineprac I/coroutine: 2
coroutineprac I/coroutine: 3
coroutineprac I/coroutine: 4
coroutineprac I/coroutine: 5
coroutineprac I/coroutine: 6
coroutineprac I/coroutine: 7
coroutineprac I/coroutine: 8
coroutineprac I/coroutine: 9
coroutineprac I/coroutine: 10
  • async
    • 새로운 코루틴을 생성합니다.
    • Deferred<T>의 인스턴스를 반환한다. 값을 얻기 위해서 await()함수를 사용합니다.
      Deferred 인터페이스는 job인터페이스를 확장한 것입니다. 그래서 우리는 Deferred 인스턴스를 코루틴을 취소하는 것 같이 job처럼 사용할 수 있습니다.
    • 리턴값이 있는 코루틴을 활용할 때, 사용합니다.
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val co = CoroutineScope(Dispatchers.Default).async {
            for (i in 1..10) {
                Log.i("coroutine", i.toString())
            }
            "ten" // return 값
        }
        CoroutineScope(Dispatchers.Default).launch {
        	val str = co.await() // await()함수로 리턴값을 str에 저장.
            Log.i("coroutine", str) // str을 로그에 출력
        }
    }
}
출력
coroutineprac I/coroutine: 1
coroutineprac I/coroutine: 2
coroutineprac I/coroutine: 3
coroutineprac I/coroutine: 4
coroutineprac I/coroutine: 5
coroutineprac I/coroutine: 6
coroutineprac I/coroutine: 7
coroutineprac I/coroutine: 8
coroutineprac I/coroutine: 9
coroutineprac I/coroutine: 10
coroutineprac I/coroutine: ten
  • runblocking
    • 다른 Coroutine Builder들과 다르게 작업이 끝날 때까지 스레드를 block합니다.
    • T타입의 결과를 반환합니다.
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        coroutineFunc()
        GlobalScope.launch {
            for (i in 1..10) {
                Log.i("coroutine", i.toString())
                delay(100)
            }
        }
    }
}

fun coroutineFunc() = runBlocking {
    launch {
        for (i in 11..20) {
            Log.i("coroutine", i.toString())
            delay(100)
        }
    }
}
출력
coroutineprac I/coroutine: 11
coroutineprac I/coroutine: 12
coroutineprac I/coroutine: 13
coroutineprac I/coroutine: 14
coroutineprac I/coroutine: 15
coroutineprac I/coroutine: 16
coroutineprac I/coroutine: 17
coroutineprac I/coroutine: 18
coroutineprac I/coroutine: 19
coroutineprac I/coroutine: 20
coroutineprac I/coroutine: 1
coroutineprac I/coroutine: 2
coroutineprac I/coroutine: 3
coroutineprac I/coroutine: 4
coroutineprac I/coroutine: 5
coroutineprac I/coroutine: 6
coroutineprac I/coroutine: 7
coroutineprac I/coroutine: 8
coroutineprac I/coroutine: 9
coroutineprac I/coroutine: 10

예제에서 coroutineFunc은 runBlocking으로 생성된 코루틴이 된다. runBlocking인 코루틴은 내 작업이 끝날 때까지 다른 작업들을 Block합니다. 그래서 11~20까지 로그를 찍는 coroutineFunc이 실행되고 끝날때 까지 MainActivity의 코루틴은 실행되지 못하고 기다리게 됩니다. coroutineFunc이 종료되고 나서야 MainActivity의 코루틴은 실행됩니다.

출처

안드로이드 개발자 가이드
코틀린 월드
코틀린 공식 가이드 자세히 읽기
Heeg's log

profile
수신제가치국평천하

0개의 댓글