안드로이드 데이터베이스(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 , , , , , , ,

  1. 이전 댓글 더보기
  2. Blog Icon
    초보 개발자 1호

    생각해보니 이 케이스에서 꼭 계약클래스를 쓸 필요는 없는 것 같네요.
    답변해주셔서 정말 감사합니다.

  3. 만드려고 하는 기능 잘 구현하시길 바랄게요.
    또 궁금한 점 있으면 질문글 남겨주세요.

    감사합니다.

  4. Blog Icon
    wkcoa

    안녕하세요 뽀따님 혹시 CursorAdapter에 대해서 질문 드려도 될까요?
    스택 오버플로나 안드로이드 책을 봐도 너무 알아낼 수 가 없어서 질문드립니다 ㅠㅠ!!

  5. 네! 질문하시는 건 언제든 편하게 하셔도 됩니다.
    하지만 올려주시는 모든 질문에 대해 만족할만한 답변을 드릴만한 지식과 경험이 부족하기에, 도움을 드릴 수 있을지는 미지수네요.

    그래도 최대한 도움드릴 수 있도록 노력할테니, 언제든 질문글 올려주세요.

    감사합니다.

  6. Blog Icon
    wkcoa

    감사합니다~!!
    지금 구현중인 부분이 리스트뷰에 체크박스를 중첩시켜놨는데요.
    예를 들어서
    ㅁ 사과
    ㅁ 포도
    ㅁ 바나나
    이런식으로 있으면 사과라는 아이템이 있으면 체크박스에 체크가 되게 하고 싶은데요
    ▣ 사과
    ㅁ 포도
    ㅁ 바나나
    이런 식인데요, 근데 문제가 되는 부분이 저 CursorAdapter를 이용했을 때, 체크박스에 위치를 찾을 수가 없어서 맨위에 체크박스만 체크가 되고 있습니다.
    이런 경우에 체크박스에 위치를 어떤식으로 찾아야 하는지 ㅠㅠ 질문드립니다!

  7. 음, 리스트뷰에 체크박스를 어떤 방식으로 만들어놓으셨는지 잘 모르겠지만, 일단 CursorAdapter를 사용하지 않는다고 가정해보시는 게 좋을 것 같아요.

    CursorAdapter를 사용하지 않는다면 체크박스에 체크를 할 수 있을까요? 체크박스의 상태를 변경시킬 수 있게 만들어져 있나요?

    체크박스를 아이템뷰에 만들고 직접 접근하게 만들어놓았는지...
    아니면 Checkable 인터페이스를 적용해 놓은 건지...
    또는 아이템뷰 외부에 체크 박스를 만들어 놓았는지...

    조금 번거로우시더라도 아이템 뷰의 레이아웃 소스만이라도 올려주시면 해결 방법을 찾기가 더 쉬울 것 같습니다.

    감사합니다.

  8. Blog Icon
    사랑합니다

    안녕하세요 빛따님
    제가 질문을 오늘 좀 많이 남기는데 정말 알고 싶어서 그런데 정보좀 얻을 수 있을까해서 질문드려봅니다.

    리시버라는 클래스가 있는데 이 클래스는 어떤 API에 저장된 JSON타입의 데이터를 받아온 후, 디비 객체를 불러 이를 저장합니다.
    1분 간격으로 이를 계속 수행하는데요.
    만약 해당 URL로부터 받아오는 JSON이 갱신이 될 경우, 위에도 적었듯 디비에도 저장하게 되는데, 이 디비에 저장된 것들을 리사이클러뷰로 출력하고 있습니다.
    그런데, 제가 궁금한게.. 이 갱신된 JSON을 받아올 경우 -> 디비도 갱신을 한다. 까지는 알겠는데, 액티비티는 클래스긴 하지만, 안드로이드에서만 사용하다보니까, 디비가 갱신이 되면 어떻게하면 메인 액티비티 또한 갱신을 할 수 있을지, 즉 다른 클래스에서 어떻게 메인 액티비티야, 화면 갱신좀 해줘. 라고 할 수 있는지가 궁금합니다.

    일단은 여기서 보고 배운 러너블로 메인 액티비티에서 1분간격으로 지속적으로 화면 새로고침을 해줘 라고 하고 있는데, 이게 문제가, 아무런 갱신도 없는데 자꾸 새로고침을 하니까 너무 자원의 낭비라는 생각이 들어서, 디비가 갱신될 경우에만 화면을 고치도록 바꾸고 싶거든요 ㅜㅜ 어떻게 조언좀 얻을 수 있을까요?

  9. 다른 클래스에서 액티비티로 화면 갱신 메시지를 보내는 것은, 지금까지 설명했던 핸들러를 사용하면 됩니다.
    Message 객체 또는 Runnable 객체를 보내어 메인 액티비티가 화면을 갱신하도록 만들면 되죠.

    그리고 디비가 갱신될 경우에만 화면을 고치도록 바꾸고 싶다고 하셨는데, 이건 큰 고민거리가 아닌 것으로 보여집니다.

    DB가 갱신되는지 안되는지는 이미 JSON을 받아와서 데이터를 확인했을 때 판별되므로, 이 때 DB가 갱신되지 않으면 메인액티비티에 메시지를 보내지 않으면 됩니다.

    그러면 당연히 화면이 갱신되지 않겠지요.

    아마도 현재 상황을 조금 꼬아서 어렵게 접근하신 것 같습니다.

    쉽고 단순하게 접근하세요.
    그리 복잡한 내용이 아닙니다.

    답변이 되었는지 모르겠네요.

    감사합니다.

  10. Blog Icon
    사랑합니다

    말씀하신 내용을 요약하면, db를 갱신하는 클래스에서 러너블 객체를 메인 액티비티로 보내서, 메인 UI 스레드를 통해 갱신될 수 있도록 하면된다는 말씀이신가요!!?

  11. 네. 맞습니다. Runnable 객체 또는 Message 객체를 메인 액티비티의 핸들러로 보내어 화면을 갱신하도록 만들면 됩니다.

    이미 관련 내용을 어느 정도 이해하고 계신 듯 하네요.
    큰 무리없이 구현하실 수 있을거라 생각합니다.

    감사합니다.

  12. Blog Icon
    마음

    안녕하세요. 안드로이드 앱을 독학하면서 구글링을 하다가 발견한 이 사이트!! 눈이 번쩍 뜨였습니다.
    앞선 여러분들이 말씀하셨듯이, 정말 설명을 체계적으로 잘 해주십니다. 많은 도움을 얻고 있습니다.
    보다 많은 내용을 기대하면서도, 업무에 바빠 그러지 못한다는 말씀에 많은 아쉬움도...

    데이터베이스의 필드에 INTEGER, TEXT, STRING 등의 타입 외에
    ArrayList도 설정이 가능한가요?
    많은 사이트를 돌아다녀 봤지만, 원하는 내용을 찾지 못했습니다.

    날짜별 일정 관련 앱을 만들 계획이며,
    날짜별로 (일정, 관련사진) 등을 저장하고 싶은데, (일정, 관련사진)이 날짜별로 저장되는 숫자가 다르거든요.
    어떤 날은 1개만, 어떤 날은 100개가, 또 어떤 날은 전혀 없을 수도 있고...
    이래서 ArrayList로 만들려고 하는데 방법을 몰라서요.
    SQLiteDatabase를 사용합니다.

    1. 방법이 있으면, 저장하고 읽는 방법 (다른 사이트 추천도 괜찮습니다^^)
    2. 방법이 없으면, 다른 어떤 방법이 있는지...

    점점 추워지는 시기입니다. 건강 조심하세요.

  13. 먼저, DB 설계와 관련된 질문은 정답을 콕 찝어서 알려드릴 수 없음을 양해바랍니다.

    처리할 데이터의 형식과 양, DB 액세스 빈도 등, DB 설계에 고려해야 할 내용들을 제가 다 알 수 없기 때문에 섣불리 답을 드리기가 힘드네요.

    그래도 일반적인 관계형 DB 설계 관점에서 접근해보자면...

    일정 데이터는 하나의 테이블에 그대로 넣으시면 될 것 같습니다. "하나의 데이터 = 하나의 레코드"로 만드는 것이죠.
    그리고 테이블 필드에는 날짜 필드를 추가하여 해당 레코드가 어떤 날짜에 해당되는지 기록하면 될 것 같네요.

    그리고 DB에서 데이터를 가져올 때 날짜 기준으로 쿼리하고, 읽어온 레코드셋을 ArrayList로 변환해서 처리하면 됩니다.

    물론, 하나의 날짜를 하나의 레코드로 저장하고 필드 하나에 특정 형식으로 "일정 정보"를 한번에 쓰고 읽는 게 불가능한건 아니지만, 데이터 양도 그렇고 파서 등에 대한 고민도 해야 하니, 별로 좋은 방법은 아니겠지요.

    음, 답변이 되었는지 모르겠네요.

    잘 설계해보시고, 또 궁금한 내용이 있으면 다시 질문글 남겨주세요.

    감사합니다.

  14. Blog Icon
    마음

    답변 감사합니다.
    그런데, 조금만 더...

    제가 알고 싶은 것은,
    날짜별로 (일정,관련사진)이 저장되는 숫자가 다르기(가변) 때문에
    필드 숫자를 어떻게 만들어야 하는지요.
    물론, 일정 필드와 관련사진 필드는 독립적인 필드로 만들 것입니다.
    예를 들어,
    일정1 필드에 관련사진1-1 필드, 관련사진 1-2 필드...
    일정2 필드에 관련사진2-1 필드, 관련사진 2-2 필드...
    일정... 필드에 ...

    이럴 경우
    특정 날짜에 몇 개의 일정이 있는지 알 수 없고,
    또 각각의 일정마다 몇 개의 관련사진이 필요할 지 알 수 없는 입장에서
    도대체 필드를 몇 개를 만들어야 하는지...

    날짜 레코드마다 최대 100개의 필드를 만들고
    각 일정마다 100개의 사진 필드를 만든다고 하더라도
    총 100*100=10000개의 필드가 필요한데,
    1. 일정이 하나도 없는 날에는 이 모든 필드가 필요하지 않으며 (코드 낭비, 저장 공간 낭비)
    2. 일정은 하나의 날짜에는 최대 100개의 일정만 저장이 가능하다 라는 근본적인 문제가 있게 됩니다.
    (물론 보통적인 현대인들의 일상생활에서 1일당 100개의 일정까지 있을까 하는 생각도 있지만, 모르죠,
    만약 제가 만들 앱이 상품 판매와 관계된다면, 1일당 판매상품은 100개를 넘을 수 있고,
    또 판매상품 당 관련사진도 100개를 넘을 수 있기 때문에...)

    좀 극단적인 예를 들기는 했습니다만,
    가변 숫자가 필요한 입장에서 필드 수를 어떻게 결정해야 할지요.
    최대 10000개를 만든다는 것은 너무나 어처구니 없는 일이지 않겠습니까?

    제가 안드로이드를 공부하기 전에, c, c++ 등을 공부했는데
    거기서는 포인터를 사용하여
    일자마다 필드에 일정 포인터, 관련사진 포인터를 사용하는 방법이 있다는 걸로 들은 적이 있지만
    (실제로 만든 경험은 없어서 정확한 것은 아님)
    안드로이드도 그런 식으로 접근하면 될 것 같다는 생각에
    포인터 필드(c나 c++의 용어를 빌면)를 만드는 방법이 없나하고 생각을 해 보거든요.

    답변 중에 파서 등을 언급하셨는데, 좋은 방법은 아니라고 하셨지만,
    이 방법 또한 하나의 방법이라면 생각해 보고 싶고
    (물론, 그보다 더 나은 방법이 있다면 그걸 사용해야겠지만...)

    그리고 읽어온 레코드셋을 ArrayList로 변환해서 처리하면 된다고 하셨는데,
    저장 공간의 낭비를 최소화하는 범위에서,
    이 가변 숫자의 (고정된 수가 아닌) 내용을 일단 저장을 해야만 읽는 작업도 가능하지 않을까
    생각해 봅니다.

    귀찮게 해드려 죄송합니다.
    전문가의 입장에서는 아무런 일도 아닐지 생각될지 모르지만,
    이제 배우는 입장에서는 1+1=2 도 어렵게 느껴지거든요.

  15. 지금 생각하고 계신 것은...
    하나의 레코드에 일정에 대한 모든 데이터를 넣으려고 하시는 것 같네요.
    그래서 자꾸 가변적으로 변하는 필드 개수를 어떻게 처리할까 고민하고 있고.

    그렇게 접근하지 마시고,
    하나의 일정에 들어가는 데이터들을 필드가 아닌 레코드로 만들어보세요.

    즉, 위의 질문글에서..

    레코드1 - 일정1, 관련사진 필드1-1, 관련사진 필드1-2, .... , 필드1-N
    레코드2 - 일정2, 관련사진 필드2-1, 관련사진 필드2-2, .... , 필드2-N

    이렇게 접근하셨는데.. 이것을,

    레코드1 - 일정1, 관련사진 데이터.
    레코드2 - 일정1, 관련사진 데이터.
    레코드3 - 일정1, 관련사진 데이터.
    ...
    레코드8 - 일정2, 관련사진 데이터.
    레코드9 - 일정2, 관련사진 데이터.
    ...


    이런 식으로 테이블을 구성해보세요.
    테이블의 필드는 "일정을 식별하기 위한 값 필드"와 "관련사진 필드"만 만드는 것입니다.

    그리고 데이터를 읽을 때 "일정1"에 대한 데이터를 쿼리해서
    ArrayList로 읽어들이면 원하시는 기능을 만들 수 있을 것 같네요.

    음.. 아마 소프트웨어 비전공이셔서, 데이터베이스가 생소한 이유로 DB 설계에 조금 어려움을 느끼실 수 있을텐데요.
    DB 관련 서적을 간단하게나마 한번 살펴보시길 추천드립니다.

    체계적으로 설명된 개념과 예제를 접하면 훨씬 더 명확하게 구현하실 수 있을 것 같습니다.

    감사합니다.

  16. Blog Icon
    사랑합니다

    빛따님 혹시 이 예제에서 굳이 INTEGER로 선언된 컬럼에 레코드로 문자열을 집어넣는 이유가 있을까요?
    String sqlInsert = ContactDBCtrct.SQL_INSERT+ "(" +
    no+ "," +
    "'" +name+ "',"+
    "'" +phone+ "',"+
    ((isOver20)? 1 : 0) + ")";

    저는 이렇게 했는데도 문제없이 잘 돌아가서요..

  17. Blog Icon
    사랑합니다

    생각해보니 어차피 문자열 사이에 숫자를 끼우면 알아서 문자열이 되는거였죠 ㅋㅋ 깜빡했네요 별의미가 없었군요

  18. 블로그 내용을 완전 분석 중이신 듯 하네요.
    감사할 따름입니다!!!

  19. Blog Icon

    비밀댓글입니다

  20. 그 파일들이 어떤 것인지는 저도 알지 못합니다.

    안드로이드 애셋 폴더의 용도가, 앱이 사용할 리소스 파일을 패키지에 포함시키기 위한 것이므로, 해당 파일은 앱에서 사용할 리소스 파일인 것이겠지요.

    앱을 만든 사람이 알고 있지 않을까... 생각이 들고요.
    앱 소스에서 각 파일의 이름을 검색해서, 어떤 방법으로 사용하는지 직접 분석하시면 될 듯 합니다.

    감사합니다.

  21. 뽀따님 좋은 글 잘 읽었는데요! 질문이 있어서요, 답해주시면 정말 감사하겠습니다!!

    1. 테이블에 레코드를 500개 이상 넣으려 하는데요, 이때

    String sqlInsert1 = WordContractDB999.SQL_INSERT +
    "(" + " '영화' " + " ," +
    " '해운대' " +
    ")";
    db.execSQL(sqlInsert1);
    를 500번 반복하면 너무 코드가 길어지는 문제점이 생기더군요. 어떻게 해야 코드를 줄일 수 있을까요?
    ArrayList와 for문을 써야할까요?

    2. Insert 기능을 가진 메소드를 Activity가 아닌 java 파일에 넣고 싶은데요, Contract class에 넣어야 할까요, 아니면 SQLite Open Helper에 넣어야 할까요?

    아직 초보라 많이 부족합니다... 답변해주시면 감사하겠습니다!!

  22. 네. 500개 이상의 데이터를 넣을 때, 500줄 이상의 INSERT 코드를 작성하진 않습니다. ArrayList와 for 루프를 통해 처리하면 됩니다. 참고로, 보통 많은 양의 데이터를 삽입할 때는, 별도의 파일에 데이터를 저장해놓고 실행 시 파일을 읽어서 넣거나, 스크립트 또는 db 파일을 별도로 만들어두고 재 사용하는 경우가 많습니다.

    그리고 Insert 기능을 가진 메서드를 Activity가 아닌 다른 곳에 넣는다고 하셨는데, 어디에 넣어도 크게 상관은 없습니다. Contract에 넣어도 되고, SQLiteOpenHelper에 작성해도 됩니다. 하지만 저라면, DB를 처리하는 별도의 클래스를 작성할 것 같습니다.

    Contract 클래스는 정적인(static) 역할이 강조되고, SQLiteOpenHelper는 DB 관리의 성격이 강하기 때문인데요.

    Activity에서 처리할 데이터를 위의 두 파일이 아닌, 테이블 당 하나의 java 파일로 작성하시는 것도 나쁘지 않은 방법이라고 생각합니다.

    어디까지나 주관적인 생각이니, 참고만 하시고 스스로 잘 따져보시고 선택하세요.

    감사합니다.

  23. Blog Icon

    비밀댓글입니다

  24. 크게 고민할 문제는 아닙니다.
    그냥 필요한대로 만드시면 됩니다.

    1. SQLiteOpenHelper의 첫 번째 파라미터에 Context 객체가 필요하다.

    2. WordDBRecord에는 Context객체가 없다.

    3. 그러므로 WordDBRecord에 Context를 변수를 추가하고, setContext() 같은 메서드를 만들어서 Context를 전달한다.

    4. WordDBRecord에 전달된 Context 객체를 SQLiteOpenHelper에서 사용한다.

    어렵지 않지요?

    익숙하지 않아서 힘드시겠지만, 그냥 만드시면 됩니다. 되는 방향으로..

    답변이 되셨는지 모르겠네요.
    추가적으로 궁금한 내용 있으시면 댓글 남겨주세요.

    감사합니다.

  25. Blog Icon

    비밀댓글입니다

  26. 대충, 위에서 설명한대로 구현하신 것 같습니다.

    하지만 딱!! 맞다, 틀리다.. 라고 결정해드릴 수 있는 문제는 아닌 거 같습니다. 코드가 정상적으로 동작하게 만들면 그게 맞는 것 아닐까요.

    거기서부터 출발해서, 점점 더 깔끔한 코드, 효율성 좋은 코드 들을 작성해 나가면 되는 거라 생각합니다.

    감사합니다.

  27. Blog Icon
    123

    글잘보고있습니다
    제가 recyclerview와 sqlite를 함께 사용하려고 하는데
    메인에서 내용을 추가하고
    어댑터에서 내용을 수정 및 삭제합니다.
    이런경우에 sqlite를 어디 클래스에다가 써야될지모르겠네요..
    참고로 내용추가, 수정 둘다 AlertDialog를 사용해서 같은 레이아웃을 불러옵니다.

  28. 프로그램 구조 또는 설계와 관련된 내용은 제가 확답을 드리기가 어렵습니다.

    질문자가 어느 정도의 기반 지식을 가지고 있는지, 만들고자 하는 프로그램의 전체 구조를 어떻게 잡고 있는지, 제가 정확히 알 수 없기 때문에 답을 드리는 것이 큰 의미가 없습니다.

    이런 경우에는, 다양한 예제와 경험을 통해 자신의 패턴을 찾아가시는 게 좋을 것 같습니다.

    일단 어디든 만들어보세요. 그리고 동작하게 만드세요. 그리고 뭔가 좀 아니다 싶거나, 다른 곳에서는 이렇게 안 하던데 같은 생각이 들거나, 누군가에게 지적을 받으면 그 때 다시 만드시면 됩니다.

    생각만으로 고민하는 시간은 조금 줄이시고, 만들고 지우고 바꾸면서 고민해보시는 건 어떨까... 제안드립니다.

    감사합니다.

  29. Blog Icon
    쌀포대


    항상 좋은 강좌 잘 보고있습니다.
    db강좌 마지막에 데이터베이스 읽어와서 화면에 출력하는 강좌가 있으면 좋을것 같습니다.
    초보라서 하나 막히면 막막 하네요 ㅎㅎ

  30. 네. 정리해야 할 내용은 쌓이고 쌓였는데..
    시간적으로 여유가 없네요.
    체력도 영... ㅜㅜ

    그래도 최대한 빨리 정리할 수 있도록 노력하겠습니다.

    감사합니다.

  31. Blog Icon
    김성규

    좋은 글 잘 읽었습니다. 많은 도움이 되었습니다.

    그런데 DBHelper를 사용하려면, context를 사용하여야 하는데, 혹 싱글톤으로 구현하려면 어떻게 해야할까요?

    getInstance 마다 context를 새로 넣어주는걸까요?

    그리고 lazy holder 패턴으로 구현하려면 어떻게 해야할까요... 매개변수 사용 자체가 어렵네요...

  32. 늦은 답변 죄송합니다.
    관련 내용은 구글링하시면 쉽게 답을 찾을 수 있을 것 같습니다.

    하지만 혹시라도 아직 문제를 해결하지 못하셨다면 다시 한번 질문글 남겨주세요.

    감사합니다.

  33. 안녕하세요, 예제 내용이랑 조금 상관이 없는 내용일수도있겠는데요..ㅠ
    sqlite db를 이용해서 리스트뷰를 출력하는 중 입니다.
    데이터들 중 특정 행을 삭제 후 vacuum을 실행했습니다.
    String deleteQuery = "delete from table_name where rowId = 2"
    db.execSQL(deleteQuery);
    db.execSQL("vacuum");
    뭐 예를들어 이렇게 실행시켰는데 vacuum이 작동하지 않습니다..
    vacuum을 실행할때 또 참고해야할 부분이 있을까요?
    logcat을 봐도 딱히 어떤 오류가 뜨지도 않고 vacuum에 관한 로그도 보지 못했습니다..ㅠ

    아니면 뭐 android studio에서는 vacuum 기능이 안된다던가 그런것도 있나요?
    db는 db browser로 테이블 만들기 전 auto vacuum을 full로 설정해놓고 db작성했습니다..

  34. 아 해결했습니다.ㅠ db브라우저 한참 겨놓고 눈빠지게 보고 있다보니 알아냈네요..
    integer타입 필드 중 하나를 primary key 설정을 해놨는데 아마 이거때문에 그랬던것 같습니다. primary key 해체를 한 후 실행을 하니 rowid가 vacuum 되네요. 추측컨데 아마 primary key랑 rowid가 그 좀 연동되는 그런 성질(?)이 있긴하나봅니다.ㅠ

  35. 관련 문제는 아래 내용을 참고하시면 도움되실 것 같습니다.

    "The VACUUM command does not change the content of the database except the rowid values. If you use INTEGER PRIMARY KEY column, the VACUUM does not change the values of that column. However, if you use unaliased rowid, the VACUUM command will reset the rowid values. Besides changing the rowid values, the VACUUM command also builds the index from scratch."

    출처 : https://www.sqlitetutorial.net/sqlite-vacuum/

    감사합니다.

  36. Blog Icon

    비밀댓글입니다

  37. 질문하신 기능을 구현하기 위해 본문의 예제를 참고하는 것은 추천하지 않습니다.

    본문의 내용은 SQLiteOpenHelper 를 사용하는 방법을 설명하고 있습니다. 질문하신 내용과는, 다루는 주제가 상이하다고 볼 수 있고요.

    일단, 질문하신 내용만으로 보자면, 데이터베이스와 앱 개발 관련하여 기본 지식을 먼저 쌓으실 필요가 있을 것 같습니다.

    한번에 해내려고 시도하지 마시고, 차근차근 기본지식을 쌓으시면서 개발해보시게 나을 것 같습니다.

    감사합니다.

  38. Blog Icon

    비밀댓글입니다

  39. 즐거운 개발 라이프 즐기시길 바랍니다.
    감사합니다.

  40. Blog Icon
    코틀린초보

    뽀따님 글 감사히 읽었습니다. 감사합니다.
    질문을 드리고 싶은 것이 있습니다.
    아래에 onUpgrade()와 다운그레이드를 어떻게 사용하는 것인지, 어떤 원리로 작동하는 것인지
    궁금하며, 주석으로 처리해 놓은 두 줄의 코드들은 주석 처리를 제거해도 상관 없는 것인지,
    아니면 항상 저렇게 주석 처리를 해놓은 상태로 둬야 하는것인지 왜 이렇게 주석처리된 것인지
    궁금합니다.
    아, 그리고, 왜 no변수에 데이터가 없는 경우에는 데이터가 저장이 되지 않는 걸까요? name이나 phone이나 기타 다른 데이터들은 값을 초기화 해줘도, no변수에 값을 적지 않으면, 나머지 변수들의 값까지 저장이 아예 안되더라구요. 왜 no변수의 데이터 값이 초기화 되어야만 다른 데이터들까지 저장 되는지 안되는지에 영향이 미치는 건가요?

    class SQLiteDBHelper(context: Context?) : SQLiteOpenHelper(context, DBFILE_CONTACT, null, DB_VERSION) {

    companion object {
    const val DB_VERSION = 1
    const val DBFILE_CONTACT = "contact.db"
    }

    override fun onCreate(db: SQLiteDatabase) {
    db.execSQL(ContactDBCtrct.SQL_CREATE_TBL)
    }

    override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
    // db.execSQL(ContactDBCtrct.SQL_DROP_TBL)
    onCreate(db)
    }

    override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
    // onUpgrade(db, oldVersion, newVersion)
    }
    }

  41. 일단 간단하게 답변을 드리자면,

    Database 구조는 한번 만들어놓는다고 해서 끝이아니라, 지속적으로 업데이트되고 관리되는 경우가 많습니다.

    그런 경우, Database 구조가 변경되면 기존에 저장된 데이터를 새로운 구조에 맞게 변경하거나, 새로 추가해야 할 필요가 있는데요, 이를 보통 Database Migration 이라고 합니다.

    이를 위해 DB에 대한 버전을 관리하고 그 버전에 따라 onUpgrade(), onDowngrade() 함수가 호출되면 Database Migration을 수행하게 만들어 줄 수 있습니다.

    모두 설명하려면 한참 많으니, 구글링을 통해 자료를 많이 찾아보시면 되고요.

    그리고 질문하시는 내용들 보면, 기본 내용에 대한 공부와 이해없이 막 질문하시는 거 같아요.

    열정을 이해하나, 조금 더 기본기를 차근차근 쌓으시면서 공부하시기 바랍니다.

    감사합니다.

  42. Blog Icon
    코틀린초보

    뽀따님, 질문드리고 싶은게 있습니다. 셀렉트문 작성시 + " WHERE $COL_NO=$sum2"이 코드만 추가하면, 자꾸 어플리케이션의 바탕화면으로 튕겨버립니다. sum2가 계속 변화하는 값이라 그런것 같은데, 어떻게 하면 이 문제가 해결될까요? 저는 원하는 COL_NO에서 COL_NAME값을 꺼내와서 각 리사이클러뷰의 포지션마다 텍스트 문자열을 넣어주려고 했는데, 원하는대로 되지 않아서 여쭤봅니다.
    private fun load_values() {
    val db: SQLiteDatabase = dbHelper.getReadableDatabase()
    val cursor: Cursor = db.rawQuery(ContactDBCtrct.SQL_SELECT + " WHERE $COL_NO=$sum2", null)

    if (cursor.moveToNext()) {
    no = cursor.getInt(0)

    for (i in 0..no) {
    sum2 = i

    val name: String = cursor.getString(1)
    tfaf2 = name
    addItem("$tfaf2")
    }
    Toast.makeText(activity, "로드 될 때 $no", Toast.LENGTH_SHORT).show()
    }
    }

  43. 질문하신 내용에서, "sum2가 계속 변화하는 값이라 문제가 생긴 것 같다" 라는 것은 잘못 알고 계신 것 같고요.

    일단 앱이 죽을 때 출력되는 에러 메시지를 확인해 보시기 바랍니다.

    질문 내용만으로는 문제점을 정확히 파악하기 힘드므로, 에러 메시지를 확인하셔서 같이 올려주시면 도움드리기 더 쉬울 것 같습니다.

    감사합니다.