안드로이드의 감을 잊지 않고 꾸준하게 공부를 동기부여를 위해 NextStep의 안드로이드 아키텍처 강의를 듣고 있다.
최근 mvvm 과제를 하면서 Room 을 사용할 일이 생겨서 기본적인 내용을 정리해 보았다.
원래 원노트에 정리해서 블로그에 옮길 때 스타일이 많이 깨져서 블로그에 공부한 내용을 올리기가 어려웠는데, 이제 VS CODE 의 플러그인으로 Markdown 으로 작성 후 원노트에 다가 배포하는 형태로 노트들을 적고 있어서 복사-붙여넣기만 하면되기에 블로그에도 한번 올려본다.
Room 을 처음 사용하는 사람들에게 도움이 되었으면 합니다.
Room 의 기본
Room?
- SQLite 를 손쉽게 사용할 수 있도록 추상화 계층을 제공
- ORM 라이브러리의 일종으로 JPA 와 유사한 것으로 보임
사용방법
- 안드로이드 모듈에서 동작
build.gradle
파일에 아래 종속성 추가
dependencies {
def room_version = "2.4.1"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
// optional - RxJava2 support for Room
implementation "androidx.room:room-rxjava2:$room_version"
// optional - RxJava3 support for Room
implementation "androidx.room:room-rxjava3:$room_version"
// optional - Guava support for Room, including Optional and ListenableFuture
implementation "androidx.room:room-guava:$room_version"
// optional - Test helpers
testImplementation "androidx.room:room-testing:$room_version"
// optional - Paging 3 Integration
implementation "androidx.room:room-paging:2.4.1"
}
ROOM 을 사용하며 만난 빌드 및 테스트 진행 시 발생오류
kapt 미사용 시 오류 발생
위처럼 하고 Build 를 하면 java.lang.RuntimeException: cannot find implementation for hbs.com.timetablescreen.Utils.AppDataBase. AppDataBase_Impl does not exist
와 같은 오류가 발생한다.
이 때는 gradle 파일 상단에 apply plugin: 'kotlin-kapt'
를 삽입하고, dependencies 에 kapt 'android.arch.persistence.room:compiler:1.1.1'
추가한다.
테스트 시 META 오류 발생
Execution failed for task ':data:mergeDebugAndroidTestJavaResource'.
> A failure occurred while executing com.android.build.gradle.internal.tasks.MergeJavaResWorkAction
> 2 files found with path 'META-INF/AL2.0' from inputs:
- /Users/parkjunpil/.gradle/caches/transforms-3/ee1541494a8030d5cbb3f3cb5379c401/transformed/jetified-jna-platform-5.5.0.jar
- /Users/parkjunpil/.gradle/caches/transforms-3/0725732782c7283c5a3e2fbe9a126f9d/transformed/jetified-jna-5.5.0.jar
Adding a packagingOptions block may help, please refer to
https://google.github.io/android-gradle-dsl/current/com.android.build.gradle.internal.dsl.PackagingOptions.html
for more information
* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.
테스트 실행 시 위와 같이 오류가 발생하는 경우 build.gradle 에 아래를 추가한다.
android {
packagingOptions {
pickFirst 'META-INF/AL2.0'
pickFirst 'META-INF/LGPL2.1'
}
}
Room 의 기본요소?
1. 데이터베이스 클래스
데이터베이스를 보유하고, 앱의 영구데이터와의 기본 연결을 위한 기본 액세스 포인트 역할. 즉, 데이터베이스 클래스는 데이터베이스와 연결된 DAO 인스턴스를 앱에 제공
아래 코드는 내가 NextStep MVVM 과제를 하면서 작성한 코드로 데이터베이스 클래스 의 역할을 하는 코드이다.
private const val DATABASE_NAME = "calculator.db"
@Database(version = 1, entities = [History::class])
abstract class CalculatorDatabase : RoomDatabase() {
abstract fun getHistoryDao(): HistoryDao
companion object {
@Volatile
private var instance: CalculatorDatabase? = null
fun getInstance(context: Context): CalculatorDatabase {
return instance ?: synchronized(this) {
instance ?: buildDatabase(context).also { instance = it }
}
}
private fun buildDatabase(context: Context): CalculatorDatabase {
return Room.databaseBuilder(
context,
CalculatorDatabase::class.java,
DATABASE_NAME
).build()
}
}
}
2. 데이터 항목
앱 데이터 베이스의 테이블을 나타내며 엔티티의 정의
아래 코드는 내가 NextStep MVVM 과제를 하면서 작성한 코드로 Room 항목을 사용하여 '데이터 항목' 의 역할을 하는 코드이다.
테이블의 스키마를 나타내고, DATA 객체 그자체로 쓰이기도 한다.
@Entity(tableName = "history")
data class History(
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val historyId: Int,
@ColumnInfo(name = "formula") val formula: String,
@ColumnInfo(name = "calculate_result") val calculateResult: String
) {
constructor(formula: String, calculateResult: String): this(0, formula, calculateResult)
}
3. 데이터 액세스 개체 - DAO
앱이 데이터베이스의 데이터를 쿼리, 업데이트, 삽입, 삭제를 하는데 사용할 수 있는 메소드를 제공
아래 코드는 내가 NextStep MVVM 과제를 하면서 작성한 코드로 Room 항목을 사용하여 '데이터 엑세스' 의 역할을 하는 코드이다.
즉 CRUD 를 하는 SQL 을 작성하고, Object 로 변환하는 로직을 작성하는 역할을 한다.
@Dao
interface HistoryDao {
@Insert
fun insert(histories: List<History>)
@Query("SELECT * FROM history")
fun getAll(): List<History>
}
Room 테스트 코드
1. data 모듈에서 작성한 테스트 코드
Android 의 구성요소를 사용하기 때문에 AndroidTest 에 작성함.
package edu.nextstep.camp.calculator.data.local
import androidx.room.Room
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class HistoryDaoTest {
private lateinit var database: CalculatorDatabase
private lateinit var historyDao: HistoryDao
private val testHistories = listOf(
History("1 + 1", "2"),
History("1 + 1 * 3", "6")
)
@Before
fun setUp() {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
database = Room.inMemoryDatabaseBuilder(appContext, CalculatorDatabase::class.java).build()
historyDao = database.getHistoryDao()
historyDao.insert(testHistories)
}
@After
fun tearDown() {
database.close()
}
@Test
fun `저장되어_있는_모든_history_목록이_나온다`() {
val actual = historyDao.getAll()
assertThat(actual).containsExactly(History(1,"1 + 1", "2"), History(2,"1 + 1 * 3", "6"))
}
@Test
fun `history가_정상적으로_저장된다`() {
val insertHistory = History(3, "1 + 4 + 6 - 2", "9")
historyDao.insert(listOf(insertHistory))
val actual = historyDao.getAll()
assertThat(actual).contains(insertHistory)
}
}
2. 코루틴 테스트
코루틴 테스트를 위해서는 build gradle 에 testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.2'
추가가 필요하다.
보통 viewModel 안의 코루틴을 사용하는 부분을 테스트할 때, 안드로이드 X 의 ViewMode 라이브러리를 사용하고 있다면 viewModelScope 를 이용해서 코루틴을 launch 시키고 있을 것이다.
이 때 사용되는 Dispatcher 는 Default 이고, Dispatchers.setMain(testDispatcher)
를 이용해 변경할 수 있다.
Dispatcher 를 TestCoroutineDispatcher
로 변경하여 동기적으로 변환하여 테스트를 진행할 수 있다.
상세한 것은 https://medium.com/androiddevelopers/easy-coroutines-in-android-viewmodelscope-25bffb605471 블로그를 참조하는 것이 좋다.
나는 ViewModel 에 ioDispacher 를 외부에서 주입할 수 있게 생성자 주입을 택하고 있기 때문에 아래 코드와 같이 ViewModel 을 생성 시 생성자에 TestCoroutineDispathcer
를 주입해서 테스를 진행했다.
@ExperimentalCoroutinesApi
val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
@Test
fun `저장된_계산한_결과들이_정상적으로_조회된다`() {
val savedHistory = listOf(
History(1,"1 + 1 - 1 * 8", "8"),
History(2,"5 - 1 * 12", "48"),
History(3,"1 + 1 / 1 * 8", "16"),
)
val viewModel = CalculatorViewModel(
calculator = Calculator(),
calculatorRepository = defaultRepository,
ioDispatcher = testDispatcher
)
coEvery { defaultRepository.getHistoryAll() } returns savedHistory
viewModel.showCalculateHistory()
val actual = viewModel.calculateHistory.getOrAwaitValue()
assertThat(actual).containsExactlyElementsIn(savedHistory.map { getStringForDisplay(it) })
}
private fun getStringForDisplay(history: History): String {
return "${history.formula}\n= ${history.calculateResult}"
}
레퍼런스
- ORM
- https://ko.wikipedia.org/wiki/%EA%B0%9D%EC%B2%B4_%EA%B4%80%EA%B3%84_%EB%A7%A4%ED%95%91
- Object Relation Mapping 으로 객체지향 패러다임을 이용하여 데이터 베이스로부터 데이터를 조작할 수 있도록 해주는 기술. 객체와 데이터베이스를 연결(맵핑) 해주는 역할을 함.
- Room 데이터 베이스 테스트
- DAO? (DataAccessObject)
- DAO에는 앱 데이터베이스에 관한 추상 액세스 권한을 제공하는 메서드가 포함되어 있으며, Room은 컴파일 시간에 정의된 DAO 구현을 자동으로 생성.
- 상세 데이터베이스 접근하기 위한 쿼리들을 노출하지 않고, 데이터를 제공하기 때문에 DB 접근 로직과 비즈니스로직을 분리하게 되기 때문에 단일책임원칙을 지원함.
- 위키백과- 데이터 접근객체
- 안드로이드의 Kotlin 코루틴
'Tech & Programming > 모바일(Android, Flutter)' 카테고리의 다른 글
디지털페이지 Flutter 전환 후기 (0) | 2021.11.26 |
---|---|
[Flutter] 웹뷰 하이브리드 모드 관련 이슈 해결 (0) | 2021.11.02 |
플러터 StatelessWidget? StatefulWdiget? (0) | 2021.08.21 |
Google I/O 2021 Keynote 키워드 요약 (0) | 2021.05.27 |
Android Context (0) | 2020.11.13 |