<코드랩 과정6: Room데이터베이스와 코루틴>
# 시작하며
새로운 코드랩에서는 Room을 활용해서 데이터를 저장하는 방법과 코루틴을 활용해서 DB를 사용하는 방법에 대해서 배운다. 이번챕터에서는 그 중 첫번째로, Room 데이터베이스를 만들기 위해 엔티티,DAO,DB를 설정하는 방법에 대해서 알아본다.
최종 완성본은 위와같이 나의 수면데이터를 저장하도록한다. START버튼을 누른 시점부터 잠이 들었다는 가정이고, END를 누르면 잠에서 깼다는 가정이다. 그렇게 수면이 종료되면 수면시간과 나의 수면 퀄리티를 저장할 수 있도록 한다. 이 전반의 과정이 Room을 활용한 로컬DB에 저장 된다.
현재 스타터 팩을 실행하면 레이아웃 구성외에는 아무런 기능이 동작하지 않는 것을 알 수 있다.
일반적인 아키텍처 구조에서는 아래 그림의 왼쪽과 같이 기능을 나눈다.
그러나 이번 코드랩에서는 레포지토리와 네트워크 기능을 사용하지 않기 때문에 우측 이미지와 같은 형태를 띄게 될 것이다.
# 안드로이드 데이터베이스 Room
데이터 베이스 세계에서는 entities라는 개체가 존재하고 queries들을 이용해서 이런 개체에 접근하거나 수정하게 된다. 엔티티가 모여서 테이블이 된다고 말할 수 있고 이러한 것들이 DataBase에 저장되어 관리된다. 이러한 엔티티를 관리하기 위한 클래스와 쿼리를 관리하기 위한 DAO(Database Access Object)를 정의하여 DB에 조금 더 손쉽게 접근할 수 있다.
로컬 DB를 사용함으로써 사용자의 경험을 증대시킬 수 있는데, 예컨대 Offline환경에서도 앱을 사용 할 수 있도록 해준다.
# Entity ( 엔티티 )
작년 학부 수업중 DB를 수강했을때의 자료를 첨부하여 엔티티에 대한 이해를 조금 도울 수 있도록 한다. 엔티티는 attribute로 구성된다. 즉 하나의 row를 담당하게 된다.
엔티티에는 colum이라는 속성을 통해서 attribute를 구성하게 되고 이 한줄의 값이 하나의 엔티티가 된다. 즉 [1, 성재, 이 , .... ] 의 한줄 의 값이 엔티티가 되는 것이다.
# 엔티티 만들기
database패키지에서 SleepNight.kt 파일에 엔티티를 정의해준다.
@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 = System.currentTimeMillis(),
@ColumnInfo(name = "quality_rating")
var sleepQuality: Int = -1
)
- @Entity 어노테이션에는 테이블이름을 옵셔널하게 지정해줄 수 있다. 지정하지 않는 경우에는 클래스명과 같은 테이블 명을 가지게 된다.
- @PrimaryKey 어노테이션을 통해 고유한 ID값이 되는 것을 지정해줄 수 있다.
- @ColumnInfo를 통해 컬럼의 이름을 옵셔널하게 지정해줄 수 있다.
엔티티는 이렇게 간단히 구성할 수 있다.
# DAO 만들기
DAO(Database Access Object)을 통해 쿼리를 정의하게 된다. 안드로이드에서는 insert,delete,update와 같은 것들을 손쉽게 사용하도록 도와준다.
이번 코드랩에서 필요한 쿼리들은 다음과 같기 때문에 필요한 쿼리들을 아래와 같이 작성해준다.
@Dao
interface SleepDatabaseDao{
@Insert
fun insert(night: SleepNight)
@Update
fun update(night: SleepNight)
@Query("SELECT * FROM daily_sleep_quality_table WHERE nightId = :key")
fun get(key: Long) : SleepNight?
@Query("DELETE FROM daily_sleep_quality_table")
fun clear()
@Query("SELECT * FROM daily_sleep_quality_table ORDER BY nightId DESC LIMIT 1")
fun getTonight(): SleepNight?
@Query("SELECT * FROM daily_sleep_quality_table ORDER BY nightId DESC")
fun getAllNights(): LiveData<List<SleepNight>>
}
# RoomDatabase 만들기
위에서 만들었던 엔티티와 DAO를 활용해서 Room을 만들어 줘야한다. 예전에도 몇번 사용해 봤지만 그때는 간단하게 사용했는데, 이번 코드랩에서는 다중쓰레드를 고려하기도 하고 DB를 인스턴스화할 필요가 없고 한번 만들어놓고 사용하면 된다고 하기 때문에 처음 보는 형태로 만들어 졌다.
@Database(entities = [SleepNight::class], version = 1, exportSchema = false)
abstract class SleepDatabase : RoomDatabase() {
abstract val sleepDatabaseDao : SleepDatabaseDao
}
@Database어노테이션에 사용되는 엔티티와 버전, exportSchema의 사용여부를 적어준다. 특히 version은 스키마가 바뀔때마다 버전을 증가시키도록 한다.
그안에 필요한 DAO를 데이터베스가 알 수 있도록 해주기 위해 추상변수로 만들어준다. 여러개의 DAO를 지정할 수 있다.
이제 companion object를 통해서 데이터베이스를 인스턴스화 해준다. companion object를 통하여 클래스를 인스턴스화 하지 않고 데이터베이스를 작성하거나 가져오는 메소드를 사용할 수 있다.
SleepDatabase의 바디안에 아래의 companion object를 다음과 같이 작성해준다.
companion object {
private var INSTANCE: SleepDatabase? = null
fun getInstance(context: Context): SleepDatabase {
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
}
}
}
instance화 된 DB가 없다면 DB를 만들어서 반환시켜주도록 하는 것이다. synchronized는 여러 쓰레드가 DB를 사용하는 경우에 대비해서 사용한다. 얼추 보기에 LOCK개념인 듯 했다. 이렇게 RoomDB도 성공적으로 만들 수 있다.
# 테스트
테스트를 진행 할 수 있는 코드도 스타터팩에 존재했다.
@RunWith(AndroidJUnit4::class)
class SleepDatabaseTest {
private lateinit var sleepDao: SleepDatabaseDao
private lateinit var db: SleepDatabase
@Before
fun createDb() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
// Using an in-memory database because the information stored here disappears when the
// process is killed.
db = Room.inMemoryDatabaseBuilder(context, SleepDatabase::class.java)
// Allowing main thread queries, just for testing.
.allowMainThreadQueries()
.build()
sleepDao = db.sleepDatabaseDao
}
@After
@Throws(IOException::class)
fun closeDb() {
db.close()
}
@Test
@Throws(Exception::class)
fun insertAndGetNight() {
val night = SleepNight()
sleepDao.insert(night)
val tonight = sleepDao.getTonight()
assertEquals(tonight?.sleepQuality, -1)
}
}
위 테스트코드를 실행해봄으로써 DB가 정상적으로 작동하는지 빌드하지 않고도 알 수 있었다.
# 마치며
먼저, 엔티티의 개념을 명확히 하기 위해서 작년에 수강했던 DB강의자료를 조금 살펴 보았고, 운영체제에서 등장하는 멀티쓰레드와 락개념이 얼핏 보이는거 같아서 이해가 수월했다. 학교에서 배우는 것들이 꽤나 큰 도움이 되는 것을 느낄 수 있었다.
디비를 이러한 싱글톤패턴(?)으로 생성하는 것을 새로 알 수 있었다.
가장 충격적인건 앱 빌드하면서 오류가 나서 검색했는데 내 블로그가 상단 2번째 노출 됐다. 구글에 에러를 검색하고 내 블로그가서 에러를 해결하는 기분좋은 상황이었다.