메이쁘

[Android][Kotlin] Coroutine을 사용한 Room DB(DataBase) 핵심 정리 및 샘플 코드! 본문

Technology/Android - Android Studio

[Android][Kotlin] Coroutine을 사용한 Room DB(DataBase) 핵심 정리 및 샘플 코드!

메이쁘 2020. 7. 1. 00:54

안녕하세요.

 

최근 안드로이드에서 SQLite 사용에 도움되는 Room DB 라이브러리에 대해 알아봅시다.

 

 

*** Coroutine에 대해 짚고 넘어가고 싶다?

https://maivve.tistory.com/154

 

[Kotlin] 코틀린의 Coroutine 이란 무엇일까 ?

안녕하세요. Coroutine 은 코루틴이라고 불리며 코틀린의 주요 기능 중 하나입니다. 코루틴은 2018년 10월 29일 Kotlin 1.3에서 정식 릴리즈되면서 추가된 기능 중 하나라고 합니다. 그럼 시작하겠습니��

maivve.tistory.com

 

 

 

 

 

Room DB(Database) 란 ?


  -  AAC(Android Architecture Component) 중 하나.

 

  -  SQLite DB 를 보다 더 쉽게 사용할 수 있도록 하는 라이브러리.

 

  -  SQLite 기능을 사용하면서, Kotlin(또는 Java) 언어를 통해 직접 DB에 접근할 수 있다.

 

  -  즉, 객체의 맵핑을 통해 DB에 직접 접근하는 ORM(Object Relational Mapping) 라이브러리

 

  -  DB에서 데이터를 송수신하는 작업이기 때문에 UI Thread인 Main Thread 에서 처리할 수 없고 Background 에서만 작업 처리가 가능하다. 보통 비동기 처리.

 

 

  *** 왜 SQLite 대신에 Room DB를 사용할까?  Room DB가 해결한 SQLite의 문제점?

    1)  SQLite 는 컴파일 시간이 확실하지 않다.

    2)  SQL 데이터에 변화가 생기면 직접 수동으로 변경해야 한다.

    3)  위 과정에서 시간이 많이 소모되며 오류 발생 확률이 높다.

 

 

 

Room DB 구성요소


 

  - Entity

      ->  DB의 테이블 과 매칭될 클래스. 

      ->  테이블에서 column 에 해당하는 정보들을 변수(parameter)로 담고 있다.

 

 

  - DAO(Data Access Object)

      ->  DB 작업(CRUD)을 함수로 정의한 클래스. 즉, 실제로 DB에 접근하는 객체.

      ->  @Dao Annotation 을 가진 interface

      ->  return 값을 livaData로 받아오면서 최신 데이터 유지

      ->  Room은 DAO의 구현을 컴파일 시간에 생성한다.

 

 

  -  Room Database

      ->  DB 생성 및 버전 관리를 담당하는 Room DB 객체.

      ->  @Database Annotation 안에 사용할 entity 클래스를 포함시킴

      ->  Singleton(싱글톤) 패턴으로 만들어야 비용을 절약하고 데이터의 일치성을 보장할 수 있다.

 

 

 

 

음.. 이렇게만 적어놓으면 감이 덜 잡힐 것입니다.

 

이제부터 직접 코드를 작성하고 보면서 이해하는 과정을 진행하겠습니다.

 

  *** 부제 : 로그 관리 테스트 앱

 

 

 

 

 

 

 

1. Gradle 추가


 apply plugin: 'kotlin-kapt'
 
 dependencies {
      def room_version = "2.2.5"

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

      // 저희는 코루틴을 사용할 것이기 때문에 해당 라이브러리까지 코드 작성
      implementation "androidx.room:room-ktx:$room_version"

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

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

  - 코루틴을 사용할 것이기 때문에 코루틴 까지만 작성하고, 하단 부분은 필요한 분들만 코드 추가하면 된다.

 

 

2. Entity 생성


/* User.kt */

@Entity(tableName = "USER")
data class User(
    @PrimaryKey(autoGenerate = true) val id: Int, // autoGenerate == auto increment
    @ColumnInfo(name = "userName") var userName: String, // 이름
    @ColumnInfo(name = "nickName") var nickName: String, // 닉네임
    @ColumnInfo(name = "addTime") var addTime: Long     // 시간(Date to Long)
) {
    constructor() : this(0, "", "", 0) // default 값
}

  -  USER 라는 이름을 가진 테이블 생성.

 

  -  PrimaryKey로 int type id 컬럼 추가.

    *** 이 때, autoGenerate = true 를 통해 auto increment 로 진행됨

 

  -  addTime 컬럼 값은 Long type 이기 때문에, 안드로이드 상에서는 Date 로 변환해서 사용한다.

 

  -  constructor() : this(~) 의 괄호 내에 입력한 값은 만약 DB로 해당 컬럼 값에 null이 insert 될 경우 기본값 을 뜻한다.

 

 

 

 

 

3. DAO 생성


/* UserDAO.kt */

@Dao
interface UserDAO {
    @Insert(onConflict = REPLACE)
    suspend fun insertAll( users : User)

    // onConflict : 중복된 Primary Key 값이 DB 내 존재할 경우 대체(REPLACE)
    // 다른 방식을 사용하기 위해선 다른 값을 집어넣으면 됨
    @Insert(onConflict = REPLACE)
    suspend fun insert(user: User)

    @Delete
    suspend fun delete(user: User)

    @Query("SELECT * FROM USER")
    suspend fun getAllUsers() : List<User>

    @Query("SELECT * FROM USER WHERE userName = :name")
    suspend fun getUser(name: String) : User
}

  -  onConflict : DB 내 중복된 값이 존재할 경우(보통 Primary Key를 보고 판단) REPLACE(대체. 즉, update와 동일)

 

  -  Insert(삽입)은 @Insert, Delect(삭제)는 @Delete

 

  -  쿼리문 내 변수로 값을 입력받아 넣고자 할 경우, :변수명 을 넣고, 호출 함수의 parameter 변수명과 일치시킨다.

    *** "SELECT * FROM USER WHERE userName = :name"

 

  -  Coroutine을 사용하기 위해 fun 앞에 suspend 추가.

 

 

 

 

 

4. TypeConverter 생성


/* UserTypeConverter.kt */
class UserTypeConverter {
    @TypeConverter
    fun fromTimestamp(value: Long?) : Date? = value?.let { Date(it) }

    @TypeConverter
    fun dateToTimeStamp(date : Date?) : Long? = date?.time
}

  -  Date 객체를 사용하기 위함. (시간을 기록하기 위함)

 

  -  Date to Long, Long to Date 함수

 

  -  Room DB에서 기본 자료형이 아닌 객체를 사용하기 위한 Annotation : @TypeConverter

 

 

5. Room Database 클래스, DB 객체 생성


/* UserDatabase.kt */

@Database(entities = [User::class], version = 1)
@TypeConverters(UserTypeConverter::class) // TypeConverter를 Database에 포함
abstract class UserDatabase : RoomDatabase() {
    abstract fun userDao(): UserDAO
}

  -  클래스 생성

 

  -  TypeConverter 객체를 Database에 집어넣기 위한 Annotation 작성

 

 

 

 

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val db = Room.databaseBuilder(
            applicationContext,
            UserDatabase::class.java, "test-db" // DB 이름
        ).build()

    }
}

 - 메인액티비티에서 db 라는 Room Database 객체 생성

 

 

 

 

 

6. 활용


*** Coroutine 을 사용하여 Room DB 작업을 동기, 비동기 중 원하는대로 자유롭게 가능

*** 뿐만 아니라, 백그라운드 쓰레드에서 작업 진행 및 효율적인 순서를 설계, 처리 가능

 

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val db = Room.databaseBuilder(
            applicationContext,
            UserDatabase::class.java, "test-db" // DB 이름
        ).build()

        var userName: String = "테스트입니다"
        var nickName: String = "테스트야옹"

        // runBlocking : 괄호 내의 코드가 전부 진행된 다음에야 괄호 밖을 벗어날 수 있다. 동기 처리 가능
        runBlocking {
            delay(1000L)
            db.userDao().insert(User(0, userName, nickName, Date()))
            Log.d("TEST", "[SUCCESS] value is saved! name : $userName And nickname : $nickName")
        }

        // IO 쓰레드에서 진행.
        GlobalScope.launch(Dispatchers.IO) {
            var user = db.userDao().getUser(userName)
            Log.d("TEST", "[SUCCESS] value getted! name : ${user.userName} And nickname : ${user.nickName}")
        }
    }
}

 

결과 로그

 

  - 정상적으로 출력 확인됨.

 

 

 

 

 

 

감사합니다!

 

궁금한 점, 보완할 점 있으면 바로 댓글달아주세요!

Comments