본문 바로가기
깡샘 코틀린

17-1 저장소에 데이터 보관하기

by 농농씨 2023. 7. 9.

여섯째 마당: 앱에 다양한 기능 추가하기(17~19장)

안드로이드 앱은 대부분 서버와 데이터를 주고받거나 데이터를 데이터베이스에 저장해서 계속 이용할 수 있게 한다. 이런 기능을 앱에 추가하는 방법을 살펴보자.

 

17장에서는 앱의 데이터를 저장하고 이용하는 방법을 다룬다. 앱은 데이터를 저장하고 가공하여 서비스 할 수 있다. 이때 앱의 데이터는 대부분 외부 서버에 저장해놓고 통신으로 주고받지만 네트워크가 불안정한 상황도 있을 수 있으므로 안정된 서비스 제공을 목적으로 기기에 저장하는 방법도 알아야 한다. 안드로이드 기기에 데이터를 저장하는 방법은 크게 세가지로 나뉜다. 1️⃣데이터베이스, 2️⃣파일, 3️⃣프리퍼런스를 이용한다. 모두 자주 사용하므로 잘 알아두자.

 

 

 

데이터베이스 프로그래밍은 데이터 베이스에 테이블을 만들고 SQL을 이용해 데이터를 삽입, 조회, 갱신, 삭제하는 방법이다. 안드로이드폰에서 이용하는 데이터베이스 관리 시스템은 오픈소스로 만들어진 SQLite(sqlite.org)이다.

SQLite는 테이블의 데이터를 앱의 저장소에 파일로 저장하며 외부 앱에서는 접근할 수 없다. 코드에서는 SQL 질의문만 작성하면 되고 실제 데이터는 SQLite가 관리해 준다.

 

 

질의문 작성하기

SQLite를 사용하려면 SQLiteDatabase라는 API를 이용해야 한다. SQLiteDatabase 객체는 openOrCreateDatabse() 함수를 호출해서 얻는다.

// 데이터베이스 객체 생성
val db = openOrCreatDatabase("testdb", Context.MODE_PRIVATE, null)

openOrCreateDatabase() 함수는 첫 번째 매개변수로 전달한 DB 파일을 열고 SQLiteDatabase 객체를 돌려준다. 만약 파일이 없으면새로 만든다. SQLiteDatabase 객체에 정의된 다음 함수를 이용하면 질의문을 작성할 수 있다.

  • public void execSQL(String sql, Object[] bindArgs)
  • public Cursor rawQuery(String sql, String[] selectionsArgs)

exelSQL(), rawQuery() 함수의 첫 번째 매개변수에 질의문을 전달하고 두 번째 매개변수에는 질의문에서 ? 문자에 대응하는 값을 배열로 전달한다. 예를 들어 질의문에 물음표를 3개 작성했다면 크기가 3인 배열을 전달해 순서대로 물음표로 표시한 곳에 대입한다.

 

exelSQL() 문은 질의문 중에서 create, alter, drop, insert, update, delete 문을 실행하는 함수이다. 다음 코드는 db 객체가 가리키는 DB 파일에 execSQL() 함수로 create 문을 실행해 테이블을 만드는 예이다.

// 테이블 생성(create 문)
db.execSQL("create table USER_TB (" + // exelSQL() 함수로 create문 실행해서 테이블 만듦
           "_id integer primary key autoincrement," +
           "name not null," +
           "phone)")

이렇게 만든 테이블에 데이터를 넣으려면 다음처럼 execSQL() 함수로 insert 문을 실행한다.

// 데이터 삽입(insert 문)
db.execSQL("insert into USER_TB (name, phone) values (?,?)",
    arrayOf<String>("kkang", "0101111")) // 배열 형태로 데이터 전달하는 듯

 

테이블에 저장된 데이터를 조회할 때는 rawQuery() 함수로 select 문을 실행한다. rawQuery() 함수의 반환값은 Cursor 객체이다. 이 객체는 테이블에서 조회한 (row)의 집합 정도로 생각하면 된다.

// 데이터 조회(select 문)
val cursor = db.rawQuery("select * from USER_TB", null)

조회한 행의 열(column) 데이터를 가져오려면 먼저 1️⃣Cursor 객체로 행을 선택하고 2️⃣그 행의 열 데이터를 가져온다.

1️⃣Cursor 객체로 행을 선택할 때는 moveTo~로 시작하는 다음 함수를 이용한다. 이 함수는 선택한 행이 있으면 true를 반환하고 없으면 false를 반환한다.

  • public abstract boolean moveToFirst():  첫 번째 행을 선택한다.
  • public abstract boolean moveToLast(): 마지막 행을 선택한다.
  • public abstract boolean moveToNext(): 다음 행을 선택한다.
  • public abstract boolean moveToPosition(): 매개변수로 지정한 위치의 행을 선택한다.
  • public abstract boolean moveToPrevious(): 이전 행을 선택한다.

2️⃣Cursor 객체로 선택한 행의 열 데이터를 가져오려면 타입에 따라 getString(), getInt() 등의 함수를 이용한다. 이 함수의 매개변수에는 가져올 데이터가 저장된 열의 인덱스를 전달한다.

  • public abstract String getString(int columnIndex)
  • public abstract int getInt(int columnIndex)
  • public abstract double getDouble(int columnIndex)
// 선택한 행의 값 가져오기
while (cursor.moveToNext()) { // Cursor 객체로 다음 행을 선택하고
    val name = cursor.getString(0) // getString() 함수로 매개변수에 열의 인덱스전달해서 열의 데이터를 가져옴
    val phone = cursor.getString(1)
}

 

지금까지 질의문을 실행하는 SQLiteDatabase 클래스의 rawQuery(), execSQL() 함수를 살펴보았는데, 이 함수를 이용하지 않고 insert(), update(), delete(), query() 함수를 사용해도 된다. 이 함수는 질의문의 각 항목을 매개변수로 대입하면 질의문으로 만들어 실행해 준다. 즉, 질의문을 코드에서 직접 작성하지 않고 항목별 정보만 매개변수로 대입해 주면 된다.

  • 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 wherClause, String[] whereArgs)
  • public Cursor query(String table, String[] columns, String selection, Strig[] selectionArgs, String groupBy, String having, String orderBy)

insert(), update() 함수의 매개수에서 ContentValues 객체 데이터의 집합이다. Map 객체처럼 키-값 형태로 데이터 집합을 저장하는데 이때 키에는 테이블의 열 이름을 지정한다.

다음은 insert() 함수를 사용하는 예이다.

// insert() 함수 사용
val values = ContentValues()
values.put("name", "kkang")
values.put("phone", "0101112")
db.insert("USER_TB", null, values) // insert() 함수로 세번째 매개변수에 열 데이터의 집합 지정함

 

다음은 각 매개변수로 질의문을 만들어 실행해주는 query() 함수를 사용하는 예이다.

// query() 함수 사용
val cursor = db.query("USER_TB", arrayOf<String>("name", "phone"), "phone=?",
    arrayOf<String>(0101112), null, null, null)

query() 함수의 각 매개변수는 다음과 같다.

  • table: 조회할 테이블명.
  • columns: 가져올 값이 담긴 열 이름을 배열로 지정함.
  • selection: select 문의 where 절 뒤에 들어갈 문자열이다.
  • selectionArgs: 질의문에서 ?에 들어갈 데이터 배열이다.
  • groupBy: select 문의 group by 절 뒤에 들어갈 문자열이다.
  • having: select 문의 having 조건이다.
  • orderBy: select 문의 order by 조건이다.

 

 

데이터 베이스 관리하기

SQLite 데이터베이스를 이용할 때는 질의문을 실행해야 하므로 반드시 SQLiteDatabase 객체를 이용해야 한다. 그런데 추가로 SQLiteOpenHelper 클래스를 이용하면 데이터베이스 프로그램을 좀 더 구조적으로 작성할 수 있다.

 

SQLiteOpenHelper 클래스는 데이터베이스를 관리하는 코드를 추상화한다. 여기서 데이터베이스를 관리하는 코드란 테이블을 생성(create)하거나 변경(alter), 제거(drop)하는 코드를 의미한다. 이런 관리 코드는 SQLiteOpenHelper 클래스에 작성하고 데이터를 조작하는 코드는 실제 필요한 곳에 작성해 성격이 다른 두 코드를 분리할 수 있다.

 

SQLiteOpenHelper는 추상 클래스이므로 이를 상속받아 하위 클래스를 작성해야 한다.

// SQLiteOpenHelper의 하위 클래스 작성
class DBHelper(context: Context): SQLiteOpenHelper(context, "testdb", null, 1) {
// SQLiteOpenHelper라는 추상 클래스를 상속받을 때 상위 클래스의 생성자를 호출하면서 DB 파일명과 DB 버전정보 전달
    override fun onCreate(db: SQLiteDatabase?) {
    }
    override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
    }
}

SQLiteOpenHelper 클래스를 상속받을 때 상위 클래스의 생성자를 호출하면서 적절한 정보를 넘겨줘야 한다. 위 코드에서 "testdb"는 DB 파일명이며 1은 개발자가 숫자로 지정하는 DB 버전 정보이다. 

 

그리고 onCreate(), onUpgrade() 함수는 SQLiteOpenHelper의 추상 함수이므로 하위 클래스에서 반드시 재정의해야 한다. 이 함수는 자동으로 호출되는데 그 시점을 알고 적절하게 활용해야 한다.

  • onCreate(): 앱이 설치된 후 SQLiteOpenHelper 클래스가 이용되는 순간 한 번 호출한다.
  • onUpgrade(): 생성자에 지정한 DB 버전 정보가 변경될 때마다 호출한다.

onCreate() 함수는 앱이 설치된 후 최초에 한 번만 호출되므로 이 함수에는 데이터베이스의 테이블을 생성하는 코드를 주로 작성한다. onUpgrade() 함수는 DB 버전 정보가 변경될 때마다 호출되므로 이 함수에는 테이블의 스키마*를 변경하는 코드를 주로 작성한다. 이처럼 테이블을 생성하거나 스키마를 변경하는 코드는 SQLiteOpenHelper에 작성하고 데이터를 조작하는 질의문은 다른 곳에서 작성하는 구조로 만든다.

*스키마(schema)는 데이터의 구조나 표현 방법, 관계 등을 나타낸다.

SQLiteOpenHelper 클래스를 이용한다면 질의문을 실행하는 SQLiteDatabase 객체도 SQLiteOpenhelper 클래스를 이용해 생성한다.

// 데이터베이스 객체 생성
val db: SQLiteDatabase = DBHelper(this).writableDatabase

SQLiteOpenHelper 클래스의 readableDatabase나 writableDatabase 속성으로 데이터베이스 객체를 생성한다.