본문 바로가기

IT/안드로이드 관련

[안드로이드] ROOM 라이브러리 사용하기 , 코루틴

 안녕하세요 남갯입니다


오늘은 구글 JetPack componet에 포함된 ROOM에 대해 포스팅해보려고합니다.


스스로 정리해서 작성하는것이기때문에 틀린점이나 비판은 댓글에 달아주시면 달게받겠습니다.



- JetPack component란?


구글 IO에서 62개정도의 작은 세션들을 공개했습니다 그 세션들의 집합을 Jetpack 이라하는데 , Android앱을 손쉽게 개발하도록 지원하는 android 소프트웨어 구성요소 컬렉션입니다

이런 컴포넌트로 상용구코드를 작성하지않고, 복잡한 작업을 간소화 시킵니다. 



-출처

https://developer.android.com/jetpack/, 

https://developers-kr.googleblog.com/2018/05/use-android-jetpack-to-accelerate-your.html



- ROOM이란? 


ROOM은 ORM(Object Relational Mapping) 라이브러리입니다. 쉽게 말해 ROOM은 데이터베이스의 객체를 자바 or코틀린 객체로 매핑해주는것 입니다. ROOM은 SQLite의 추상레이어 위에 제공하고 있으며 SQLite의 모든 기능을 제공하면서 편한 데이터베이스의 접근을 허용합니다





- ROOM과 SQLite의 차이점


 1. SQLite 경우 쿼리에 대한 에러를 컴파일에 확인하는것이 없지만 ROOM에서는 컴파일 도중 SQL에 대한 유효성을 검사 가능합니다


 2. Schema가 변경이 될경우 SQL쿼리를 수동으로 업데이트 해야하지만 ROOM의 경우는 쉽게 해결이 가능합니다.


 3. SQLite 경우 Java데이터 객체를 변경하기위해 많은 상용구 코드(Boiler Plate code)를 사용해야하지만 ROOM의 경우

ORM라이브러리가 상용구 코드(Boiler Plate code) 없이 매핑 가능합니다.

※ boilerPlateCode : 수정하지 않거나 최소한의 수정만을 거쳐 여러곳에 필수적으로 사용되는 코드


 4. ROOM의 경우 LiveDataRxJava를 위한 Observation 으로 생성하여 동작할 수 있지만 SQLite는 그렇지 않습니다.




- ROOM의 3개 구성요소


ROOM에는 크게 3가지 구성요소(Database, Entity, Dao) 가 있습니다.

- Database : 데이터베이스 보유자입니다.

- Entity : Database 내의 테이블을 뜻합니다. 

- Dao : 데이터베이스에 엑세스하는데 사용되는 메소드들을 갖고있습니다. select, insert, delete, join...등 데이터를 쓰거나 읽을때 사용합니다.




- 출처 

https://stackoverflow.com/questions/50650077/sqlite-database-vs-room-persistence-library

https://medium.com/mindorks/sqlite-made-easy-room-persistence-library-ecd1a5bb0a2c

https://developer.android.com/training/data-storage/room/



- ROOM gradle 설정


androidX 기준 


dependencies {
   
def room_version = "2.1.0-alpha03"

    implementation
"androidx.room:room-runtime:$room_version"
    annotationProcessor
"androidx.room:room-compiler:$room_version" // use kapt for Kotlin

   
// optional - RxJava support for Room
    implementation
"androidx.room:room-rxjava2:$room_version"

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

   
// optional - Coroutines support for Room
    implementation
"androidx.room:room-coroutines:$room_version"

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

-출처

https://developer.android.com/topic/libraries/architecture/adding-components#room




- ROOM 이용하기

저는 현재 개발중인 앱에다가 넣을것입니다. 개발할 앱은 다이어리앱인데 달력에 이미지표시하는 테이블과 글쓴내용을 저장하는 테이블을 만드려 합니다.

 



패키지의 구성은 저렇게 구성하였습니다

-db Package

dao package

entity

database




-Database

데이터 베이스는 AAC의 예제소스를 참조하여 싱글톤으로 구성했습니다. 또한 싱글톤의 중복생성을 방지하기위해 synchronized로 구성했습니다. 사용하는 Entity에 관해서는 위에 entities 로 명시해서 사용시에 쉽게 불러올 수 있습니다.

@Database(entities = arrayOf(EventIconEntity::class, WriteDataEntity::class), version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun EventIconDao(): EventIconDao
abstract fun WriteDao(): WriteDao

companion object {
private var INSTANCE: AppDatabase? = null

fun getInstance(context: Context): AppDatabase? {
if (INSTANCE == null) {
synchronized(AppDatabase::class) {
INSTANCE = Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "your_db.db").build()
}
}
return INSTANCE
}
}


-Entity

테이블을 대표하는 Entity는 저는 두개의 테이블이 필요했기 때문에 아래와 같이 구성했습니다.

저는 자동으로 증가하는 id값을 PK로 잡아서 사용했습니다.


1.아이콘 정보와 날짜를 저장하는 EventIconEntity


@Entity(tableName = "evnetIcon")
data class EventIconEntity(@PrimaryKey(autoGenerate = true) val id: Long, var icon: Int = 0, var date: String)

※tableName을 설정안하게 되면 클래스이름으로 테이블 이름을 사용하게 됩니다.

(By default, Room uses the class name as the database table name. If you want the table to have a different name, set the tablename property of the entity annotation, as shown in the following code snippet:)


※autogenerate 는 sql문에서 autoIncrement와 같이 자동으로 증가하는 id값에대한 설정입니다.

외래키(ForeignKey) 설정 가능합니다. AAC 에서 확인해보세요 

※@ColumnInfo(name = 할이름) 을 통해 원하는 칼럼의 이름으로 변경 가능합니다.


2.글의 제목과 내용의 정보를 저장하는 WriteDataEntity

@Entity(tableName = "writeData")
data class WriteDataEntity(@PrimaryKey(autoGenerate = true) val id: Long, var title: String, var content: String, var date: String)


-Dao

entity로 설정한 테이블에 관한 쿼리문을 작성하는 부분입니다. DAO는 인터페이스로 작성해야합니다.


아래의 링크에서 ROOM 라이브러리를 유용하게 이용하는 팁 7가지와 BASEDAO를 이용하는법을 알려주고 있습니다. 

출처 : 

7가지 방법

https://medium.com/androiddevelopers/7-pro-tips-for-room-fbadea4bfbd1

-BASEDAO   

https://gist.github.com/florina-muntenescu/1c78858f286d196d545c038a71a3e864


interface BaseDao<T> {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(obj : T)

@Delete
fun delete(obj : T)

@Update(onConflict = OnConflictStrategy.ABORT)
fun update(obj : T)
}


저같은경우는 위와같이 만들었는데요? 필요에 맞춰서 만드시면 됩니다.

추가적으로 onConflict 는 데이터베이스 충돌이 날 경우 어떻게 처리할지를 명시한것입니다.


*총 5가지의 전략적인 충돌처리방법을 설정할 수 있습니다.


public @interface OnConflictStrategy {
/**
* OnConflict strategy constant to replace the old data and continue the transaction.
*/
int REPLACE = 1;
/**
* OnConflict strategy constant to rollback the transaction.
*/
int ROLLBACK = 2;
/**
* OnConflict strategy constant to abort the transaction.
*/
int ABORT = 3;
/**
* OnConflict strategy constant to fail the transaction.
*/
int FAIL = 4;
/**
* OnConflict strategy constant to ignore the conflict.
*/
int IGNORE = 5;

}

위에서 만든 BaseDao를 상속받아 아래와같이 사용이 가능합니다.


@Dao
interface EventIconDao : BaseDao<EventIconEntity> {

@Query("SELECT * FROM eventIcon WHERE id = :id")
fun selectById(id: Int): Maybe<EventIconEntity>

@Query("SELECT * FROM eventIcon")
fun selectAll(): Maybe<List<EventIconEntity>>

@Query("SELECT * FROM eventIcon WHERE date = :date")
fun selectCountByDate(date : String) : Int

@Query("SELECT * FROM eventIcon WHERE date = :date")
fun selectByDate(date : String) : EventIconEntity

@Query("DELETE FROM eventIcon WHERE date = :date")
fun deleteByDate(date : String)


}

위코드에서도 볼 수 있듯이 ROOM에서의 장점은 rxJava에서 제공하는 Single, Maybe Flowable와 LiveData타입을 이용할 수 있습니다.


-Single , Maybe, Flowable


- Single 타입은 1개가 오면 success 아무것도 안오면 error를 타게됩니다.

- Maybe 타입은 행이 1개 or 0개가 오거나 Update시

 success -> oncomplete를 타게됩니다.

- Flowable 타입은 어떤 행도 존재하지 않을경우 onNext나 onError을 방출하지 않습니다.


출처 : https://medium.com/androiddevelopers/room-rxjava-acb0cd4f3757




-LiveData


LiveData의 경우

https://riggaroo.co.za/android-architecture-components-looking-room-livedata-part-1/

이 블로그에 들어가면 설명이 잘되어있고 확인바랍니다



- 주의할점


ROOM을 이용할 때 쿼리의 호출은 MainThread에서 하면 아래와 같은 에러가 발생합니다.


java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time


출처 : https://stackoverflow.com/questions/44167111/android-room-simple-select-query-cannot-access-database-on-the-main-thread


메인스레드를 이용해 많은 양의 쿼리를 하다보면 오랜기간동안 UI가 동작하지 않을 수 있기때문에 막혀있습니다. 따라서 UI스레드 이외에서 동작해야합니다. (사실 DATABASE를 빌드할때 세팅값으로 .allowMainThreadQueries() 라는 값을 설정해주면 동작하긴 하지만 샘플코드에서는 위와같은 이유로 절대 쓰지말라고 하고있습니다.)


비동기 처리를위해 아래와 같은 선택을 할 수 있습니다.

1. AsyncTask 

2. RxJava

3. Coroutine

4. JavaThread


저의 앱에 개발할것이기 때문에 새롭게 Coroutine을 이용해서 만들어보려고 합니다.


-RxKotlin 동작


실제 RxKotlin를이용해서도 한번 돌려봤는데요? RxKotlin은 통신라이브러리 같은것에 주로 이용해보다가 ROOM에도 동작이 가능하다길래 아래와 같이 동작시켜 봤습니다.

addDisposable(appDatabase?.EventIconDao()?.selectAll()
!!.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
initCalendar(it)
LogUtil.e("test", it.toString())
}, { error -> LogUtil.e("test", "error" + error.toString()) }
, { LogUtil.e("test", "complete") }))



-Coroutine 이란?


코루틴(coroutine)은 루틴의 일종으로서, 협동 루틴이라 할 수 있다(코루틴의 "Co"는 with 또는 togather를 뜻한다). 상호 연계 프로그램을 일컫는다고도 표현가능하다. 루틴과 서브 루틴은 서로 비대칭적인 관계이지만, 코루틴들은 완전히 대칭적인, 즉 서로가 서로를 호출하는 관계이다. 코루틴들에서는 무엇이 무엇의 서브루틴인지를 구분하는 것이 불가능하다. 코루틴 A와 B가 있다고 할 때, A를 프로그래밍 할 때는 B를 A의 서브루틴으로 생각한다. 그러나 B를 프로그래밍할 때는 A가 B의 서브루틴이라고 생각한다. 어떠한 코루틴이 발동될 때 마다 해당 코루틴은 이전에 자신의 실행이 마지막으로 중단되었던 지점 다음의 장소에서 실행을 재개한다(The Art of Computer programming, 도널드 커누스 저).


출처 : 

https://ko.wikipedia.org/wiki/%EC%BD%94%EB%A3%A8%ED%8B%B4



Coroutine 라이브러리를 사용해서 Coroutine을 쉽게 시작하려면 launch와 async함수를 사용하면 됩니다. async 와 launch는 컨셉적으로는 거의 동일한 기능이지만 launch는 Job을 return 하고 어떠한 결과값도 전달하지 않지만 async는 Deferred을 return 하면서 약속된 값을 제공하는 차이점이 있습니다 만약 실행중인 코드가 예외로 종료되게되면 launch에서는 android 응용프로그램과 충돌하고 async 코드는 Deferred에 저장되고 처리하지않으면 자동으로 삭제시킵니다.


Coroutine에서의 dispatcher는 두가지가 있습니다

-uiDispatcher : 안드로이드 UI스레드에서 실행합니다. (Dispatchers.Main)

-bgDisPatcher  BG 스레드에서 실행합니다. (Dispatchers.IO)



출처:

https://proandroiddev.com/android-coroutine-recipes-33467a4302e9



-Room에 Coroutine 비동기처리 적용


저는 room의 비동기 처리를 간단하게 이용하려 합니다.

일단 아래와같이 최신버젼을 그래들에 추가합니다.


Gradle setting 적용 -kotlin 1.3.11 버젼 기준

coroutineVersion = "1.1.0"

// coroutine
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion"


난독화를 사용하시면 필요에 따라 프로가드도 적용합니다.


Proguard 적용

#coroutine
# ServiceLoader support
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}

# Most of volatile fields are updated with AFU and should not be mangled
-keepclassmembernames class kotlinx.** {
volatile <fields>;
}


난독화를 사용하시면 필요에 따라 프로가드도 적용합니다.


메인스레드를 이용하지 않은 비동기 처리를 하기위해 BGDispatchers를 이용해 아래와같이 동작시킵니다.

CoroutineScope(Dispatchers.IO).launch {
appDatabase?.EventIconDao()?.selectAll()
}


아래와같이 데이터를 가져오는것을 확인했습니다.




- 마무리하며

이해를 완전히 다 하면서 한상태가 아니라 많이 어려움이 많았던것 같습니다. 제가 글솜씨도 별로 좋은편이 아니라 많이 어려움이 있었습니다. 지속적으로 이런 개발글과 함께 이해된부분을 작성하고 발전하는 남갯이 되도록 하겠습니다.



-그 외 다양한 글들


coroutine을 이용한 room

https://proandroiddev.com/android-coroutine-recipes-33467a4302e9


-----------------------------------------------------------------------------------------------------------------------------------------------------------

//database change observing

https://stackoverflow.com/questions/48978145/observing-changes-in-room-database-table-using-itemkeyeddatasource?rq=1



//database delete시 이용방법

https://stackoverflow.com/questions/47538857/android-room-delete-with-parameters