[Android] 8(1). 관계형 데이터베이스와 안드로이드

9 minute read


관계형 데이터베이스와 안드로이드

안드로이드에서 사용하는 SQLite는 관계형 데이터베이스입니다. 관계형 데이터베이스는 데이터의 저장 형태와 관계를 정의하는데, 컬럼(열)과 로우(행)가 있는 테이블(표)을 생각하면 됩니다.

관계형 데이터베이스에서 실제 데이터가 저장되어 있는 테이블은 마치 엑셀의 시트와 같습니다.

image-20210818231317437


테이블과 쿼리 이해하기


관계형 데이터베이스의 테이블과 쿼리에 대해 먼저 알아봅니다.


테이블

테이블은 한 종류의 데이터가 저장되는 단위입니다. 예를 들어 앱에 [공지 사항]과 [묻고 답하기]라는 메뉴가 있다면 각각 1개의 테이블로 구성합니다.

테이블에 저장되는 데이터의 속성은 컬럼(필드)으로 구분되고, 각 컬럼에 값이 채워진 한 줄의 데이터 단위를 로우(레코드, 튜플)라고 합니다.

image-20210818231331210


쿼리 이해하기

관계형 데이터베이스는 SQLStructured Query Language이라는 데이터를 정의, 조작, 제어하는 용도의 언어로 사용합니다. 이때 사용하는 명령어를 SQL 구문 또는 쿼리라고 합니다.

쿼리로 테이블을 생성하거나 테이블에 읽고, 쓰고, 삭제할 수 있습니다.

👍 예시

테이블 A에 있는 모든 데이터를 조회하는 쿼리는 다음과 같습니다.

SELECT * FROM 테이블A 
  • SELECT: 읽어와라
  • *(애스터리스크): 전부
  • FROM: 어디로부터
  • 테이블 A: 테이블 A

즉, 테이블 A로부터 전부 읽어와라라는 명령어입니다.


이외에도 특정 컬럼을 지정해서 레코드를 읽어오고 싶다면 * 대신에 쉼표( , )로 구분하여 사용하고,

SELECT 컬럼1, 컬럼2, 컬럼3 FROM 테이블A

특정 레코드를 지정하고 싶다면 WHERE 구문을 사용합니다.

SELECT * FROM WHERE no=2

두 식을 조합하여 특정 레코드의 특정 컬럼만 읽어올 수도 있습니다.

SELECT 컬럼1 FROM 테이블A WHERE no=2


쿼리의 종류

쿼리는 테이블의 생성과 관련되는 DDL, 앞서 예로 든 SELECT와 같이 데이터를 읽고 쓰는 것과 관련된 DML, 그리고 모바일 용 데이터베이스에서는 잘 사용되지 않지만 권한을 처리하는 DCL, 이렇게 3가지로 분류할 수 있습니다.


1. DDL

DDLData Definition Language은 데이터의 구조를 정의하는 명령어입니다. 테이블을 생성하고 컬럼의 속성을 정의하는 일이 포함됩니다.

SQL 설명
CREATE TABLE 테이블 생성
CREATE TABLE 테이블명 (컬럼 타입)
DROP TABLE 테이블 삭제
DROP TABLE 테이블명
ALTER TABLE 테이블 수정(컬럼 수정, 추가, 삭제)
ALTER TABLE 테이블명 ADD COLUMN 컬럼 타입
ALTER TABLE 테이블명 MODIFY COLUMN 컬럼 타입
ALTER TABLE 테이블명 DROP COLUMN 컬럼 타입


2. DML

DMLData Manipulation Language은 데이터를 조작하는 명령어입니다. 가장 많이 사용되는 명령어이고 기본적으로 C(Create), R(Read), U(Update), D(Delete)에 해당하는 네 가지 명령어에 대한 이해가 필요합니다.

SQL 명령 설명
SELECT Read 데이터 조회
SELECT 컬럼 FROM 테이블명 WHERE 조건
INSERT Create 데이터 삽입
INSERT INTO 테이블명 VALUES (데이터)
UPDATE Update 데이터 수정
UPDATE 테이블명 SET 컬럼 = 데이터 WHERE 조건
DELETE Delete 데이터 삭제
DELETE FROM 테이블명 WHERE 조건


3. DCL

DCLData Control Language은 데이터를 조작하는 명령어로 혼동될 수 있지만, 사실은 데이터베이스 ‘권한’과 관련된 명령어입니다. 특정 유저에게 읽기와 쓰기 권한을 부여할 때 주로 사용합니다. 주로 Oracle, MSSQL, MySQL 같은 DBMS(데이터베이스 관리 시스템)에서 사용합니다.



SQLite 데이터베이스


안드로이드의 기본 데이터베이스는 경량 데이터베이스인 SQLite입니다.

데이터베이스는 설명하려면 책 한 권이 필요할 만큼 깊이있는 분야입니다. 여기서는 안드로이드 앱에서 사용할 삽입, 조회, 수정, 삭제와 관련된 기능에 한정해서 알아보겠습니다. 실제 데이터베이스를 사용할 때도 이 4개의 명령이 90% 이상을 차지합니다.

CREATE TABLE 테이블명(
	[컬럼명1] [타입] [옵션], [컬럼명2] [타입], [컬럼명3] [타입] [옵션]
)

컬럼은 [컬럼명] [타입] [옵션] 순으로 작성하고 공백으로 구분하며, 2개 이상의 컬럼을 작성할 때는 쉼표( , )로 구분합니다.


다음은 데이터베이스 타입의 종류입니다. 이외에도 BLOB, NUMERIC 등이 있지만 다음 3개만 사용하면 됩니다.

  • INTEGER: 정수형 컬럼
  • TEXT: 문자형 컬럼
  • REAL: 소수점이 있는 숫자형

예제

컬럼명 타입 설명
no INTEGER 메모의 순번, 자동 증가 옵션 적용
content TEXT 메모의 내용을 문자로 입력, 옵션 없음
datetime INTEGER 작성 시간을 숫자로 입력, 옵션 없음
CREATE TABLE memo(
	no INTEGER PRIMARY KEY, 
  content TEXT, 
  datetime INTEGER
)

PRIMARY KEY 옵션은 ‘해당 컬럼에 중복 깂이 없는 유일한 키로 사용하겠다’는 옵션이고, 데이터를 순서대로 나열하고 싶을 때 사용합니다.

쿼리를 작성할 때 일정한 규칙(특히 컬럼이나 테이블명은 소문자 또는 대문자 한 가지로만 작성)만 있다면 모두 소문자 또는 대문자로 작성해도 상관없습니다.


SQLiteOpenHelper 사용하기

SQLite를 사용하기 위해서는 안드로이드의 컨텍스트가 가지고 있는 createDatabase( ) 메서드를 사용하거나, SQLiteOpenHelper를 상속받아서 사용할 수 있습니다.

여기서는 더 쉬운 방법인 SQLiteOpenHelper 클래스를 상속받아서 사용하는 방법에 대해 설명합니다. SQLiteOpenHelper 클래스는 데이터 베이스를 파일로 생성하고 코틀린 코드에서 사용할 수 있도록 데이터베이스와 연결하는 역할을 합니다.

image-20210818212131471

앞에서 예로 들었던 SELECT 쿼리도 Helper 클래스를 통해서 실행한 후 결과 데이터를 반환받게 됩니다.


이제 데이터베이스를 조작하는 클래스를 구현해보겠습니다.


1. 클래스 생성하고 SQLiteOpenHelper 상속받기

SqliteHelper라는 클래스를 하나 생성하고 SQLiteOpenHelper를 상속받습니다.

// SQLiteOpenHelper를 상속받아 데이터베이스를 사용
// 파라미터: 컨텍스트, DB명, 팩토리, 버전 정보(팩토리는 사용하지 않음)
class SqliteHelper(context: Context, name: String, version: Int): SQLiteOpenHelper(context, name, null, version){
  
}

SQLiteOpenHelper에는 구현해야 하는 2개의 추상 메서드가 있으므로 이 메서드들을 구현해야 합니다.

class SqliteHelper(context: Context, name: String, version: Int): SQLiteOpenHelper(context, name, null, version){

    override fun onCreate(db: SQLiteDatabase?) {
      
    }
    // SqliteHelper에 전달되는 버전 정보가 변경되었을 대 현재 생성되어 있는 데이터베이스의 버전보다 높으면 호출
    override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
        /* 여기서는 사용하지 않음 */
    }
}

onUpgrade( ) 메서드

onUpgrade 메서드는 SqliteHelper에 전달되는 버전 정보가 변경되었을 때 현재 생성되어 있는 데이터베이스의 버전보다 높을 경우 호출됩니다. 버전 변경 사항이 없으면 호출되지 않습니다. onUpgrade 메서드는 여기서는 다루지 않습니다.


2. 데이터베이스 생성

클래스를 생성하면 onCreate 메서드 내에서 테이블을 생성합니다. onCreate( ) 메서드는 최초 데이터베이스 생성 시 호출되는데, 테이블을 생성함으로써 데이터베이스를 생성합니다.

    // 데이터 베이스 최초 생성 시에만 호출
    // 파라미터: 사용할 데이터베이스
    override fun onCreate(db: SQLiteDatabase?) {
        // 테이블 생성 쿼리 작성
        val create = "create table memo" +
                "(" +
                "no integer primary key, " +
                "content text, " +
                "datetime integer" +
                ")"
        // db의 execSQL 메서드에 전달하여 쿼리 실행
        db?.execSQL(create)
    }


3. 데이터 클래스 정의

테이블의 하나의 로우(레코드)에 해당하는 하나의 데이터를 저장할 데이터 클래스를 생성합니다.

여기서는 Memo라는 이름으로 만들겠습니다.

// Memo 클래스의 INSERT, SELECT, UPDATE, DELETE에 모두 사용
data class Memo(var no: Long?, var content: String, var datetime: Long){}

몇 가지 살펴볼 점에 대해 설명하겠습니다.

  • ‘no’와 ‘datetime’의 타입을 데이터베이스에서는 INTEGER로 정의했지만 여기서는 Long으로 정의합니다. 이는 숫자의 범위가 서로 다르기 때문입니다. 특별한 이유가 없다면 SQLite에서 INTEGER로 선언한 것은 소스 코드에서는 Long으로 사용합니다.
  • ‘no’에만 null을 허용한 것은 PRIMARY KEY 옵션으로 값이 자동으로 증가되기 때문에 데이터 삽입 시에는 필요하지 않아서입니다.


4. 4개의 기본 메서드 구현

삽입, 조회, 수정, 삭제에 해당하는 4개의 기본 메서드를 구현합니다.

  • 삽입(INSERT) 메서드

SQLiteOpenHelper를 이용해서 값을 입력할 때는 코틀린의 Map 클래스처럼 (키, 값)의 형태로 사용되는 ContentValues 클래스를 사용합니다. ContentValues에 put(“컬럼명”, 값) 메서드로 저장합니다.

    // INSERT
		fun insertMemo(memo: Memo){
        // 삽입할 데이터 작성
        val values = ContentValues()
        values.put("content", memo.content)
        values.put("datetime", memo.datetime)
    }

삽입할 데이터를 준비했으면 쓰기 전용 데이터베이스를 이용해 데이터를 삽입합니다. 데이터베이스 사용이 끝났으면 항상 close( )를 호출해서 닫아줘야 합니다.

    // INSERT
    fun insertMemo(memo: Memo){
        val values = ContentValues()
        values.put("content", memo.content)
        values.put("datetime", memo.datetime)
        // 쓰기 전용 데이터베이스 가져오기
        val wd = writableDatabase
        // 데이터베이스에 데이터 삽입
        wd.insert("memo", null, values)
        // 쓰기 전용 데이터베이스 닫기
        wd.close()
    }


아래 코드는 쿼리를 이용해 삽입을 하는 메서드입니다.

    // 삽입 메서드 쿼리로 작성하기
    fun insertMemo(memo: Memo){
        val query = "insert into memo(content, datetime) " +
                "values('${memo.content}','${memo.datetime}')"
        val db = writableDatabase
        db.execSQL(query)
        db.close()
    }
  • 조회(SELECT) 메서드

조회 메서드는 반환값이 있으므로 보통 메서드의 가장 윗줄에 반환할 값을 변수로 선언하고, 가장 아랫줄에서 반환하는 코드를 먼저 작성한 후 그 사이에 구형 코드를 작성합니다.

    // SELECT
    fun selectMemo(): MutableList<Memo>{
        val list = mutableListOf<Memo>() // 반환할 값 선언
      
        /* 여기에 코드 작성 */

        return list // 반환
    }

여기서는 메모의 전체 데이터를 조회하는 쿼리를 작성해봅니다.

먼저 쿼리를 문자열로 작성하고 이번에는 읽기 전용 데이터베이스를 가져옵니다. 데이터베이스의 rawQuery( ) 메서드에 작성한 쿼리를 담아서 실행하면 커서(cursor) 형태로 값이 반환됩니다.

    // SELECT
    fun selectMemo(): MutableList<Memo>{
        val list = mutableListOf<Memo>() 

        // 쿼리 작성(여기서는 전체 선택)
        val select = "select * from memo"
        // 읽기 전용 데이터베이스 가져오기
        val rd = readableDatabase
        // 데이터베이스의 rawQuery() 메서드에 쿼리를 담아서 호출하면 커서(cursor) 형태로 값이 반환
        val cursor = rd.rawQuery(select, null)

        return list 
    }

커서(Cursor)

데이터셋을 처리할 때 현재 위치를 포함하는 데이터 요소입니다. 커서를 사용하면 쿼리를 통해 반환된 데이터셋을 반복문으로 하나씩 처리할 수 있습니다. 반복할 때마다 커서가 현재 위치를 가리키고 있어 [데이터 읽기 ➡ 다음 줄 이동]의 단순 로직으로 데이터를 쉽게 처리할 수 있습니다.


쿼리에 해당하는 커서를 가져왔으면 커서의 moveToNext( ) 메서드를 사용하여 모든 레코드를 참조할 수 있습니다. moveToNext( ) 메서드는 다음 줄에 사용할 수 있는 레코드가 있는지 여부를 반환하고, 해당 커서를 다음 위치로 이동시킵니다.

        while (cursor.moveToNext()){
            // 반복문을 돌면서 테이블에 정으된 3개의 컬럼에서 값 꺼낸 후 변수에 저장
            val no: Long = cursor.getLong(cursor.getColumnIndex("no"))
            val content = cursor.getString(cursor.getColumnIndex("content"))
            val datetime = cursor.getLong(cursor.getColumnIndex("datatime"))
            // 컬럼값들로 Memo 클래스를 생성하고 반환할 목록에 추가
            list.add(Memo(no, content, datetime))

컬럼명으로 조회해서 위칫값으로 값 꺼내기

컬럼에서 값을 꺼내기 위해서는 먼저 몇 번째 커럼인지를 컬럼명으로 조회해야 합니다.

val 컬럼 위치 = cursor.getColumnIndex("컬럼명")

그리고 위칫값으로 값을 꺼내면 됩니다. 값을 꺼낼 때는 값의 타입에 맞게 getLong(), getStirng() 등의 메서드를 사용합니다.

cursor.getLong(컬럼 위치)


마지막으로는 마찬가지로 데이터베이스를 닫아야 합니다. 조회 메서드에서는 cursor를 사용하기 때문에 cursor를 먼저 닫고, 데이터베이스를 닫습니다.

다음은 selectMemo 메서드의 전체 코드입니다.

    // SELECT
    fun selectMemo(): MutableList<Memo>{
        val list = mutableListOf<Memo>() // 반환할 값 선언

        // 쿼리 작성(여기서는 전체 선택)
        val select = "select * from memo"
        // 읽기 전용 데이터베이스 가져오기
        val rd = readableDatabase
        // 데이터베이스의 rawQuery() 메서드에 쿼리를 담아서 호출하면 커서(cursor) 형태로 값이 반환
        val cursor = rd.rawQuery(select, null)
        // 테이블에 레코드가 있을 때 동안
        while (cursor.moveToNext()){
            // 반복문을 돌면서 테이블에 정으된 3개의 컬럼에서 값 꺼낸 후 변수에 저장
            val no: Long = cursor.getLong(cursor.getColumnIndex("no"))
            val content = cursor.getString(cursor.getColumnIndex("content"))
            val datetime = cursor.getLong(cursor.getColumnIndex("datatime"))
            // 컬럼값들로 Memo 클래스를 생성하고 반환할 목록에 추가
            list.add(Memo(no, content, datetime))
        }
        // 커서와 읽기 전용 데이터베이스 닫기
        cursor.close()
        rd.close()

        return list // 반환
    }


  • 수정(UPDATE) 메서드

수정 메서드는 삽입 메서드와 매우 비슷합니다. 수정할 값을 ContentValues 클래스를 사용해 생성한 다음, 쓰기 전용 데이터베이스를 가져온 후 데이터베이스의 update() 메서드를 사용하여 데이터를 수정합니다.

    // UPDATE
    fun updateMemo(memo: Memo){
        // 수정할 데이터 작성
        val values = ContentValues()
        values.put("content", memo.content)
        values.put("datetime", memo.datetime)
        // 쓰기 전용 데이터베이스에서 수정
        // 파라미터: 테이블명, 수정할 값, 수정할 조건, 조건파라미터
        val wd = writableDatabase
        wd.update("memo", values, "no = ${memo.no}", null)
        wd.close()
    }

update() 메서드의 파라미터는 총 4개인데, (테이블명, 수정할 값, 수정할 조건, 조건 변수) 순서입니다.

수정할 조건은 PRIMARY KEY로 지정된 컬럼을 사용하며, 여기서는 PRIMARY KEY로 선언된 컬럼이 no이기 때문에 “no=숫자”가 됩니다.

여기서 네번째 파라미터에 null을 입력했는데, 세번째 값을 “no=?” 로 입력한 경우에 네번째 값에 ‘?’에 매핑할 값을 arrayOf(“${memo.no}”) 형태로 전달할 수 있습니다. 여기서는 세번째에 조건과 값을 모두 할당했기 때문에 네번째에 null을 사용하는 것입니다.


아래 코드는 쿼리를 이용해 수정을 하는 메서드입니다.

    // 수정 메서드 쿼리로 작성하기
    fun updateMemo(memo: Memo){
        val query = "update memo set content='${memo.content}', " +
                                    "datetime='${memo.datetime}'" +
                                    "where no = ${memo.no}"
        val db = writableDatabase
        db.execSQL(query)
        db.close()
    }


  • 삭제(DELETE) 메서드

데이터를 삭제할 때도 마찬가지로 쓰기 전용 데이터베이스를 사용합니다.

    // DELETE
    fun deleteMemo(memo: Memo){
        // 쿼리 작성
        val delete = "delete from memo where no = ${memo.no}"
        // 쿼리 실행, 데이터 삭제
        val db = writableDatabase
        db.execSQL(delete)
        db.close()

        // 쓰기 전용 데이터베이스에서 삭제
        // 파라미터: 테이블명, 삭제할 조건, 조건파라미터
        val wd = writableDatabase
        wd.delete("memo", "no = ${memo.no}", null)
        wd.close()
    }

delete 메서드의 파라미터는 update 메서드의 파라미터에서 ‘수정할 값’에 해당하는 것만 빠진 형태입니다.



execSQL 메서드로 실행하기

삽입, 수정, 삭제에 해당하는 쿼리의 경우 각각 데이터베이스의 insert, update, delete 메서드로 간단히 실행할 수 있습니다.

그런데 더욱 복잡한 데이터베이스에서 더욱 정밀하게 데이터를 다루고 싶을 때에는 쿼리를 직접 문자열로 작성하는 것이 좋습니다. 예를 들어 앞의 삭제 메서드는 다음 코드처럼 작성할 수 있습니다.

    // DELETE
    fun deleteMemo(memo: Memo){
        // 쿼리 작성
        val delete = "delete from memo where no = ${memo.no}"
        // 쿼리 실행, 데이터 삭제
        val db = writableDatabase
        db.execSQL(delete)
        db.close()
    }



정리


  • SQLite는 안드로이드에서 사용하는 관계형 데이터베이스로, 로우와 컬럼을 이용해 데이터의 저장 형태와 관계를 정의합니다.
  • 안드로이드에서 주로 사용하는 쿼리는 생성(CREATE), 삽입(INSERT), 조회(SELECT), 수정(UPDATE), 삭제(DELETE)입니다.
  • 안드로이드에서 데이터베이스를 사용하려면 SQLiteOpenHelper 클래스를 상속받아야 합니다.
  • 데이터베이스를 사용할 때는 하나의 레코드에 해당하는 데이터를 담기 위한 data class를 함께 사용합니다.
  • 데이터베이스 조작을 위해서는 4개의 기본 메서드를 구현합니다.
    • 삽입: 삽입할 데이터를 ContentValues 클래스로 정의한 후에 쓰기 전용 데이터베이스의 insert 메서드를 사용합니다.
    • 조회: 쿼리를 문자열로 정의하고 읽기 전용 메서드의 rawQuery 메서드를 호출하여 Cursor 인스턴스와 moveToNext() 메서드를 사용합니다.
    • 수정: 수정할 데이터를 ContentValues 클래스로 정의한 후에 쓰기 전용 데이터베이스의 update 메서드를 사용합니다.
    • 삭제: 삭제할 데이터를 ContentValues 클래스로 정의한 후에 쓰기 전용 데이터베이스의 delete 메서드를 사용합니다.
    • 삽입, 수정, 삭제 메서드의 경우 쿼리를 문자열로 정의하고 쓰기 전용 데이터베이스의 execSQL 메서드를 사용할 수 있습니다.

Categories:

Updated:

Leave a comment