Android

[안드로이드]RoomDB와 SQLite 비교

나맘임 2023. 11. 12. 18:49

RoomDB와 SQLite 

안드로이드에서 로컬 데이터베이스를 구현하는데 크게 두 가지 방식이 존재한다.

RoomDB와 SQLite 이라고 할 순 있는데, 엄연히 말하면 안드로이드에서 자체적으로 SQLite가 내장되어 있고 이 SQLite를 개발자가 사용하기 쉽게 추상화 시켜준 것을 RoomDB라고 말한다.

따라서 RoomDB는 SQLite로 구성되어 있다고 볼 수 있다.

 

하지만 이 글에선 쉽게 차이를 설명하기 위해 SQLite를 RoomDB를 사용하지 않고 접근하는 방식(SQL Helper) 의미로 작성하였다.

 

그러면 SQLite가 뭘까??

그림 1. SQLite 공식 홈페이지 설명

공식 홈페이지 에선 "SQLite는 작고 빠르며 자체 포함, 높은 신뢰성, 모든 기능이 구현된 SQL 데이터베이스 엔진을 실현하는 C 언어 라이브러리" 라고 말한다.

 

SQL 언어를 사용하는 DBMS(DataBase Management System) 소프트웨어로써 매우 높은 신뢰성과 가볍고 빠른 특징을 가지고 있다.

 

이 때문에 SQLite는 정말 많은 디바이스에서 사용한다. 항공 장치부터 각종 휴대폰 단말기까지 폭이 넓다.

 

안드로이드에서도 SQLite가 내장되어 있으며 이 때문에 우리가 어플을 사용하고 종료해도 데이터가 계속 남아있을 수 있다.

 

 

SQLite를 접근할 수 있도록 API를 제시하고 있지만 안드로이드에선 현재  RoomDB를 사용해 SQLite를 사용한다.

 

그러면 기존 SQLite가 얼마나 불편하길래 RoomDB이라는 놈이 등장한 것일까?

 

한번 알아보자

RoomDB와 SQLite를 비교해보자

그림 2. 안드로이드 공식 문서에서 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 Home Page

 

SQLite Home Page

SQLite is a C-language library that implements a small, fast, self-contained, high-reliability, full-featured, SQL database engine. SQLite is the most used database engine in the world. SQLite is built into all mobile phones and most computers and comes bu

www.sqlite.org

SQLite - 나무위키 (namu.wiki)

 

SQLite - 나무위키

이 저작물은 CC BY-NC-SA 2.0 KR에 따라 이용할 수 있습니다. (단, 라이선스가 명시된 일부 문서 및 삽화 제외) 기여하신 문서의 저작권은 각 기여자에게 있으며, 각 기여자는 기여하신 부분의 저작권

namu.wiki

SQLite를 사용하여 데이터 저장  |  Android 개발자  |  Android Developers

 

SQLite를 사용하여 데이터 저장  |  Android 개발자  |  Android Developers

DataStore는 로컬 데이터를 저장하는 최신 방법을 제공합니다. SharedPreferences 대신 DataStore를 사용해야 합니다. 자세한 내용은 DataStore 가이드를 참고하세요. SQLite를 사용하여 데이터 저장 컬렉션을

developer.android.com

[Android] SQLite 사용하기 (velog.io)

 

[Android] SQLite 사용하기

SQLite는 데이터베이스 관리 시스템이다. 서버가 아니라 응용 프로그램에 넣어 사용하는 비교적 가벼운 데이터베이스이다.

velog.io

[안드로이드 스튜디오] Android DB(SQLite) 연동 및 Selecte 쿼리 조회 (tistory.com)

 

[안드로이드 스튜디오] Android DB(SQLite) 연동 및 Selecte 쿼리 조회

안드로이드 앱 내장 DB로 SQLite 데이터베이스가 사용이 되는데, Android Studio에서 SQLiteHelper를 활용한 간단한 DB 연동 및 Cursor 인터페이스를 활용해 Selecte 쿼리로 데이터를 가져오는 간단한 예제를

kim-oriental.tistory.com

Room DB에 대해 (velog.io)

 

Room DB에 대해

Room은 스마트폰 내장 DB에 데이터를 저장하기 위해 사용하는 ORM(Object Relational Mapping)라이브러리입니다. 쉽게 말해서, Room은 데이터베이스의 객체를 코틀린(or 자바)의 객체로 매핑해주는 역할을

velog.io