안드로이드 데이터베이스(DB) 프로그래밍 4. [SQLiteOpenHelper 사용 예제] (Android Database 4)

2017. 4. 18. 15:42


1. SQLiteDatabase 사용 예제에 대한 문제와 개선

이전 글 [안드로이드 데이터베이스(DB) 프로그래밍 3 -SQLiteDatabase 사용 예제]에서 SQLiteDatabase 클래스를 사용하여 SQLite 데이터베이스를 다루는 예제에 대해 살펴봤습니다. SQLiteDatabase.openOrCreateDatabase() 함수를 호출하여 데이터베이스를 열고, SQLiteDatabase 클래스 객체를 확보한 다음, 데이터베이스를 다루는 함수를 사용하여 데이터를 추가하거나, 수정, 삭제 또는 조회하는 예제를 작성하였습니다.


그런데 분명 [안드로이드 데이터베이스(DB) 프로그래밍 3 -SQLiteDatabase 사용 예제]에서 작성한 예제가, SQLiteDatabase에 대한 사용법을 보여주고 있고, 앱의 동작도 문제가 없지만, 코드 작성에 대하여 몇 가지 개선해야 할 점이 있습니다.

1.1 SQL Helper(SQLiteOpenHelper 클래스) 사용.

먼저, 예제에서 데이터베이스 파일을 열 때 SQLiteDatabase.openOrCreateDatabase() 함수를 직접 호출했는데, 안드로이드에서는 이 방법보다 SQLiteOpenHelper 클래스를 사용하기를 권장하고 있습니다. (관련 내용은 [SQL Helper를 사용하여 데이터베이스 생성]에서 확인할 수 있습니다.)


SQLiteOpenHelper를 사용하면 SQLiteDatabase.openOrCreateDatabase() 함수를 사용하여 데이터베이스를 열 때 파일의 경로까지 직접 지정해야 하는 번거로움을 해소할 수 있습니다. 그리고 앱의 기능 수정으로 인해 데이터베이스 테이블의 구조가 바뀌어야 하는 상황(데이터베이스 업그레이드)에 대해서도 적절히 대처할 수 있는 기반을 제공함으로써 데이터베이스 관리를 용이하게 만들어줍니다.


그러므로 데이터베이스를 열 때는 SQLiteDatabase.openOrCreateDatabase()를 직접 호출하지 말고, SQLiteOpenHelper를 사용하는 것이 좋습니다. 일단 SQLiteOpenHelper 객체를 생성하고 나면, SQLiteOpenHelpergetWritableDatabase() 또는 getReadableDatabase() 함수를 통해 SQLiteDatabase 객체를 참조할 수 있습니다.

1.2 계약 클래스 (Contract Class) 사용.

[안드로이드 데이터베이스(DB) 프로그래밍 3 -SQLiteDatabase 사용 예제]의 예제에서는 SQL 문장을 만들 때, 테이블 이름(CONTACT_T)과 필드 이름(NO, NAME, ...)을 매번 직접 지정하였습니다.

    private void init_tables() {
        // 코드 계속 ...
        String sqlCreateTbl = "CREATE TABLE IF NOT EXISTS CONTACT_T (" +
                    "NO "           + "INTEGER NOT NULL," +
                    "NAME "         + "TEXT," +
                    "PHONE "        + "TEXT," +
                    "OVER20 "       + "INTEGER" + ")" ;
        // ... 코드 계속
    }

    private void load_values() {
        // 코드 계속 ...
        String sqlQueryTbl = "SELECT * FROM CONTACT_T" ;
        // ... 코드 계속
    }

    private void save_values() {
        // 코드 계속 ...
        String sqlInsert = "INSERT INTO CONTACT_T " +
                    "(NO, NAME, PHONE, OVER20) VALUES (" +
                    Integer.toString(no) + "," +
                    "'" + name + "'," +
                    "'" + phone + "'," +
                    ((isOver20 == true) ? "1" : "0") + ")" ;
        // ... 코드 계속
    }

그런데 만약, 기능 수정 또는 확장으로 인해 데이터베이스 구조가 변경되어야 한다면 어떻게 될까요? 그러한 구조 변경으로 인해 테이블 이름 또는 필드 이름이 수정되어야 된다면? 음.. 뭐, 어쩔 수 없이 모든 테이블, 필드 이름을 찾아서 수정된 이름으로 변경해야겠죠.


물론, 테이블이나 필드 이름이 사용되는 곳이 얼마 없으니 상관없지 않을까... 라고 생각할 수도 있습니다. 하지만 테이블과 필드의 갯수가 많아지고 SQL 문장을 조합하는 곳이 많아지면, 이름 수정 작업은 큰 부담으로 다가올 수 밖에 없습니다.


이런 문제를 해결할 수 있는 방법은 계약 클래스(Contract Class)를 사용하는 것입니다.


계약 클래스(Contract Class)는 프로그램 개발 과정에서 참조되는 여러 상수들을 정의한 클래스를 말합니다. 이 의미를 데이터베이스에서 적용해보자면, 데이터베이스의 계약 클래스(Contract Class)란, 테이블 이름, 열(Column) 이름, 기능 별 SQL 문장들에 대한 상수 정의를 포함한 클래스를 의미합니다. "계약(Contract)"이라는 단어가 "어떤 행위를 함에 있어 관련된 사람들이 지켜야 할 의무 또는 약속을 문서로 남긴 것"을 의미하듯, 계약 클래스(Contract Class)는 데이터베이스를 사용함에 있어 개발자가 사용해야 할 여러 정보를 상수로 정의해둔 것"이라고 정리할 수 있죠.


계약 클래스(Contract Class)를 사용함으로써 얻을 수 있는 장점은 데이터베이스와 관련된 정보를 한 곳에서 관리할 수 있다는 것과, 그로 인해 데이터베이스 구조 또는 이름이 변경될 때의 수정 작업을 최소화할 수 있다는 것입니다.


참고로, 계약 클래스(Contract Class)를 만들 때는 앱의 패키지 루트에 생성하는 것이 좋습니다. 앱 소스의 어디서든 바로 참조 가능하도록 만드는 것이 좋기 때문입니다. 그리고 계약 클래스(Contract Class)는 인스턴스를 만들 필요가 없기 때문에 생성자의 접근 제한자(Access Modifier)를 private으로 선언합니다.


계약 클래스(Contract Class)에 대한 구체적인 사용 방법은 아래 예제의 ContactDBCtrct 클래스를 참고하시기 바랍니다.

2. 개선된 SQLite 데이터베이스 사용 예제

그럼 이제 예제를 통해 [안드로이드 데이터베이스(DB) 프로그래밍 3 -SQLiteDatabase 사용 예제]의 예제를 개선해보도록 하겠습니다.


예제의 화면 레이아웃은 [안드로이드 데이터베이스(DB) 프로그래밍 3 -SQLiteDatabase 사용 예제]에서 사용한 레이아웃을 수정없이 그대로 사용하겠습니다.

예제 화면 레이아웃


2.1 MainActivity의 레이아웃 작성.

MainActivity의 레이아웃은 이전 예제에서 사용한 코드를 그대로 적용합니다.

[STEP-1] "activity_main.xml" - MainActivity의 레이아웃 XML 코드 작성.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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:id="@+id/content_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context="com.ppottasoft.databaseadvancedexample.MainActivity"
    tools:showIn="@layout/activity_main">

    <TableLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:stretchColumns="1">

        <TableRow
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:gravity="center"
                android:textSize="24sp"
                android:text="No" />

            <EditText
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="20dp"
                android:textSize="24sp"
                android:id="@+id/editTextNo"/>
        </TableRow>

        <TableRow
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:gravity="center"
                android:textSize="24sp"
                android:text="Name" />

            <EditText
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="20dp"
                android:textSize="24sp"
                android:id="@+id/editTextName"/>
        </TableRow>

        <TableRow
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:gravity="center"
                android:textSize="24sp"
                android:text="Phone" />

            <EditText
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="20dp"
                android:textSize="24sp"
                android:id="@+id/editTextPhone"/>
        </TableRow>

        <TableRow
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:gravity="center"
                android:textSize="24sp"
                android:text="Over20" />

            <CheckBox
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="20dp"
                android:id="@+id/checkBoxOver20"/>
        </TableRow>

        <TableRow
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <LinearLayout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_span="2">

                <Button
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:textSize="24dp"
                    android:text="Save"
                    android:id="@+id/buttonSave" />

                <Button
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:textSize="24dp"
                    android:text="CLEAR"
                    android:id="@+id/buttonClear" />
            </LinearLayout>

        </TableRow>
    </TableLayout>
</RelativeLayout>

2.2 계약 클래스 (Contract Class) 추가.

이제 코드 개선의 첫 번째 과정으로, 데이터베이스와 관련된 상수를 포함하는 계약 클래스(Contract Class)를 추가합니다. 계약 클래스(Contract Class)에 어떤 정보를 포함시킬 것인지는 오로지 개발자의 결정에 따릅니다. 단순히 데이터베이스 이름, 테이블 이름, 열(Column) 이름 정도의 상수만 포함시킬 수 있고, 더 나아가 단순 조합 SQL 문 또는 기능 별 컬럼과 파라미터 조합을 리턴하는 클래스 정적 메소드까지 선언할 수도 있습니다.


예제에서 사용하는 계약 클래스는 테이블 이름, 열(Column) 이름, 단순 조합 SQL 문 정도만 포함합니다.

[STEP-2] "ContactDBCtrct.java" - 데이터베이스 Contact를 위한 계약 클래스.
public class ContactDBCtrct {

    private ContactDBCtrct() {} ;

    public static final String TBL_CONTACT = "CONTACT_T" ;
    public static final String COL_NO = "NO" ;
    public static final String COL_NAME = "NAME" ;
    public static final String COL_PHONE = "PHONE" ;
    public static final String COL_OVER20 = "OVER20" ;

    // CREATE TABLE IF NOT EXISTS CONTACT_T (NO INTEGER NOT NULL, NAME TEXT, PHONE TEXT, OVER20 INTEGER)
    public static final String SQL_CREATE_TBL = "CREATE TABLE IF NOT EXISTS " + TBL_CONTACT + " " +
            "(" +
                COL_NO +        " INTEGER NOT NULL" +   ", " +
                COL_NAME +      " TEXT"             +   ", " +
                COL_PHONE +     " TEXT"             +   ", " +
                COL_OVER20 +    " INTEGER"          +
            ")" ;

    // DROP TABLE IF EXISTS CONTACT_T
    public static final String SQL_DROP_TBL = "DROP TABLE IF EXISTS " + TBL_CONTACT ;

    // SELECT * FROM CONTACT_T
    public static final String SQL_SELECT = "SELECT * FROM " + TBL_CONTACT ;

    // INSERT OR REPLACE INTO CONTACT_T (NO, NAME, PHONE, OVER20) VALUES (x, x, x, x)
    public static final String SQL_INSERT = "INSERT OR REPLACE INTO " + TBL_CONTACT + " " +
            "(" + COL_NO + ", " + COL_NAME + ", " + COL_PHONE + ", " + COL_OVER20 + ") VALUES " ;

    // DELETE FROM CONTACT_T
    public static final String SQL_DELETE = "DELETE FROM " + TBL_CONTACT ;
}

2.3 SQLiteOpenHelper 상속 및 구현.

코드 개선의 두 번째 과정으로, SQLiteOpenHelper를 상속하는 클래스를 만듭니다.

[STEP-3] "ContactDBHelper.java" - SQLiteOpenHelper를 상속한 Helper 클래스 정의
public class ContactDBHelper extends SQLiteOpenHelper {

    public static final int DB_VERSION = 1 ;
    public static final String DBFILE_CONTACT = "contact.db" ;

    public ContactDBHelper(Context context) {
        super(context, DBFILE_CONTACT, null, DB_VERSION);
    }
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(ContactDBCtrct.SQL_CREATE_TBL) ;
    }
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // db.execSQL(ContactDBCtrct.SQL_DROP_TBL) ;
        onCreate(db) ;
    }
    public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // onUpgrade(db, oldVersion, newVersion);
    }
}

2.4 데이터베이스 Helper 클래스 변수 선언 및 초기화.

SQLiteOpenHelper클래스를 상속한 ContactDBHelper를 사용하기 위해 MainActivity에 클래스 변수로 선언하고, 초기화 과정을 수행합니다. 초기화 과정은 매우 간단합니다. ContactDBHelper 클래스의 인스턴스를 만드는 것만으로 수행되니까요.

[STEP-4] "MainActivity.java" - ContactDBHelper 변수 선언 및 초기화.
public class MainActivity extends AppCompatActivity {

    ContactDBHelper dbHelper = null ;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // 코드 계속 ...

        // init sqlite db helper.
        init_tables() ;

        // ... 코드 계속
    }

    private void init_tables() {
        dbHelper = new ContactDBHelper(this) ;
    }

    // ... 코드 계속
}

2.5 앱 실행 시, 테이블 데이터 조회하여 표시. (SELECT)

이제 새로 추가된 ContactDBHelper 클래스 객체를 사용하는 데이터 조회 기능을 작성하겠습니다. [안드로이드 데이터베이스(DB) 프로그래밍 3 -SQLiteDatabase 사용 예제]의 예제에서는 SQLiteDatabase.openOrCreateDatabase() 함수를 통해 확보한 SQLiteDatabase 객체를 직접 사용했지만, 여기서는 SQLiteOpenHelper 클래스에서 제공하는 getReadableDatabase()함수를 통해 SQLiteDatabase 객체를 가져옵니다.


그리고 데이터 조회를 위한 SELECT 문을 직접 만들지 않고, 계약 클래스에 미리 만들어둔 ContactDBCtrct.SQL_SELECT 를 사용합니다.

[STEP-5] "MainActivity.java" - SELECT 문을 통해 데이터 조회 및 표시.
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        // ... 코드 계속

        // init sqlite db helper.
        init_tables() ;

        // load values from db.
        load_values() ;

        // 코드 계속  ...
    }

    private void load_values() {

        SQLiteDatabase db = dbHelper.getReadableDatabase() ;
        Cursor cursor = db.rawQuery(ContactDBCtrct.SQL_SELECT, null) ;

        if (cursor.moveToFirst()) {
            // no (INTEGER) 값 가져오기.
            int no = cursor.getInt(0) ;
            EditText editTextNo = (EditText) findViewById(R.id.editTextNo) ;
            editTextNo.setText(Integer.toString(no)) ;

            // name (TEXT) 값 가져오기
            String name = cursor.getString(1) ;
            EditText editTextName = (EditText) findViewById(R.id.editTextName) ;
            editTextName.setText(name) ;

            // phone (TEXT) 값 가져오기
            String phone = cursor.getString(2) ;
            EditText editTextPhone = (EditText) findViewById(R.id.editTextPhone) ;
            editTextPhone.setText(phone) ;

            // over20 (INTEGER) 값 가져오기.
            int over20 = cursor.getInt(3) ;
            CheckBox checkBoxOver20 = (CheckBox) findViewById(R.id.checkBoxOver20) ;
            if (over20 == 0) {
                checkBoxOver20.setChecked(false) ;
            } else {
                checkBoxOver20.setChecked(true) ;
            }
        }
    }

    // 코드 계속  ...
}

2.6 입력 데이터 저장하기. (INSERT INTO)

데이터 조회 기능과 마찬가지로, 데이터베이스 Helper 클래스로부터 SQLiteDatabase의 객체를 가져온 다음, INSERT 문을 실행합니다. 이 때, INSERT 문이 실행되면 데이터베이스의 내용이 변경되기 때문에, SQLiteOpenHelper.getWritableDatabase() 함수를 통해 SQLiteDatabase 객체를 가져와야 합니다.

[STEP-6] "MainActivity.java" - 데이터 저장하기.
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        // ... 코드 계속

        Button buttonSave = (Button) findViewById(R.id.buttonSave) ;
        buttonSave.setOnClickListener(new Button.OnClickListener() {
            @Override
            public void onClick(View v) {
                save_values() ;
            }
        });

        // 코드 계속  ...
    }

    // ... 코드 계속

    private void save_values() {
        SQLiteDatabase db = dbHelper.getWritableDatabase() ;

        db.execSQL(ContactDBCtrct.SQL_DELETE) ;

        EditText editTextNo = (EditText) findViewById(R.id.editTextNo) ;
        int no = Integer.parseInt(editTextNo.getText().toString()) ;

        EditText editTextName = (EditText) findViewById(R.id.editTextName) ;
        String name = editTextName.getText().toString() ;

        EditText editTextPhone = (EditText) findViewById(R.id.editTextPhone) ;
        String phone = editTextPhone.getText().toString() ;

        CheckBox checkBoxOver20 = (CheckBox) findViewById(R.id.checkBoxOver20) ;
        boolean isOver20 = checkBoxOver20.isChecked() ;

        String sqlInsert = ContactDBCtrct.SQL_INSERT + 
                " (" +
                Integer.toString(no) + ", " +
                "'" + name + "', " +
                "'" + phone + "', " +
                ((isOver20 == true) ? "1" : "0") +
                ")" ;

        db.execSQL(sqlInsert) ;
    }

    // 코드 계속 ...
}

2.7 데이터 삭제하기. (DELETE)

삭제하는 코드 또한 개선된 방법을 적용합니다.

[STEP-7] "MainActivity.java" - 데이터 삭제하기.
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        // ... 코드 계속

        Button buttonClear = (Button) findViewById(R.id.buttonClear) ;
        buttonClear.setOnClickListener(new Button.OnClickListener() {
            @Override
            public void onClick(View v) {
                delete_values() ;
            }
        });

        // 코드 계속  ...
    }

    // ... 코드 계속

    private void delete_values() {
        SQLiteDatabase db = dbHelper.getWritableDatabase() ;

        db.execSQL(ContactDBCtrct.SQL_DELETE) ;

        EditText editTextNo = (EditText) findViewById(R.id.editTextNo) ;
        editTextNo.setText("") ;

        EditText editTextName = (EditText) findViewById(R.id.editTextName) ;
        editTextName.setText("") ;

        EditText editTextPhone = (EditText) findViewById(R.id.editTextPhone) ;
        editTextPhone.setText("") ;

        CheckBox checkBoxOver20 = (CheckBox) findViewById(R.id.checkBoxOver20) ;
        checkBoxOver20.setChecked(false) ;
    }
}

3. 예제 실행 화면

예제의 실행 화면은 [안드로이드 데이터베이스(DB) 프로그래밍 3 -SQLiteDatabase 사용 예제]의 예제와 동일합니다.

예제 실행 화면


각 필드에 값을 입력하고 "SAVE" 버튼을 누르면, 입력된 값들이 데이터베이스에 저장됩니다. 그리고 앱을 다시 실행하면, 데이터베이스에 저장된 값이 화면에 표시되는 것을 확인할 수 있습니다.

데이터베이스 저장 및 저장된 데이터 표시


4. 참고.

.END.


ANDROID 프로그래밍/DB