일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- 멤버변수
- 2022 플러터 안드로이드 스튜디오
- 서비스 쓰레드 차이
- 안드로이드 레트로핏 사용법
- 안드로이드 os 구조
- jvm 작동 원리
- android ar 개발
- 큐 자바 코드
- ar vr 차이
- rxjava cold observable
- 안드로이드 유닛 테스트 예시
- Rxjava Observable
- 안드로이드 유닛 테스트
- 2022 플러터 설치
- 서비스 vs 쓰레드
- 안드로이드 라이선스
- 안드로이드 유닛테스트란
- 안드로이드 레트로핏 crud
- 안드로이드 라이선스 종류
- 클래스
- ANR이란
- 스택 큐 차이
- 자바 다형성
- android retrofit login
- jvm이란
- 스택 자바 코드
- 플러터 설치 2022
- rxjava disposable
- 객체
- rxjava hot observable
- Today
- Total
나만을 위한 블로그
[Android] SQLite vs Room DB 비교 및 구현 - 1 - 본문
간단한 데이터 저장은 쉐어드 프리퍼런스, dataStore로 할 수 있지만 좀 복잡한 데이터면 다른 방법을 쓰는 게 낫다.
그 방법으로 떠오르는 게 SQLite, Room인데 안드로이드 디벨로퍼에선 Room DB 사용을 더 권장하고 있다.
사실 대놓고 Room 쓰라고 광고하고 있다. 하단의 하이퍼링크를 클릭하면 SQLite 문서로 이동하긴 하지만 도입부에 주의 문구를 표시하고 있다.
어떤 단점이 있어서 SQLite 대신 Room을 쓰라고 하는데, 디벨로퍼가 말하는 단점은 딱 와닿지 않는다. 써보지 않았다면 더 모를 수 있다.
이 포스팅에선 SQLite부터 시작해서 두 요소가 뭔지 간단하게 확인하고 어떻게 사용하는지 간단한 예시를 보면서 각각 어떤 특징을 갖는지 확인한다.
먼저 SQLite부터 확인한다. SQLite는 별도의 공식 홈페이지가 있으니 여길 먼저 확인한다.
참고로 SQLite는 사람마다 읽는 법이 다 다르다. 에스큐엘라이트, 에스큐라이트, 시퀄라이트 등등이 있는데 위키백과 기준 영어권에선 에스큐엘라이트 or 시퀄라이트라고 읽는다고 하니 참고한다.
About SQLite
sqlite.org
SQLite는 독립형, 서버리스, 제로 구성, 트랜잭션 SQL DB 엔진을 구현하는 in-process 라이브러리다. SQLite의 코드는 공용 도메인에 있으며 어떤 용도로든 무료로 쓸 수 있다. SQLite는 세계에 가장 많이 배포된 DB로 여러 유명 프로젝트를 포함해 많은 앱에 쓰인다
SQLite는 임베디드 SQL DB 엔진이다. 대부분의 다른 SQL 데이터와 달리 SQLite엔 별도의 서버 프로세스가 없다. SQLite는 일반 디스크 파일을 직접 읽고 쓴다. 여러 테이블, 인덱스, 트리거, 보기가 포함된 완전한 SQL DB가 단일 디스크 파일에 포함돼 있다. DB 파일 형식은 크로스 플랫폼이므로 32bit, 64bit 시스템 간 또는 빅엔디안, 리틀엔디안 아키텍처 간에 DB를 자유롭게 복사할 수 있다. 이런 기능 덕분에 SQLite는 앱 파일 형식으로 널리 쓰인다. SQLite DB 파일은 미국 국회도서관에서 권장하는 저장 형식이다. 오라클을 대체하는 게 아니라 fopen()을 대체하는 거라고 생각하라
SQLite는 컴팩트한 라이브러리다. 모든 기능을 활성화하면 대상 플랫폼, 컴파일러 최적화 설정에 따라 라이브러리 크기가 750KiB 미만이 될 수 있다. 일반적으로 SQLite는 메모리 사용량이 많을수록 더 빨리 실행된다
https://en.wikipedia.org/wiki/SQLite
SQLite - Wikipedia
From Wikipedia, the free encyclopedia Serverless relational database management system SQLite (,[4][5] [6]) is a free and open-source relational database engine written in the C programming language. It is not a standalone app; rather, it is a library that
en.wikipedia.org
SQLite는 C언어로 작성된 무료 오픈소스 관계형 DB 엔진이다. 독립형 앱이 아니라 소프트웨어 개발자가 앱에 내장하는 라이브러리다. 따라서 임베디드 DB 제품군에 속한다. 주요 웹 브라우저, OS, 핸드폰, 기타 임베디드 시스템에서 쓰이며 가장 널리 배포된 DB 엔진이다
많은 프로그래밍 언어가 SQLite 라이브러리에 바인딩을 갖고 있다. 일반적으로 PostgreSQL 구문을 따르지만 기본적으로 타입 검사를 적용하지 않는다. 예를 들어 정수로 정의된 column에 문자열을 넣을 수 있단 뜻이다. 가벼운 임베디드 DB지만 SQLite는 트랜잭션, ACID 보장을 포함해 대부분의 SQL 표준과 관계형 모델을 구현한다. 그러나 구체화된 보기, 트리거, ALTER TABLE 문의 완벽 지원 등 다른 DB에 구현된 많은 기능이 생략돼 있다...(중략)...SQLite는 대부분 SQL DB 시스템에서처럼 column에 타입을 할당하는 대신 개별 값에 타입을 할당하고 언어적으로는 동적으로 타입을 지정하는 특이한 타입 시스템을 쓴다. 또한 약하게 유형화되어 정수 column에 문자열을 넣을 수 있다. 이렇게 하면 동적 입력된 스크립팅 언어에 바인딩할 때 column에 유연성을 더할 수 있다
정리하면 SQLite는 C언어로 만들어진 오픈소스 관계형 DB 엔진이고 별도의 앱이 아닌 내장 라이브러리 형태로 여러 곳에서 쓰이는 것을 알 수 있다. 안드로이드의 경우 핸드폰 내부에 저장하기 때문에 당연히 별도의 서버는 필요없고 SQLite DB 구현이 기본 제공된다.
이 SQLite를 안드로이드에서 구현하는 방법은 아래와 같다. 저장할 데이터는 Person data class다.
data class Person(
var id: Long = 0,
var name: String,
var age: Int,
var height: Double,
var address: String
)
먼저 Helper 클래스를 만든다. 앞서 말했듯 안드로이드가 기본 제공하기 때문에 앱 수준 gradle에 라이브러리를 추가할 필요 없이 바로 쓸 수 있다.
import android.content.Context
import android.database.sqlite.SQLiteOpenHelper
class PersonDatabaseHelper(context: Context) : SQLiteOpenHelper(
context, DATABASE_NAME, null, DATABASE_VERSION
) {
companion object {
private const val DATABASE_NAME = "person_database.db"
private const val DATABASE_VERSION = 1
private const val TABLE_PERSONS = "persons"
// 테이블 column으로 사용할 값
private const val COLUMN_ID = "id"
private const val COLUMN_NAME = "name"
private const val COLUMN_AGE = "age"
private const val COLUMN_HEIGHT = "height"
private const val COLUMN_ADDRESS = "address"
}
}
이제 클래스 시그니처에 컴파일 에러가 발생하는데 메서드를 2개 추가로 구현해야 한다. onCreate(), onUpgrade()다.
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
class PersonDatabaseHelper(context: Context) : SQLiteOpenHelper(
context, DATABASE_NAME, null, DATABASE_VERSION
) {
companion object {
private const val DATABASE_NAME = "person_database.db"
private const val DATABASE_VERSION = 1
private const val TABLE_PERSONS = "persons"
// 테이블 column으로 사용할 값
private const val COLUMN_ID = "id"
private const val COLUMN_NAME = "name"
private const val COLUMN_AGE = "age"
private const val COLUMN_HEIGHT = "height"
private const val COLUMN_ADDRESS = "address"
}
override fun onCreate(db: SQLiteDatabase) {
val createTableQuery = """
CREATE TABLE $TABLE_PERSONS (
$COLUMN_ID INTEGER PRIMARY KEY AUTOINCREMENT,
$COLUMN_NAME TEXT NOT NULL,
$COLUMN_AGE INTEGER NOT NULL,
$COLUMN_HEIGHT REAL NOT NULL,
$COLUMN_ADDRESS TEXT NOT NULL
)
""".trimIndent()
db.execSQL(createTableQuery)
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
db.execSQL("DROP TABLE IF EXISTS $TABLE_PERSONS")
onCreate(db)
}
}
onCreate()에선 DB 테이블을 만드는 SQL 쿼리문을 쓴다. Person data class에 정의한 대로 값을 저장하는 SQL문을 쓰면 될 뿐이다.
onUpgrade()에선 DB가 업그레이드될 경우(column 추가 같은 DB에 변경사항이 생긴 경우) 기존 테이블을 삭제하고 다시 생성하도록 DROP SQL문을 사용한다. 테이블을 삭제한단 건 기존 데이터들을 전부 지우겠다는 것이기 때문에 실제 사용 시에는 더 신중하고 복잡한 로직이 요구된다.
이제 Person 객체를 추가, 수정, 삭제하는 함수를 만든다.
import android.content.ContentValues
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import com.example.regacyviewpractice.data.model.Person
class PersonDatabaseHelper(context: Context) : SQLiteOpenHelper(
context, DATABASE_NAME, null, DATABASE_VERSION
) {
companion object {
private const val DATABASE_NAME = "person_database.db"
private const val DATABASE_VERSION = 1
private const val TABLE_PERSONS = "persons"
// 테이블 column으로 사용할 값
private const val COLUMN_ID = "id"
private const val COLUMN_NAME = "name"
private const val COLUMN_AGE = "age"
private const val COLUMN_HEIGHT = "height"
private const val COLUMN_ADDRESS = "address"
}
override fun onCreate(db: SQLiteDatabase) {
val createTableQuery = """
CREATE TABLE $TABLE_PERSONS (
$COLUMN_ID INTEGER PRIMARY KEY AUTOINCREMENT,
$COLUMN_NAME TEXT NOT NULL,
$COLUMN_AGE INTEGER NOT NULL,
$COLUMN_HEIGHT REAL NOT NULL,
$COLUMN_ADDRESS TEXT NOT NULL
)
""".trimIndent()
db.execSQL(createTableQuery)
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
db.execSQL("DROP TABLE IF EXISTS $TABLE_PERSONS")
onCreate(db)
}
fun addPerson(person: Person): Long {
val db = this.writableDatabase
val values = ContentValues().apply {
// id는 자동 생성되므로 넣지 않음
put(COLUMN_NAME, person.name)
put(COLUMN_AGE, person.age)
put(COLUMN_HEIGHT, person.height)
put(COLUMN_ADDRESS, person.address)
}
// insert 메소드는 새로 추가된 행의 id를 반환
val id = db.insert(TABLE_PERSONS, null, values)
db.close()
return id
}
fun getAllPersons(): List<Person> {
val personList = mutableListOf<Person>()
val selectQuery = "SELECT * FROM $TABLE_PERSONS"
val db = this.readableDatabase
val cursor = db.rawQuery(selectQuery, null)
if (cursor.moveToFirst()) {
do {
val person = Person(
id = cursor.getLong(cursor.getColumnIndexOrThrow(COLUMN_ID)),
name = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_NAME)),
age = cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_AGE)),
height = cursor.getDouble(cursor.getColumnIndexOrThrow(COLUMN_HEIGHT)),
address = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_ADDRESS))
)
personList.add(person)
} while (cursor.moveToNext())
}
cursor.close()
db.close()
return personList
}
fun getPerson(id: Long): Person? {
val db = this.readableDatabase
val cursor = db.query(
TABLE_PERSONS,
null,
"$COLUMN_ID = ?",
arrayOf(id.toString()),
null, null, null
)
var person: Person? = null
if (cursor.moveToFirst()) {
person = Person(
id = cursor.getLong(cursor.getColumnIndexOrThrow(COLUMN_ID)),
name = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_NAME)),
age = cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_AGE)),
height = cursor.getDouble(cursor.getColumnIndexOrThrow(COLUMN_HEIGHT)),
address = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_ADDRESS))
)
}
cursor.close()
db.close()
return person
}
fun updatePerson(person: Person): Int {
val db = this.writableDatabase
val values = ContentValues().apply {
put(COLUMN_NAME, person.name)
put(COLUMN_AGE, person.age)
put(COLUMN_HEIGHT, person.height)
put(COLUMN_ADDRESS, person.address)
}
// update 메소드는 업데이트된 행의 수를 반환
val result = db.update(
TABLE_PERSONS,
values,
"$COLUMN_ID = ?",
arrayOf(person.id.toString())
)
db.close()
return result
}
fun deletePerson(id: Long): Int {
val db = this.writableDatabase
// delete 메소드는 삭제된 행의 수를 반환
val result = db.delete(
TABLE_PERSONS,
"$COLUMN_ID = ?",
arrayOf(id.toString())
)
db.close()
return result
}
fun getPersonCount(): Int {
val countQuery = "SELECT * FROM $TABLE_PERSONS"
val db = this.readableDatabase
val cursor = db.rawQuery(countQuery, null)
val count = cursor.count
cursor.close()
db.close()
return count
}
}
addPerson()은 매개변수로 받은 Person 객체를 SQLite에 저장하고 새로 추가된 객체의 id를 리턴한다.
함수 안에서 ContentValues를 사용하는데 이걸 쓰는 이유는 무엇인가?
https://stackoverflow.com/a/71835174
Android Studio: ContentValues vs Database.execSQL()
My question can be considered as a continuation of What is the difference between inserting data using ContentValues and Raw SQL in SQLite Android? My question is more towards: Which is more common...
stackoverflow.com
ContentValues는 적절한 SQL을 안정적으로 작성하는 SQLiteDatabase.insert() 같은 편리한(사용하기 쉬운) 메서드와 같이 쓰인다. 이런 메서드는 값을 올바르게 묶는다. 많은 시나리오에 적합하고 매우 편하며 안정적이다. 또한 단일 execSQL이 수행하지 않는 값 리턴 작업 등을 수행할 수도 있다
insert()의 경우 리턴되는 값은 긴 값(코틀린에선 Long)이며 삽입된 row의 rowid거나, row가 삽입되지 않았지만 충돌이 트래핑된 경우 -1이다...(중략)
이외에도 아래와 같은 이유로 ContentValues()를 쓴다.
- SQL 인젝션 방지 : 입력값을 SQL 쿼리에 직접 문자열로 삽입하면 SQL 인젝션의 위험이 있다. ContentValues는 값들을 이스케이프 처리해서 안전하게 전달해준다
- 타입 안전성 보장 : 각 자료형에 맞는 put()을 제공해서 컴파일 타임에 에러를 잡을 수 있어서 안전하다
id를 리턴하는 이유는 데이터가 DB에 잘 저장됐는지 확인하기 위함이다. 보통 성공하면 새로 추가된 row의 id(양수)를 리턴하고 실패하면 -1을 리턴한다. 이걸 통해 addPerson()의 성공 여부를 알 수 있어서 id를 리턴하게 만들었다. 필요하다면 리사이클러뷰로 구현 시 이 id를 아이템 맨 앞에 표시해서 유저한테 데이터가 잘 저장됐음을 알릴 수 있다.
getAllPerson()에선 DB에 저장된 모든 Person 객체를 조회한다. 이 때 cursor를 사용하는데 이 요소가 생소하다면 아래 링크들을 확인한다.
https://developer.android.com/reference/android/database/Cursor
Cursor | API reference | Android Developers
developer.android.com
이 인터페이스는 DB 쿼리가 리턴한 결과 집합에 대한 임의의 읽기-쓰기 접근을 제공한다. 커서 구현은 동기화할 필요가 없으므로 여러 쓰레드에서 커서를 사용하는 코드는 커서 사용 시 자체 동기화를 수행해야 한다. 구현은 AbstractCursor를 서브클래싱해야 한다
https://stackoverflow.com/a/9938504
What is use of Cursor in Android Development?
I was going through some of code on the internet regarding the database connection, retrieval. I saw Cursor cur1= moveToFirst() in a lot of code and I want to know what the use of a cursor is and w...
stackoverflow.com
커서는 DB의 2차원 테이블을 나타내는 인터페이스다. SELECT로 일부 데이터를 검색하려고 하면 DB는 먼저 커서 객체를 만들고 그 참조를 리턴한다. 이 참조의 포인터는 0번 position을 가리키며 그렇지 않으면 커서의 첫 번째 position을 가리키기 때문에, 커서에서 데이터를 검색하려면 먼저 moveFirst()를 써야 한다
커서에서 moveToFirst()를 호출하면 커서 포인터를 첫 번째 위치로 가져갈 수 있고 첫 번째 레코드에 있는 데이터에 접근할 수 있다
쉽게 말해서 DB 쿼리 결과에 포함된 row들을 하나씩 순회해서 그 안에 있는 데이터에 접근할 수 있게 해주는 객체다.
getAllPerson()의 rawQuery()로 SELECT문을 실행한 결과를 바라보는 커서 객체를 만든 다음, moveToFirst()를 통해 첫 번째 데이터로 이동했다면 다음 row가 없을 때까지 계속 row를 순회하며 getXXX()로 값들을 얻어와 리스트에 add한다.
do-while을 썼기 때문에 다음 row가 없다면 루프가 종료되고 커서, DB 객체를 close한 뒤 최종적으로 리스트를 리턴하는 구조다. 그냥 검색 결과를 리스트에 담아 리턴하는 함수인데 내부 구현에서 커서를 써서 복잡해 보일 뿐이다.
getAllPerson()을 이해했다면 getPerson(), getPersonCount()도 이해할 수 있을 것이다. updatePerson, deletePerson()은 딱히 볼 게 없으므로 넘어간다.
그리고 get이 붙은 함수 안에선 readableDatabase를 호출하고 그 외의 함수 내부에선 writableDatabase를 호출하는데 읽기 전용 DB를 열 것인지 읽고 쓸 수 있는 DB를 열 것인지 정하는 것이다. 쿼리의 특성에 맞춰 사용하면 된다. 단순히 접근해서 읽기만 한다면 readableDatabase를 사용하고 추가, 수정, 삭제해야 하는 쿼리면 writableDatabase를 사용하면 된다. 참고로 두 함수 모두 같은 객체를 리턴하며 특성만 다를 뿐이다. 자세한 건 아래 문서를 확인한다.
SQLiteOpenHelper | API reference | Android Developers
developer.android.com
이제 액티비티에 위 함수들을 사용하는 코드를 작성한다.
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import com.example.regacyviewpractice.data.datasource.local.sqlite.PersonDatabaseHelper
import com.example.regacyviewpractice.data.model.Person
import com.example.regacyviewpractice.databinding.ActivityDataPermanenceBinding
class DataPermanenceActivity : AppCompatActivity() {
private val TAG = this::class.simpleName
private lateinit var binding: ActivityDataPermanenceBinding
private lateinit var dbHelper: PersonDatabaseHelper
private lateinit var person1: Person
private lateinit var person2: Person
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityDataPermanenceBinding.inflate(layoutInflater)
setContentView(binding.root)
person1 = Person(
name = "김철수",
age = 20,
height = 190.0,
address = "OO시 OO구"
)
person2 = Person(
name = "김영희",
age = 21,
height = 180.0,
address = "ㅁㅁ시 ㅁㅁ구"
)
dbHelper = PersonDatabaseHelper(this)
executeAddOperation()
executeReadOperation()
executeUpdateOperation()
executeDeleteOperation()
}
private fun executeAddOperation() {
person1.id = dbHelper.addPerson(person1)
person2.id = dbHelper.addPerson(person2)
Log.d(TAG, "## [sqlite] 새 person 추가됨. person1 : $person1")
Log.d(TAG, "## [sqlite] 새 person 추가됨. person2 : $person2")
}
private fun executeReadOperation() {
val personId = person1.id
val personFromDb: Person? = dbHelper.getPerson(personId)
personFromDb?.let {
Log.d(TAG, "## [sqlite] id가 ${personId}인 Person 조회 결과 : $it")
}
val persons = dbHelper.getPersonCount()
Log.d(TAG, "## [sqlite] executeReadOperation() > getPersonCount : $persons")
}
private fun executeUpdateOperation() {
person1.apply {
age = 30
address = "ㅂㅂ시 ㅂㅂ구"
}
val updateResult = dbHelper.updatePerson(person1)
Log.d(TAG, "## [sqlite] updateResult : $updateResult")
Log.d(TAG, "## [sqlite] 업데이트 결과 : $person1")
val updatedPerson = dbHelper.getAllPersons()
Log.d(TAG, "## [sqlite] 업데이트 후 Person 목록 확인 =======")
repeat(updatedPerson.size) {
Log.d(TAG, "## [sqlite] ${updatedPerson[it]}")
}
val persons = dbHelper.getPersonCount()
Log.d(TAG, "## [sqlite] executeUpdateOperation() > getPersonCount : $persons")
Log.d(TAG, "## [sqlite] 업데이트 결과 확인 종료")
}
private fun executeDeleteOperation() {
val deleteResult = dbHelper.deletePerson(person2.id)
Log.d(TAG, "## [sqlite] person2 삭제 결과 : $deleteResult")
val remainPersons = dbHelper.getAllPersons()
Log.d(TAG, "## [sqlite] 삭제 후 Person 목록 확인 =======")
repeat(remainPersons.size) {
Log.d(TAG, "## [sqlite] ${remainPersons[it]}")
}
val persons = dbHelper.getPersonCount()
Log.d(TAG, "## [sqlite] executeDeleteOperation() > getPersonCount : $persons")
Log.d(TAG, "## [sqlite] 삭제 결과 확인 종료")
}
override fun onDestroy() {
dbHelper.close()
super.onDestroy()
}
}
액티비티가 파괴될 때 쓸데없이 메모리를 낭비하지 않기 위해 dbHelper.close()를 호출한다.
로그는 아래처럼 표시될 것이다.
## [sqlite] 새 person 추가됨. person1 : Person(id=1, name=김철수, age=20, height=190.0, address=OO시 OO구)
## [sqlite] 새 person 추가됨. person2 : Person(id=2, name=김영희, age=21, height=180.0, address=ㅁㅁ시 ㅁㅁ구)
## [sqlite] id가 1인 Person 조회 결과 : Person(id=1, name=김철수, age=20, height=190.0, address=OO시 OO구)
## [sqlite] executeReadOperation() > getPersonCount : 2
## [sqlite] updateResult : 1
## [sqlite] 업데이트 결과 : Person(id=1, name=김철수, age=30, height=190.0, address=ㅂㅂ시 ㅂㅂ구)
## [sqlite] 업데이트 후 Person 목록 확인 =======
## [sqlite] Person(id=1, name=김철수, age=30, height=190.0, address=ㅂㅂ시 ㅂㅂ구)
## [sqlite] Person(id=2, name=김영희, age=21, height=180.0, address=ㅁㅁ시 ㅁㅁ구)
## [sqlite] executeUpdateOperation() > getPersonCount : 2
## [sqlite] 업데이트 결과 확인 종료
## [sqlite] person2 삭제 결과 : 1
## [sqlite] 삭제 후 Person 목록 확인 =======
## [sqlite] Person(id=1, name=김철수, age=30, height=190.0, address=ㅂㅂ시 ㅂㅂ구)
## [sqlite] executeDeleteOperation() > getPersonCount : 1
## [sqlite] 삭제 결과 확인 종료
executeAddOperation() 내부 로그가 1~2번 줄까지 표시되면서 데이터가 저장된 걸 볼 수 있고, executeReadOperation()의 결과는 3~4번 줄에서 확인할 수 있다.
5~11번 줄이 executeUpdateOperation()의 로그다. updatePerson()이 1을 리턴하는 건 실제로 업데이트된 row의 수를 리턴하기 때문이다. 로그 상으로도 person1의 age, address가 이전과 다른 값으로 저장된 걸 볼 수 있다.
12~16번 줄이 executeDeleteOperation()의 로그다. update와 마찬가지로 delete 명령으로 삭제된 row의 개수는 1개 뿐이기 때문에 삭제 결과가 1로 표시되고, person2인 김영희와 관련된 row를 제거했기 때문에 남아있는 건 김철수와 관련된 데이터 뿐이다.
그런데 로그만으로는 보는 데 한계가 있다. SQLite에 저장된 데이터를 시각적으로 볼 수 있는 방법이 있지 않을까?
예전에 SQLite 포스팅을 작성한 적이 있는데 여기서 확인하면 된다.
https://onlyfor-me-blog.tistory.com/271
[Android] SQLite 사용법 - INSERT -
SQLite에 대해선 이전에 포스팅을 작성한 적이 있다. onlyfor-me-blog.tistory.com/45 [Android] SQLite란? - 1 - SQLite는 쉐어드, 룸 DB, Realm 따위와 같이 안드로이드에서 제공하는 앱 DB의 한 종류이다. 특이한 것
onlyfor-me-blog.tistory.com
설치 후 위 글에 나온대로 따라하면 데이터를 볼 수 있다. 이전과 달라진 건 데이터 보기가 데이터 탐색으로 변경됐다는 것 정도다.
앞서 delete까지 실행된 상태에서 .db 파일을 추출하고 확인했기 때문에 저렇게 1개의 row만 표시되는데, 원하는 데이터를 추가하고 코드를 수정하면서 확인하면 실제로 데이터가 어떻게 저장되는지 더 잘 확인할 수 있을 것이다.
다음 포스팅에선 Room DB가 뭔지와 간단한 구현을 확인한다.
'Android' 카테고리의 다른 글
[Kotlin] 코루틴 디스패처 (0) | 2025.05.03 |
---|---|
[Android] SQLite vs Room DB 비교 및 구현 - 2 - (0) | 2025.04.28 |
[Android] 액티비티 UI 상태 저장 (onSaveInstanceState, onRestoreInstanceState) (0) | 2025.04.02 |
[Android] 일반적인 UseCase 패턴 실수 (0) | 2025.03.26 |
[Android] Result와 Result.fold() 알아보기 (0) | 2025.03.24 |