저번 포스팅에 이어서 정리하도록 하겠다.
# 알게된 것
-- 👇 이번 포스팅 --
- Room DB사용하기
- Thread 만들기
-- 👇 다음 포스팅 --
- runOnUIThread 사용하기
- LayoutInflater 이용하여 xml 적용
# Room
## Room은 무엇일까
안드로이드에서 SharedPreference를 사용해서 간단한 정보를 저장할 수 있으나, 정보가 커지고, 많아지고, 관계가 생김에 따라서 이것들을 처리하기가 까다로워 진다. 다양한 서비스에서 그렇듯이 DB(데이터베이스)를 사용해서 정보를 저장해야한다
안드로이드는 SQLite를 사용한다. 당연히 모든 DB가 그렇듯이 쿼리문을 하나씩 작성하면서 사용하다보면 불편하기도하고 관리하기도 힘들어질 것이다. 그래서 안드로이드가 제공하는 Room을 사용해서 체계화시켜 편리하게 사용할 수 있다.
쉽게 말해 안드로이드 핸드폰(로컬)의 DB를 보다 쉽게 사용할 수 있게 해준다.
## Room을 사용하기 위한 셋팅
Room은 외부 라이브러리이기 때문에 다운받아서 사용해야한다. 친절하게 안드로이드 공식문서(하단첨부)에서는 어떻게 셋팅을 해야하는지 알려준다.
build.gradle에 다음과 같이 추가해준다
plugins {
//추가
id 'kotlin-kapt'
}
dependencies {
//추가
implementation "androidx.room:room-runtime:2.4.2"
kapt "androidx.room:room-compiler:2.4.2"
}
그리고 async를 누르면 필요한 라이브러리를 싱크해준다. 이제 Room을 사용 할 수 있다.
## Room의 기본 요소
- entity (model정의 ) => 테이블이다. 또한 삽입될 데이터를 형식이라고 할 수 있다.
- Dao(Data Access Objects) => 쿼리문 담당
- Room 데이터베이스 => DB를 만들고 Dao와 연결시켜줌
이렇게 패키지를 만들어 구분했다. 또한 작은 순서부터 큰 걸 구현하는 형식으로 만들었다. 즉 model -> dao -> DataBase순으로 만들었다.
## Entity ( model )
@Entity
data class History (
@PrimaryKey val uid:Int?,
@ColumnInfo(name = "expression") val expression:String?,
@ColumnInfo(name = "result") val result: String?
)
위 코드는 아래의 표와 같이 DB에 생성되는 하나의 테이블이라고 생각하면된다. @(어노테이션)을 이용하여 @Entity를 써줘야 하며, 내부에 @PrimaryKey와 같은 어노테이션을 만들어 해당 칼럼의 역할을 알려 줄 수 있다. 이 외에도 다양한 어노테이션이 있다. 공식문서를 참고하자
uid | expression | result |
@Entity(tableName = "users")
와 같이 다양한 옵션이 존재한다.
### data class
그렇다면 data class가 뭘까라는 의문이 생길 수 있는데, class를 조금 더 간편하게 만들어 주는 역할을 한다. getter(), setter()와 같은 것을 자동으로 생성해주는 역할이라고 한다. 자세히는 따로 공부하여 정리하도록 하겠다.
## Dao(Data Access Objects)
Dao는 쿼리문이라고 생각하면 된다. 내가 원하는 쿼리문을 작성하여 Activity에서 사용하게 해준다. 또 기본적으로 DELETE INSERT UPDATE와 같은 것을 제공해준다. interface로 구현하며 @Dao어노테이션을 추가한다. 아래와 같이 사용하면 된다.
@Dao
interface HistoryDao {
@Query("SELECT * FROM history")
fun getALl(): List<History>
@Insert
fun insertHistory(history: History)
@Query("DELETE FROM history")
fun deleteAll()
// 들어온 데이터만 지워줌
@Delete
fun delete(history: History)
// 특정한 것만 반환시켜줌
@Query("SELECT * FROM history WHERE result LIKE :result LIMIT 1")
fun findByResult(result: String) : History
// 필터링과 같이 해당하는 걸 모두 List반환
@Query("SELECT * FROM history WHERE result LIKE :result")
fun findByResult(result: String) : List<History>
}
## RoomDataBase
실질적으로 데이터베이스를 만들고 그 데이터베이스의 Dao를 연결해주는 추상클래스다.
@Database어노테이션에 entities를 아래와같이 필수적으로 입력해주고, version을 표기해준다. version은 DB가 수정됨에 따라서 관리해주는데 용이하게 해준다.
@Database(entities = [History::class], version = 1)
abstract class AppDataBase : RoomDatabase() {
abstract fun historyDao(): HistoryDao
}
참고 코드 : <공식문서는 아래와 같이 사용 가능함을 예를들어 표기해준다 >
// Song and Album are classes annotated with @Entity.
@Database(version = 1, entities = {Song.class, Album.class})
abstract class MusicDatabase : RoomDatabase {
// SongDao is a class annotated with @Dao.
abstract fun getSongDao(): SongDao
// AlbumDao is a class annotated with @Dao.
abstract fun getArtistDao(): ArtistDao
// SongAlbumDao is a class annotated with @Dao.
abstract fun getSongAlbumDao(): SongAlbumDao
}
## 사용하기
이제 Room에 필요한 모든 것을 다 만들었다. 생각보다 간단하고 별거 없었다. 위에서 말했던 것 처럼 아래에서 위로 작은개념에서 큰개념으로 올라오면서 만들었더니 생각에 흐름이 좋았다.
이제 데이터베이스를 사용할 차례이다. 내가 만든 계산기에서는 계산연산이 끝나면 그 값이 DB에 저장되고, 기록보기 버튼을 누르면 ScrollView에 DB에 있는 값들을 불러오게 된다. 그리고 기록들을 지우는 버튼도 존재한다.
즉,
- DB에 데이터 삽입
- DB에서 모든 데이터 불러오기
- DB에 있는 모든 데이터 지우기
이 필요하고, 이기능들을 Dao에 정리했던 것이다.
DB에 접근하여 정의한 Dao사용하기 위한 순서는 다음과 같다.
- RoomDataBase연결 ( Build )
- 상황에 따른 데이터 삽입, 불러오기, 지우기 in Thread
데이터 베이스 관련된 작업은 Thread에서 하는 것이 좋다고 했다. 그래서 Thread를 만들어주고 그 안에서 Runnable에 행동을 담아서 실행시킨다. 대신 UI를 변경해주는 작업은 main UI Thread에서만 수행해야한다. 그렇기 때문에 runOnUiThread에서 UI가 변경되는 작업을 수행시킨다.
### RoomDataBase연결
lateinit var db : AppDataBase // AppDataBase라는 추상클래스를 만들었기 때문에 그에맞는 반환형을 가짐
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//데이터 베이스
db = Room.databaseBuilder(
applicationContext,
AppDataBase::class.java,
"historyDB"
).build()
}
이제 onCreate가 되면 db에 데이터베이스가 연결된다.
### Dao를 이용해 데이터베이스에 삽입
관계없는 코드는 간추렸다.
fun resultButtonClicked(view: View) {
val expressionText = expressionTextView.text.toString()
val resultText = calculateExpression()
//TODO 디비에 넣어줘야함
Thread(Runnable {
db.historyDao().insertHistory(History(null,expressionText,resultText))
}).start()
}
첫번째(null)은 우리가 primary로 지정했기 때문에, null을 보내도 자동으로 생선된다.
// 엔티티에서 정의했던 형식임
History(null,expressionText,resultText)
저렇게 되면 db에 insert된다.
### Dao를 이용해 데이터베이스에서 정보 가져오기
똑같은 방식이지만, Dao에서 불러온 데이터를 가지고 UiThread로 그려주는 코드가 담겨있다.
fun historyButtonClicked(view: View) {
historyLinearLayout.removeAllViews()
historyLayout.isVisible = true
// TODO 디비에서 모든 기록 가져오기
// TODO 가져온거 그려주기
Thread(Runnable {
db.historyDao().getALl().reversed().forEach {
//아마도 쓰레드에서 UI를 그려주는 작업을 하면 안될거 같음 -> UI쓰레드 열어줌
runOnUiThread{
val historyView = LayoutInflater.from(this).inflate(R.layout.history_row,null,false)
historyView.findViewById<TextView>(R.id.expressionTextView).text = it.expression
historyView.findViewById<TextView>(R.id.resultTextView).text = " = ${it.result}"
historyLinearLayout.addView(historyView)
}
}
}).start()
}
### Dao를 이용해 데이터베이스에 지우기
fun historyClearButtonClicked(view: View) {
historyLinearLayout.removeAllViews()
// TODO 디비에서 모든 기록 삭제
Thread(Runnable {
db.historyDao().deleteAll()
}).start()
}
# 느낀 점
인터페이스와 추상화 클래스, 데이터 클래스와 같은 개념을 확실히 정립할 필요가 있다고 느꼈다. 생각보다 간단했다. Dao에 알맞은 쿼리를 작성하는것이 중요할 거 같으며, 여러 관계과 형성 됐을때 얼마나 잘 관리하는지가 관건일거 같다.
LayoutInflater에 대해서는 다음 포스팅에서 공부해보고 업로드 하도록 하겠다.
# 궁금한점
버튼 이벤트에 쓰레드 생성하여 런어블을 전달하게 되는데 그러면, 버튼을 누를때마다 쓰레드가 생성되는것 아닌가? 그렇다면 쓰레드가 너무 많이 생겨서 효율이 별로일거같은데 쓰레드를 죽여주는 동작은 하지 않아도 되는 것인가? 또는 쓰레드는 자동으로 닫히는 것인가?
# 참고
👇 Room
👇 entity
👇 Dao