Room은 로컬 데이터베이스에 데이터를 저장하기 위해 사용하는 라이브러리이다.
Room은 SQLite를 완벽히 활용하면서 원활한 데이터베이스 액세스가 가능하도록 SQLite에 추상화 계층을 제공합니다.
Room 사용 시 이점(SQLite API를 바로 사용했을 때에 비해)
- SQL 쿼리의 컴파일 시간 확인 (잘못된 쿼리문을 컴파일 타임에 잡아줌)
- 반복적이고 오류가 발생하기 쉬운 상용구 코드를 최소화하는 편의 주석(annotation)
- 데이터베이스 마이그레이션이 용이함.
앱에서 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'를 더 추가해준다.
데이터베이스를 보유하고 앱의 영구 데이터와의 기본 연결을 위한 기본 액세스 포인트 역할을 합니다.
@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는 클래스가 아니라 인터페이스로 작성한다.
삽입, 추가, 삭제는 @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>>
}
앱 데이터베이스의 테이블을 나타냅니다.
@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은 모두 취소됩니다.
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 모두 취소되어 로그가 아무것도 찍히지 않았습니다.
디스패처는 코루틴이 어떤 스레드에서 실행될지를 결정해줍니다.
Main : Android 메인 스레드에서 코루틴을 실행하는 디스패처입니다. 이 디스패처는 UI와 상호작용을 하기 위한 목적으로만 사용되어야 합니다.
IO : 디스크 또는 네트워크 I/O작업을 수행하기 위한 디스패처입니다.
Default : CPU를 많이 사용하는 작업을 기본스레드 외부에서 실행하도록 최적화 되어있는 디스패처입니다. 정렬 작업이나 JSON파싱 작업에 최적화 되어있습니다.
Unconfined : 중도에 코루틴이 실행되는 스레드가 변경되는 디스패처(아직 잘 모르겠음) 더 자세한 설명은 이곳을 참고하세요. (안드로이드 개발에서는 사용하지 않는것을 권고한다고 합니다.)
GlobalScope.launch {
//do something...
}
CoroutineScope(Dispatchers.[종류]).launch {
//do something...
}
ViewModelScope.launch {
//do something...
}
코루틴의 확장함수로써 코루틴을 시작하기위해 사용합니다.
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
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
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의 코루틴은 실행됩니다.