▩ 목 차 ▩
1. Room
1-1. Room이란 ?
1-2. Room 구조
1-3. Room 사용법
1-3-1. gradle 추가
1-3-2. Entity
1-3-3. DAO(Data Access Object)
1-3-4. Room Database
1-3-4-1. Room Database(싱글톤 패턴 사용 X)
1-3-4-2. Room Database(싱글톤 패턴 사용 O)
1-3-5. Room Database 사용하기
2. Room에 대한 고찰
나는 중고거래앱을 만들었을 당시 Firebased의 realtimebase의 NoSQL DB을 이용해보았고, MySQL DB를 이용해보았다.
==> 이것들은 외장 DB였고, 스마트폰 내장에 저장되는게 아니라 각각의 Tool에 저장을 하고 빼온 것이다.
그렇기에 이번에는 간단하게 MVVM 디자인 패턴을 적용시키는데, 외장 DB를 사용하는것보다는 간단하게 내장 DB를 사용하려고 한다.
==> 스마트폰 내장 DB는 바로 "Room" 이라는 DB 였다.
한번 같이 배워보자.
( Firebase 사용 : https://bj-turtle.tistory.com/19 MySQL 사용 : https://bj-turtle.tistory.com/49 )
■ 1. Room ■
■ 1-1. Room이란 ?
Room은 스마트폰 내장 DB에 데이터를 저장하기 위해 사용하는 라이브러리이다.
예를들어 나만의 어떠한 정보(사용자 정보)를 저장할때는 내장 DB를 사용한다. 구체적인 예시로 웹툰을 즐겨찾기로 해놓는다던가, 메모를 저장한다던가 이런것을 내장 DB를 이용하면 쉽게 작성할 수 있다. 여기에 자동 로그인 여부라던지 포함이 될 수 있는데, 이것은 true/false 값을 저장만 하는것이다. 여기에 Room을 사용하는것은 너무 비효율적일 것이다. 그렇기에 단순한 데이터(소량)일 경우에 sharedpreference를 사용한다.
( 과거에는 SQLite라는 DB를 이용하였으나 사용하기 어렵기 때문에 Room이 등장한 것이였다. 구글에서는 당연히 Room을 사용할 것을 권장하고 있다. )
■ 1-2. Room 구조
Room DB는 ORM(Object - Relational Mapping)으로 동작하며 크게 3가지로 구성되어 있다. (추후에 자세히 설명)
- Room Database : 데이터베이스 홀더를 포함하여 앱의 지속적인 관계형 데이터의 기본 연결을 위한 기본 엑세스 포인트 역할을 한다.
- Entity : 데이터베이스 내의 테이블을 나타낸다.
- DAO(Data Access Object) : 데이터베이스에 접근하는데 사용하는 메서드가 포함되어 있다.
■ 1-3. Room 사용법
■ 1-3-1. gradle 추가
그냥 코드를 작성하는 곳에 Room을 입력하고 Alt+Enter(오류 찾아주는 기능)을 해서 import를 하는 방법이 가장 빠르고 쉽다. 여기서 2가지가 나오는데 androidx.room.Room 을 import 해야 한다.
import를 하게 되면 gradle에 추가가 된 것을 확인 할 수 있다.
■ 1-3-2. Entity
위에서 말했듯이 데이터베이스 내의 테이블을 나타낸다. 쉽게 말을 하자면, 관련이 있는 속성들이 모여 하나의 정보 단위를 이룬 것이다.
( Entity(개체)와 Object(객체)는 비슷해 보이지만 다른 의미이다. 객체는 개체를 포함한 더 큰 개념이며, 대상에 대한 정보뿐만 아니라 동작, 기능, 절차 등을 포함하는 것이 객체이다. )
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class User(
var name: String,
var age: String,
var phone: String
){
@PrimaryKey(autoGenerate = true) var id: Int = 0
}
위의 코드는 Entity를 생성하는 코드이다.( DB에서 테이블을 만든다고 생각하면 된다.)
data class에 @Entity 어노테이션을 붙여주고 저장하고 저장하고 싶은 속성의 변수 이름과 타입을 정해준다.
PrimaryKey는 키 값으로 유일한(Unique)값 이어야 한다. ( 직접 지정해도 되지만 autoGenerate)를 true로 주면 자동으로 값을 생성한다. )
■ 1-3-3. DAO(Data Access Object)
DAO는 Data Access Object의 줄임말로, 데이터에 접근할 수 있는 메서드를 정의해놓은 인터페이스이다.
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Update
@Dao
interface UserDao {
@Insert
fun insert(user: User)
@Update
fun update(user: User)
@Delete
fun delete(user: User)
}
Dao도 마찬가지로 어노테이션을 이용해 작성을 한다. @Dao 어노테이션을 작성을 하고 그 안에 메서드를 정의하면 되는데,
@Insert를 붙이면 테이블에 데이터 삽입
@Update를 붙이면 테이블의 데이터 수정
@Delete를 붙이면 테이블의 데이터 삭제 이다.
주의할 점은 class가 아니라 interface라는 점이다.
만약에 위에 있는 기능인 삽입, 수정, 삭제 외에 다른 기능을 만들고 싶다면 어떻게 하면 될까?
==> 예를 들어 특정 값 불러오기 혹은 테이블 전체 값 가져오기를 하고 싶은 경우, @Query 어노테이션을 붙이고 그 안에 어떤 동작을 할 건지 SQL 문법으로 작성을 해주면 된다.
( 내가 MySQL을 이용하면서 간단하게 공부한 SQL이다. 참고하면 좋을 것이다. https://bj-turtle.tistory.com/37 )
@Query("SELECT * FROM User") //테이블의 모든 값 가져오기
fun getAll(): List<User>
@Query("DELETE FROM User WHERE name = :name")
fun deleteUserByName(name: String)
■ 1-3-4. Room Database
■ 1-3-4-1. Room Database(싱글톤 패턴 사용 X)
이번 목차는 데이터베이스를 생성하고 관리하는 데이터베이스 객체 만들기 위해서 추상 클래스를 통해 만들어 줘야 한다.
우선 RoomDatabase 클래스를 상속받고, @Database어노테이션을 통해 데이터베이스임을 표시한다.
@Database 어노테이션 괄호 안에 entities가 있는데 여기에 위에서 만든 entity를 넣어주면 된다. version은 앱을 업데이트하다가 entity의 구조를 변경했을 때 이전 구조와 현재 구조가 다르다 라는 것을 알려주기 위해 즉, 구분해주기 위한 역할을 한다. 만약 구조가 바뀌었는데 버전이 같다면 에러가 뜨면서 디버깅이 되지 않을 것이다. 우리는 처음 데이터베이스를 생성하는 상황이기때문에 그냥 1을 넣어주면 될 것이다.
import androidx.room.Database
import androidx.room.RoomDatabase
@Database(entities = [User::class], version = 1)
abstract class UserDatabase: RoomDatabase() {
abstract fun userDao(): UserDao
}
또한 하나의 데이터베이스 안에서 여러 개의 entity(테이블)을 가져야 한다면 arrayOf() 함수를 통해 콤마로 구분해서 entity를 넣어주면 된다.
import androidx.room.Database
import androidx.room.RoomDatabase
@Database(entities = arrayOf(User::class,Student::class), version = 1)
abstract class UserDatabase: RoomDatabase() {
abstract fun userDao(): UserDao
}
■ 1-3-4-2. Room Database(싱글톤 패턴 사용 O)
그리고 안드로이드 공식문서(https://developer.android.com/training/data-storage/room?hl=ko)에 따르면 데이터베이스 객체를 인스턴스할 때 싱글톤으로 구현을 하여 효율적으로 사용하라고 권장하고 있다.
==> 여러 인스턴스에 액세스 할 필요가 없을 뿐더라, 객체 생성에 비용이 많이 들기 때문에
그렇기에 우리는 싱글톤 패턴을 구현하기 위해서 우리는 추상 클래스 안에다가 싱글톤 객체를 생성해야 하기 때문에 companion object로 객체를 선언해서 사용하면 될 것이다. ( 만약 싱글톤으로 구현하지 않을 것이라면 아래의 코드 부분을 내가 호출할 부분에서 사용하면 될 것이다.)
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database(entities = [User::class], version = 1)
abstract class UserDatabase: RoomDatabase() {
abstract fun userDao(): UserDao
companion object {
private var instance: UserDatabase? = null
@Synchronized
fun getInstance(context: Context): UserDatabase? {
if(instance == null) {
synchronized(UserDatabase::class.java){
instance = Room.databaseBuilder(
context.applicationContext,
UserDatabase::class.java,
"user-database"
).build()
}
}
return instance
}
}
}
위의 코드는 데이터베이스 객체를 싱글톤 객체(companion object 이용)로 구현하였다.
코드를 보게되면, UserDatabase 클래스 타입의 instace변수를 null로 초기화를 시켜주고, getInstance()메소드를 만든다. 이때 instance를 위에서 바로 null로 초기화 해줬기 때문에 instance가 null이라면 databaseBuilder라는 static 메서드를 통해 데이터베이스 객체를 생성한다. databaseBuilder() 메소드 안에는 context와 database클래스와 database를 저장할 때 사용할 데이터베이스의 이름을 정해서 넘겨주면 된다. (context,database.class,"database 이름")
■ 1-3-5. Room Database 사용하기
싱글턴 패턴을 사용했을때와 사용하지 않았을때와의 사용 방법이 다르다. 구글에서 싱글톤 패턴을 권장했으니 Room Database를 싱글톤 패턴을 적용시키고 그것을 사용하는것을 추천한다.
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.room.Room
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
var newUser = User("홍길동", "20", "010-1234-1234")
// //싱글톤 패턴을 사용하지 않은경우
// val db = Room.databaseBuilder(
// applicationContext,
// UserDatabase::class.java,
// "user-database"
// ).build()
// db.userDao().insert(newUser)
//싱글톤 패턴을 사용한 경우
val db = UserDatabase.getInstance(applicationContext)
db!!.userDao().insert(newUser)
}
}
위와 같이 코드를 작성하게 되면, Userdad 인터페이스에서 만든 insert 메서드를 통해 newUser가 데이터베이스에 추가된다.
여기서 끝난 것 같지만, 실행시켜보면 "Cannot access database on the main thread since it may potentially lock the UI for a long period of time" 에러가 뜬다.
==> 오류가 뜨는 이유는, 오래 걸리는 작업이므로 다른 곳에서 작업하라는 의미로 오류가 뜬다. ( 이것은 비동기 개념인 코루틴과 관련이 깊은데 코루틴의 자세한 내용은 다음에 한번 익혀보도록 하자.. )
//싱글톤 패턴을 사용한 경우
val db = UserDatabase.getInstance(applicationContext)
CoroutineScope(Dispatchers.IO).launch {
db!!.userDao().insert(newUser)
}
위와 같이 코루틴을 이용하여 코드를 작성해주면 오류 없이 데이터베이스에 내용이 들어간 것을 알 수 있다.
( 이때까지만 해도 그냥 예제를 보고 작성을 하였고, 추후에 나혼자 데이터베이스에 넣어져 있는 데이터를 확인하기 위해 textview에 넣어 확인하려고 했는데 오류가 떴다.. 아래에서 계속 )
나는 간단하게 데이터베이스에 넣어져 있는 내용을 TextView로 출력하고 싶어서 나는 아래의 코드를 작성을 하여 DB에 저장된 내용을 출력하고 싶었다. (myText 변수는 TextView 라고 생각하면 된다.)
myText.text = db!!.userDao().getAll().toString()
하지만 오류가 발생하였고, 위에서 발생한 오류의 내용과 같다. 그래서 다시 한번 정확하게 이 오류에 대해 찾아보고 해결방법이 무엇이 있나 확인을 해보았다.
Cannot access database on the main thread since it may potentially lock the UI for a long period of time.
즉, 구체적으로 오류의 원인은 안드로이드는 데이터베이스 쿼리가 메인스레드를 점유할 것을 염려하여 메인스레드가 아닌 다른 스레드에서 쿼리를 실행할 것을 강요하기 때문이다.
찾아보니 해결 방법은 3개이다.
- 메인스레드에서 강제 실행
- 새로운 스레드 생성
- 코루틴 사용
위에서의 1번 방법은 안드로이드가 권장하지 않는 방법이며, 2번 방법은 시간이 오래걸려 3번 방법이 나오게 되었으므로 3번 방법을 이용하는 것이였다.
위에서 물론 오래 걸리는 작업이므로 다른 곳에서 작업 하라고 해서 코루틴을 이용한다고 했으나, CoroutineScope 내부에 작성을 하는것이 코루틴을 이용하는것인지 몰랐고, 다시 한번 코루틴에 대해서 상기했다.
==> 즉, 데이터베이스에 관련된 작업을 하기 위해선 각각의 작업마다 코루틴(CoroutineScope()함수)를 이용하여 작업을 해야 한다는 것이다.
내가 원하는 작업은 DB에 저장된 내용을 가져오는것과 DB 테이블로부터 내가 원하는 name을 찾아서 지우는 작업을 하기 위해선 아래와 같이 해야 한다는 것이다.
//싱글톤 패턴을 사용한 경우
val db = UserDatabase.getInstance(applicationContext)
CoroutineScope(Dispatchers.IO).launch {
db!!.userDao().insert(newUser)
}
CoroutineScope(Dispatchers.IO).launch {
db!!.userDao().deleteUserByName("홍길동")
myText.text = db.userDao().getAll().toString()
}
■ 2. Room에 대한 고찰 ■
Room에 대해 공부를 해보았는데, 안드로이드에서는 Room이 메인 쓰레드에 침범을 하여 많은 것들을 점유할 것을 미리 방지하고자 강제로 메인 쓰레드에 쓰게 하지 않는 이상 사용을 하지 못한다는 것을 깨달았다. 아예 이러한 생각조차 없었기 때문에 오류가 발생했을때 뭐지? 왜 안돼!!! 이러한 생각이였으나, 오류에 대해 찾아보니 당연히 그래야만 했던 것이다.
이러한 오류를 해결하기 위해서 아주 좋은 방법이 있었으니..! 그것은 바로 비동기를 위해 있는 코루틴이라는 것이였으니! 많이는 들어봤으나 이런곳에 쓰이는 줄 몰랐다. 그래서 다음 공부할 주제를 코루틴으로 정했고, 맛만 보고 다시 mvvm 패턴에 많은 것들을 적용 시켜 보겠다...