📝 SeSAC의 'JetPack과 Kotlin을 활용한 Android App 개발' 강좌를 정리한 글 입니다.
데이터 영속화에 있어서 "그냥 Read/Write 와 SharedPreference 로 충분한 것 아니냐?" 라고 생각 할 수도 있다. Read/Write 는 주로 이미지를, SharedPreference 는 주로 설정 파일(Map) 에 사용된다.
얼핏 보면 SharedPreference 로 커버칠 수 있게 보이지만, K-V 로 데이터를 저장하기 때문에 데이터를 구조화하기 어렵다. 그래서 대량의 데이터 저장에는 부적합히다.
대부분의 작업은 서버사이드에서 진행되지만 기기 로컬에 데이터를 저장해야 될 경우가 있다. 특히 데이터 캐싱 차원에서 많이 그렇다. 이 때 안드로이드 폰에 내장되어 있는 SQLite
를 사용할 수 있다.
데이터베이스로 데이터 영속화
SQLite(www.sqlite.org) 은 오픈소스로 만들어진 데이터 베이스로 관계형 데이터 베이스
복잡하고 구조화된 어플리케이션 데이터 저장 및 관리
SQLite Database 는 별도의 프로세스가 아닌 라이브러리를 이용
데이터베이스는 생성한 어플리케이션의 일부로 통합
SQLite 를 이용한 데이터는 파일에 저장되며 /data/data/<package_name>/databases 폴더에 저장
(내장 메모리 공간에 저장되므로 외부 앱에서 이용 불가!)
SQLite 데이터는 파일
로 저장된다.
안드로이드 데이터베이스 프로그램의 핵심 클래스는 2개 이다.
SQLiteDatabase 클래스
SQLiteOpenHelper 클래스
데이터베이스에서 SQLiteDatabase 객체
이용은 필수이다. 데이터베이스를 사용한다는 것은 곧 Sql문을 사용한다는 뜻이고, 이 Sql문을 제공해주는 것이 바로 SQLiteDatabase
이기 때문이다.
SQLiteOpenHelper
는 필수는 아니지만 사용하면 프로그램의 구조가 좋아진다.
SQLiteOpenHelper
클래스는 앱을 위한 데이터베이스 관리적인 코드를 한 곳에 추상화 시킬 목적을 가진다. 여기서 관리적인 코드란 데이터베이스에 테이블을 create 하거나 alter 혹은 drop 을 위한 코드를 말한다.
class DBHelper(context: Context): SQLiteOpenHelper(context, "testdb", null, 1) {
//..............
}
SQLiteOpenHelper 클래스를 상속 받아 작성
두 번째 매개변수는 파일명 이다.
마지막 매개변수는 개발자가 제공하는 데이터베이스 버전 정보다.
이 버전 정보가 바뀌는 순간 내부적으로 감지가 이루어진다.
onCreate() : 앱이 인스톨 된 후 최초로 SQLiteOpenHelper 클래스가 이용되는 순간 한 번 호출
onUpgrade() : SQLiteOpenHelper 의 생성자에 지정한 DB 버전 정보가 변경될 때마다 호출
위 2개의 함수는 자동 호출된다.
onCreate()
는 앱이 실행되고 최초로 SQLiteOpenHelper 클래스가 이용되는 순간 호출된다.
즉, 최초에 자동 콜 후에 다시는 호출 되지 않는다. 때문에 거의 대부분 테이블 create 내용을 담는다.
onUpgrade()
에는 대부분 스키마 변경을 담는다.
val db: SQLiteDatabase = DBHelper(this).writableDatabase
SQLiteDatase 객체를 SQLiteOpenHelper 클래스로 획득
SQLiteOpenHelper 의 readableDatabase 혹은 writableDatabase 프로퍼티로 SQLiteDatabase 객체를 획득
readableDatabase 은 select문, writableDatabase 은 나머지 Sql문이라 보면 된다.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="select"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
package com.kotdev99.android.c59
class DBHelper(context: Context) : SQLiteOpenHelper(context, "testdb", null, 1) {
override fun onCreate(p0: SQLiteDatabase?) {
val studentSql = """
create table tb_member (
_id integer primary key autoincrement,
name not null,
email,
phone)
""".trimIndent()
p0?.execSQL(studentSql)
p0?.execSQL("insert into tb_member (name, email, phone) values " +
"('kotdev', 'kotdev99@gmail.com', '1111')")
}
override fun onUpgrade(p0: SQLiteDatabase?, p1: Int, p2: Int) {
p0?.execSQL("drop table tb_student")
onCreate(p0)
}
}
package com.kotdev99.android.c59
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val button = findViewById<Button>(R.id.button)
button.setOnClickListener {
// select문 으로 저장 되어 있는 데이터 뽑기
val db: SQLiteDatabase = DBHelper(this).readableDatabase
val cursor = db.rawQuery("select name from tb_member", null)
if (cursor.moveToFirst()) {
Toast.makeText(this, "${cursor.getString(0)}", Toast.LENGTH_SHORT).show()
}
db.close()
}
}
}
val db = openOrCreateDatabase("testdb", Context.MODE_PRIVATE, null)
SQLite 를 이용하기 위한 최소한의 API
openOrCreateDatabase() 함수를 이용해 획득
SQLiteOpenHelper 객체를 이용해 획득
Sql문을 사용하기 위해서는 반드시 SQLiteDatabase
객체를 얻어야 한다. 이 객체를 얻는 방법이 2개가 있는데 하나는 SQLiteOpenHelper
, 다른 하나는 openOrCreateDatabase()
라는 함수다.
즉, openOrCreateDatabase()
함수를 사용했다는 것은 OpenHelper를 사용하지 않았다는 말이다. OpenHelper를 사용했다면 OpenHelper로 객체를 얻어주면 된다.
db.execSQL("create table USER_TB (" +
"id integer primary key autoincrement," +
"name not null," +
"phone)")
첫 번째 매기변수에 Select문을 제외한 나머지 Sql 문을 준다.
Select문과 나머지 Sql문이 리턴 타입이 달라서 함수가 구분되어 있다.
db.execSQL("insert into USER_TB (name, phone) values (?, ?)", arrayOf<String>("kotdev", "0101111"))
?
를 사용 할 수도 있다.
두번 째 매개변수에 ?
개수만큼 배열 정보를 줘서 데이터를 ?
에 대입시킨다.
val cursor = db.rawQuery("select * from USER_TB", null)
첫 번째 매개변수에 Select 문을 주면 된다.
만약 ?
가 있다면 두번 째 매개변수에 물음표에 해당되는 값을 주면 된다.
public abstract boolean moveToFirst()
public abstract boolean moveToLast()
public abstract boolean moveToNext()
public abstract boolean moveToPosition (int position)
public abstract boolean moveToPrevious()
rawQuery() 함수의 리턴 값은 Cursor 객체이며 select 된 row의 집합객체
Cursor 객체를 움직여 row를 선택하고 선택된 row의 column data 를 획득
즉, Cursor
로 row를 먼저 선택 후 column에 있는 데이터를 획득한다.
Cursor 가 raw를 선택하면 true, 선택하지 못하면 false를 리턴한다.
while (cursor.moveToNext()) {
val name = cursor.getString(0)
val phone = cursor.getString(1)
}
MainActivity 의 EditText 에 입력한 값을 SQLite 저장하고,
ReadActivity 에서 SQLite 테이블의 값을 출력 해보자!
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<EditText
android:id="@+id/add_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="제목을 입력하세요"
android:inputType="text" />
<EditText
android:id="@+id/add_content"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:hint="메모를 입력하세요"
android:inputType="textMultiLine"
android:scrollbars="vertical" />
<Button
android:id="@+id/add_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="ADD" />
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="DB Select 결과" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Title : " />
<TextView
android:id="@+id/read_title"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Content : " />
<TextView
android:id="@+id/read_content"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</LinearLayout>
package com.kotdev99.android.c60
class DBHelper(context: Context) : SQLiteOpenHelper(context, "memodb", null, 1) {
override fun onCreate(p0: SQLiteDatabase?) {
val memoSQL = "create table tb_memo (" +
"_id integer primary key autoincrement," +
"title," +
"content)"
p0?.execSQL(memoSQL)
}
override fun onUpgrade(p0: SQLiteDatabase?, p1: Int, p2: Int) {
TODO("Not yet implemented")
}
}
package com.kotdev99.android.c60
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val titleView = findViewById<EditText>(R.id.add_title)
val contentView = findViewById<EditText>(R.id.add_content)
val addBtn = findViewById<Button>(R.id.add_btn)
addBtn.setOnClickListener {
val title = titleView.text.toString()
val content = contentView.text.toString()
val helper = DBHelper(this)
val db = helper.writableDatabase
db.execSQL(
"insert into tb_memo (title, content) values (?,?)",
arrayOf(title, content)
)
db.close()
val intent = Intent(this, ReadActivity::class.java)
startActivity(intent)
}
}
}
package com.kotdev99.android.c60
class ReadActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_read)
val titleView = findViewById<TextView>(R.id.read_title)
val contentView = findViewById<TextView>(R.id.read_content)
val helper = DBHelper(this)
val db = helper.readableDatabase
val cursor =
db.rawQuery("select title, content from tb_memo order by _id desc limit 1", null)
while (cursor.moveToNext()) {
titleView.text = cursor.getString(0)
contentView.text = cursor.getString(1)
}
db.close()
}
}
public long insert (String table, String nullColumnHack, ContentValues values)
public int update (String table, ContentValues values, String whereClause, String[] whereArgs)
public int delete (String table, String whereClause, String[] whereArgs)
public Cursor query (String table, String[] columns, String selection, String[] selectionArgs,
String groupBy, String having, String orderBy)
insert(), update(), delete(), query() 함수를 이용한 SQL 문 실행
SQL 문에 들어갈 부분을 매개변수로 대입하면 SQL 문을 만들어 실행시켜 주는 함수
execSql() 와 rawQuery() 함수의 경우에는 개발자가 직접 매개변수에 Sql 문을 작성해 주어야 한다.
하지만 insert(), update(), delete(), query() 함수를 사용하면 매개변수로 테이블과 Column, values 등 을 던져주면 알아서 Sql 문을 만들어 실행시켜 준다.
val values = ContentValues()
values.put("name", "kotdev")
values.put("phone", "01044442322")
db.insert("USER_TB", null, values)
ContentValues 는 insert, update 를 위한 컬럼 데이터 집합 객체
Map 객체처럼 키-값 형태로 데이터 여러건을 ContentValues 에 등록
컬럼명을 Key로, 해당 컬럼에 들어갈 데이터를 Value로 준다.
table : select 하고자 하는 테이블 명
columns : 획득하고자 하는 column 명, 배열 데이터로 column 명 지정
selection : select 문의 where 뒤에 들어갈 문자열
selectionArgs : selection 에 지정된 문자열이 데이터가 들어갈 자리를 ? 로 표현했다면 ? 에 들어갈 데이터
groupBy : select 문의 group by 뒤에 들어갈 문자열
having : select 문의 having 조건
orderBy : select 문의 order by 조건
execSql() 와 rawQuery() 함수가 아닌, insert() 와 query() 를 사용해 작성 해보자!
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<EditText
android:id="@+id/add_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="제목을 입력하세요"
android:inputType="text" />
<EditText
android:id="@+id/add_content"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:hint="메모를 입력하세요"
android:inputType="textMultiLine"
android:scrollbars="vertical" />
<Button
android:id="@+id/add_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="ADD" />
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="DB Select 결과" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Title : " />
<TextView
android:id="@+id/read_title"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Content : " />
<TextView
android:id="@+id/read_content"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</LinearLayout>
package com.kotdev99.android.c61
class DBHelper(context: Context?) :
SQLiteOpenHelper(context, "memodb", null, 1) {
override fun onCreate(db: SQLiteDatabase) {
val memoSQL = ("create table tb_memo " +
"(_id integer primary key autoincrement,"
+ "title,"
+ "content)")
db.execSQL(memoSQL)
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
db.execSQL("drop table tb_memo")
onCreate(db)
}
}
package com.kotdev99.android.c61
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val titleView = findViewById<EditText>(R.id.add_title)
val contentView = findViewById<EditText>(R.id.add_content)
val addBtn = findViewById<Button>(R.id.add_btn)
addBtn.setOnClickListener {
val title = titleView.text.toString()
val content = contentView.text.toString()
val helper = DBHelper(this)
val db = helper.writableDatabase
val values = ContentValues()
values.put("title", title)
values.put("content", content)
db.insert("tb_memo", null, values)
db.close()
val intent = Intent(this, ReadActivity::class.java)
startActivity(intent)
}
}
}
package com.kotdev99.android.c61
class ReadActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_read)
val titleView = findViewById<TextView>(R.id.read_title)
val contentView = findViewById<TextView>(R.id.read_content)
val db = DBHelper(this).readableDatabase
val cursor = db.query(
"tb_memo",
arrayOf("title", "content"),
null,
null,
null,
null,
"_id desc limit 1"
)
while (cursor.moveToNext()) {
titleView.text = cursor.getString(0)
contentView.text = cursor.getString(1)
}
}
}
결과는 동일하다.