RoomDB와 SQLite
안드로이드에서 로컬 데이터베이스를 구현하는데 크게 두 가지 방식이 존재한다.
RoomDB와 SQLite 이라고 할 순 있는데, 엄연히 말하면 안드로이드에서 자체적으로 SQLite가 내장되어 있고 이 SQLite를 개발자가 사용하기 쉽게 추상화 시켜준 것을 RoomDB라고 말한다.
따라서 RoomDB는 SQLite로 구성되어 있다고 볼 수 있다.
하지만 이 글에선 쉽게 차이를 설명하기 위해 SQLite를 RoomDB를 사용하지 않고 접근하는 방식(SQL Helper) 의미로 작성하였다.
그러면 SQLite가 뭘까??
공식 홈페이지 에선 "SQLite는 작고 빠르며 자체 포함, 높은 신뢰성, 모든 기능이 구현된 SQL 데이터베이스 엔진을 실현하는 C 언어 라이브러리" 라고 말한다.
SQL 언어를 사용하는 DBMS(DataBase Management System) 소프트웨어로써 매우 높은 신뢰성과 가볍고 빠른 특징을 가지고 있다.
이 때문에 SQLite는 정말 많은 디바이스에서 사용한다. 항공 장치부터 각종 휴대폰 단말기까지 폭이 넓다.
안드로이드에서도 SQLite가 내장되어 있으며 이 때문에 우리가 어플을 사용하고 종료해도 데이터가 계속 남아있을 수 있다.
SQLite를 접근할 수 있도록 API를 제시하고 있지만 안드로이드에선 현재 RoomDB를 사용해 SQLite를 사용한다.
그러면 기존 SQLite가 얼마나 불편하길래 RoomDB이라는 놈이 등장한 것일까?
한번 알아보자
RoomDB와 SQLite를 비교해보자
공식 문서를 들어가자마자 떡하니 SQLite API 사용 주의문을 적어놨다.
그림 2와 같이 공식 문서에선 크게 두 가지 이유로 RoomDB 사용을 권유하고 있다.
1. SQL 쿼리와 데이터 객체 간에 변환하려면 많은 상용구 코드를 사용해야 한다.
2. 원시 SQL 쿼리에 관한 컴파일 시간 확인이 없다.
이에 대해 이제부터 알아보겠다.
RoomDB와 SQLite의 차이점 1. 많은 상용구 코드를 사용해야 한다.
이게 얼마나 차이가 나는지 코드 구성을 확인해보면 된다.
(1) 데이터베이스 생성
SQLite
class MembersDbHelper(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION){
override fun onCreate(db: SQLiteDatabase?) {
db?.apply {
execSQL("DROP TABLE IF EXISTS Members")
execSQL("create table Members (mID integer primary key autoincrement, Name text, Age integer);")
}
}
override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
}
companion object {
// If you change the database schema, you must increment the database version.
const val DATABASE_VERSION = 1
const val DATABASE_NAME = "members.db"
}
}
RoomDB
@Entity
data class Member(
val name: String,
val age: Int,
){
@PrimaryKey(autoGenerate = true) var id: Int = 0
}
@Database(
entities = [Member::class],
version = 1,
exportSchema = false
)
abstract class MemberDatabase : RoomDatabase() {
abstract fun memberDao() : MemberDao
companion object{
private var instance : MemberDatabase? = null
@Synchronized
fun getInstance(context: Context) : MemberDatabase? {
if (instance == null){
synchronized(MemberDatabase::class){
instance = Room.databaseBuilder(
context.applicationContext,
MemberDatabase::class.java,
"memberdatabase"
).build()
}
}
return instance
}
}
}
데이터베이스 생성에서
SQLite는 SQL문을 사용해서 데이터베이스를 생성하지만,
RoomDB는 추상화가 되어있어 추상 클래스와 인터페이스, 데이터 클래스등을 사용하여 데이터베이스를 생성한다.
(2) SQL 문 - SELECT
"SELECT * from [테이블 명]" 으로 한번 차이를 확인해보겠다.
SQLite
fun getAllMember() :Array<String>{
val db = this.readableDatabase
val cursor = db.rawQuery("SELECT * FROM Members",null)
val result = ArrayList<String>()
while (cursor.moveToNext()){
val line = ArrayList<String>()
for (i in 0 .. 2){
line.add(cursor.getString(i))
}
result.add(line.joinToString())
}
cursor.close()
return result.toTypedArray()
}
RoomDB
@Dao
interface MemberDao {
@Query("Select * From Member")
fun getAllMembers() : LiveData<List<Member>>
}
딱 봐도 너무 다르다.
이는 SQLite의 select 문 읽는 방식에서 오는 것인데,
Cursor라는 객체를 이용하여 읽는다.
이 Cursor는 기본적으로 각 row의 첫번째 col부터 가리킨다.
while (cursor.moveToNext()){
val line = ArrayList<String>()
for (i in 0 .. 2){
line.add(cursor.getString(i))
}
result.add(line.joinToString())
}
그렇기 때문에 Cursor.get 메소드를 이용하여 인덱스(col 값)을 집어넣어
한 row의 모든 col 값을 가져올 수 있게 되는 것이다.
이에 반면 Room은 어노테이션 안에 SQL 문을 사용하고 함수의 반환값만 설정해주면 알아서
SQL 구문을 실행하여 결과값을 반환한다.
(3) SQL 문 - INSERT
SQLite
fun insertMember(name : String, age: Int) {
val db = writableDatabase
val values = ContentValues().apply {
put("Name",name)
put("Age",age)
}
db.insert("Members", null, values)
}
insert가 실패하면 -1을 리턴한다.
RoomDB
@Insert
fun insertMember(member: Member)
이 또한 차이가 난다.
RoomDB에선 SQLite와 다르게 @Insert 어노테이션만 있다면 자동으로 Insert를 진행해준다.
하지만 SQLite는 하나하나 객체들을 만들어서 insert를 시켜줘야하며
각 테이블의 table, col 명을 정확히 알고 있어야 한다.
(3) SQL 문 - DELETE
SQLite
fun deleteMember(name:String){
val db = writableDatabase
val selection = "Name = ?"
val selectionArgs = arrayOf("${name}")
db.delete("Members",selection,selectionArgs)
}
RoomDB
@Query("DELETE From Member")
fun deleteLogin(member: Member)
SQLite에선 특정 값을 지우기 위해 where 절을 사용해야하나
RoomDB에선 인자로 Entity를 전달만 해주면 알아서 지워준다.
RoomDB와 SQLite의 차이점 2. SQL 쿼리에 관한 컴파일 시간에 확인이 없다.
이는 어노테이션의 존재 유무 + 추상화 때문에 차이가 발생하며,
SQLite는 쿼리문이 잘못됐는지 컴파일러가 확인할 수가 없다!!
이 때문에, 개발자는 테이블 구조가 변동되었을 때,
그곳에 접근하고자 하는 쿼리문이 어딘가에 존재할 때 오류를 찾기가 정말 어렵다.
이를 예시를 들어 설명해보겠다.
기존 테이블
override fun onCreate(db: SQLiteDatabase?) {
db?.apply {
execSQL("DROP TABLE IF EXISTS Members")
execSQL("create table Members (mID integer primary key autoincrement, Name text, Age integer);")
execSQL("INSERT INTO Members VALUES (1,'Kim',20);")
execSQL("INSERT INTO Members VALUES (2,'Lee',30);")
execSQL("INSERT INTO Members VALUES (3,'Park',40);")
}
}
변경된 테이블
override fun onCreate(db: SQLiteDatabase?) {
db?.apply {
execSQL("DROP TABLE IF EXISTS Members")
execSQL("create table Members (mID integer primary key autoincrement, Name text, Age integer, Job text);")
execSQL("INSERT INTO Members VALUES (1,'Kim',20);")
execSQL("INSERT INTO Members VALUES (2,'Lee',30);")
execSQL("INSERT INTO Members VALUES (3,'Park',40);")
}
}
변경된 테이블 Select ALL
fun getAllMember() :Array<String>{
val db = this.readableDatabase
val cursor = db.rawQuery("SELECT * FROM Members",null)
val result = ArrayList<String>()
while (cursor.moveToNext()){
val line = ArrayList<String>()
for (i in 0 .. 3){
line.add(cursor.getString(i))
}
result.add(line.joinToString())
}
cursor.close()
return result.toTypedArray()
}
만약 위와 같은 상황이 벌어졌다면 SQL 문이 잘못 됐으므로 컴파일러가 알려줘야 하는게 정상이다.
하지만 그대로 아무 문제 없이 실행되고 null 값이 job에 들어간다.
그렇게 select ALL로 접근하게 되면 null을 받기 때문에 오류가 발생한다.
이는 작은 예시라 문제가 크게 와닿지 않을 수 있겠지만 프로그램이 커질 수록 오류를 찾긴 매우 힘들어진다.
참고 문헌
SQLite를 사용하여 데이터 저장 | Android 개발자 | Android Developers
[Android] SQLite 사용하기 (velog.io)
[안드로이드 스튜디오] Android DB(SQLite) 연동 및 Selecte 쿼리 조회 (tistory.com)
'Android' 카테고리의 다른 글
[안드로이드]Retrofit(okhttp)의 WebSocket에 대하여 (1) | 2024.01.07 |
---|---|
[안드로이드] 어노테이션(Annotation) 개념과 예시 (2) | 2023.11.19 |
[안드로이드] 단위 테스트를 도와주는 JUnit 5 (2) | 2023.10.29 |
[안드로이드]Glide에 대하여 (0) | 2023.10.08 |
[안드로이드]Retrofit2 에 관하여 (0) | 2023.09.17 |