2014년 11월 25일 화요일

[Android] SQLite활용 : 서버에 있는 데이터베이스(DB) 내려받아 설치하기 [출처] [Android] SQLite활용 : 서버에 있는 데이터베이스(DB) 내려받아 설치하기|작성자 LifeClue


펌위치 : http://blog.naver.com/legendx/40153264388

내용이 이상하거나, 잘못됐거나, 이해가 안되는 부분은 꼭 댓글로 남겨주시라~! 본인이 답변도 드리고, 추후에 방문한 분들께 더 나은 포스트를 제공해드리는데 큰 도움이 된다.
서버와 DB를 활용한 원격 판올림은 서버에 SQLite 데이터베이스(이하 DB)를 올려 놓은 후 앱에서 다운로드 받아 새로운 DB로 갱신하여 앱을 판올림하는 것이다.
이는 안드로이드보다는 아이폰에 더 필요한 사항으로, 검수가 약 일주일이 걸리는 현실을 고려해 최근 여러 앱에 도입되는 추세다. (물론 이 전에도 지속적으로 도입되고 있었다. 예로 버스, 지하철 앱)

다행인 것은 안드로이드도, iOS도 SQLite를 지원한다는 것이다.
그러므로 우리는 이미지가 들어있지 않는 한 하나의 DB를 가지고 두 플랫폼을 모두 지원할 수 있다.
(하지만 본인 회사에서 서비스하는 앱은 이미지가 첨부되기 때문에 DB를 따로 운영한다 -_-; 무결성은 전적으로 사람에게로...;;;)
(현재 두 플랫폼 레이아웃에 맞춰봤을 때 더 큰 이미지를 탑재해 하나의 DB로 관리하고 있다. 아이폰 또는 안드로이드(xhdpi) 이미지를 탑재해서 둘 중 하나의 플랫폼에서 크기를 조절하여 쓰는 것도 한 방법이다.)

웹에서 찾아보면 "SQLite 활용하기", "DB 서버에서 다운받아 설치하기", "SQLite 테이블 생성" 등을 볼 수 있다. 본 포스트는 이러한 내용들을 통합한 것이라고 보면 된다. 참고로 본 포스트에서는 테이블 또는 레코드의 생성(테이블), 삭제, 수정, 삽입(레코드) 등은 다루지 않는다.

본 포스트에서 설명할 "서버에 업로드된 DB를 단말에서 받아 설치"하는 내용은 형규님의 포스트를 참고하였다.

본 포스트를 참고하기 위해서는 DB에 대한 기본지식, SQL문의 작성법, Java 네트워크 프로그래밍 등이 필요하다.

DB를 이용한 판올림의 과정은 다음과 같다.
(최초)DB생성 > DB 입력 > 서버에 DB 업로드 > 단말에 다운로드 > 단말에 설치 > 단말에서 활용
DB 생성은 최초 한 번만 해주면 되며, 이후에는 입력부터 시작해 업로드로 진행된다.

/* DB 생성, 입력 */
DB 생성은 SQLite를 위한 툴을 이용하면 한결 간편하다.
특히 Firefox의 부가기능인 SQLite Manager를 이용하면 쉽게 DB를 관리할 수 있다.
SQLite Manager로 검색하면 사용법등이 잘 나와있는 블로그들이 많다.
물병자리 시대의 여명 블로그 : SQLite Manager 설치하기

SQLite Manager에서 DB를 생성하면 DB 파일을 저장할 곳을 묻는데, 해당 위치에 가 보면 DB명으로 sqlite파일이 생성되어 있을 것이다.

/* 서버에 DB 업로드 */
DB가 생성되었다면 서버에 자리 한 켠을 마련하여 FTP등으로 올려놓자.

/* 단말에 다운로드 */
단말에서 DB를 내려받을지를 확인하는 경우는 2가지다.
1. 받아놓은 DB파일이 없을 때 (새로 설치 과정을 따르게 된다.)
2. 서버 파일이 갱신됐을 때 (판올림 과정을 따르게 된다.)

먼저 받아놓은 파일이 있는지 검색한다.
// 자주색으로 된 부분은 자신의 기호에 맞게 변경하여 사용하도록 한다.
File dbFile = new File(
Environment.getDataDirectory().getAbsolutePath() + "/data/" + getPackageName() + "/mydb.sqlite");
if (dbFile.exists()) {
    // 파일이 있을 경우 처리
} else {
    // 파일이 없다면 최초 DB 설치 과정으로 진행
    showDialog(DIALOG_DB_FIRST);
}

Environment.getDataDirectory().getAbsolutePath() 는 앱에 관련된 파일을 저장할 수 있는 공간의 경로를 받아온다.
위 경로 + data폴더에 파일을 저장할 수 있다.
그 중 자신이 쓸 것은 자신의 패키지 경로이므로 getPackageName()을 붙여준다.
마지막으로 DB 파일명을 붙인다.
(잘 이해가 안되시는 분은 mydb.sqlite 부분만 자신이 원하는 파일명으로 고쳐서 사용하시면 된다.)
DataDirectory를 사용하는 이유는 앱이 지워질 때 같이 지워질 수 있도록 하기 위함이다.
SD카드에 저장할 경우 사용자가 앱을 지워도 DB가 남아 기기 저장공간이 지저분해 질 수 있다.

- SD카드에 설치한 경우, 앱을 삭제했을 때 DB파일이 남는다. 외부에서 접근이 쉽다. 용량 확보가 용이하다.
- Data영역에 설치한 경우, 응용프로그램 관리에서 해당 앱 정보를 봤을 때 데이터에 있는 용량이 바로 이 DB 파일 용량이며, 데이터 지우기를 눌렀을 경우 DB파일을 지울 수 있고 앱을 삭제했을 경우 함께 삭제된다.

되도록 모든 문자열은 상수로 정하여 사용하는 것이 좋다. DB 경로는 DB를 열 때마다 사용된다.
(이 말인즉슨, Environment.getDataDirectory().getAbsolutePath() + "/data/" + getPackageName() + "/mydb.sqlite 과 같은 DB 경로는 여러 곳에서 쓰일 수 있기 때문에 매번 저 긴 코드를 붙여넣기 보다는 상수로 만들어 사용하는 것이 훨씬 용이하고 관리도 편하다는 것이다.)

다시 코드로 돌아가서 showDialog(DIALOG_DB_FIRST); 가 실행되면 현재 설치된 DB가 없어 설치를 진행해야 한다는 AlertDialog가 노출되고, 확인을 누르면 새로 다운로드받아 설치하게 된다.
(DIALOG_DB_FIRST는 상수다.)
*showDialog 참고
랩하는 프로그래머님 : [안드로이드] 다이얼로그 생성하기

그렇다면 DB 갱신은 어떻게 하는지 알아보자.

// 아래 코드는 위의 받아놓은 파일이 있는지 검색하는 코드를 확장한 것으로,
// 파일이 있을 경우를 처리하는 코드와 파일이 없을 경우를 처리하는 코드가 추가되었다.
/* checkDB() */
{
    File dbFile = new File(K.getDbAbsolutePath(this));
    if (dbFile.exists()) {

        // 파일이 있을 경우, DB의 상태를 확인한다. SELECT문을 날려서 데이터가 날라오지 않는다면 DB파일에 이상이 생긴 것이므로 다시 다운로드 받는다.
         SQLiteDatabase db = SQLiteDatabase.openDatabase(K.getDbAbsolutePath(SplashActivity.this)null, SQLiteDatabase.OPEN_READONLY | SQLiteDatabase.NO_LOCALIZED_COLLATORS);
        Cursor cursor = db.rawQuery("SELECT * FROM tablename LIMIT 1, 10"null);
        // SELECT문이 레코드를 못찾을 경우 count는 -1이다.        if (!db.isOpen() || cursor.getCount() <= 0) { 
            // DB가 비정상이라면 새로운 DB를 받기 전 사용자에게 대화상자로 알린다.
            handler.sendEmptyMessage(WHAT_DB_RECOVERY);
            return;
        } else {

            // 네트워크 처리를 위해 Thread 생성
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() { 
                    try {
                        // 서버의 DB 경로로 URL 객체를 생성한다.
    // K.DB_URL = "http://xxxxxxx.xxx/xxxxxx/db.sqlite"
                        URL url = new URL(K.DB_URL);
                        // 해당 URL로 연결한다.
                        URLConnection conn = url.openConnection();
                        // 해당 경로에 위치한 대상의 가장 최근에 수정된 날짜를 받아온다.(Millisecond)
                        // lastModified 변수는 long 형태로 선언되어야 한다. 클래스 멤버다.
                        lastModified = conn.getLastModified();
                        // 받아온 수가 0보다 크면 제대로 연결됐으므로 정상처리한다.
                        if (lastModified > 0) { 

                            // 단말기 내에 저장된 DB의 최신 변경 날짜를 조회하기 위해 생성한다.

                            // SharedPreferences 에 대해서는 다음을 참고하자.
                            // [Android] 상태 저장하기 (savedInstanceState, SharedPreference)

                            SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(SplashActivity.this);
                            // 저장된 DB의 수정 날짜를 조회해 서버의 수정 날짜와 비교한다.
                            if (lastModified > pref.getLong("savedLatest", 0)) {
                                handler.sendEmptyMessage(WHAT_DB_UPDATE);
                            // 서버 DB가 갱신되지 않았다면 메인화면으로 넘어간다. (앱 바로 사용)                            } else {
                                handler.sendEmptyMessage(WHAT_DB_SKIP);
                            }

                        } else {
                            // 아니면 (lastModified가 0이면) 오류를 알린다.
                            handler.sendEmptyMessageDelayed(WHAT_DB_CHECK_ERROR, 1000);
                        }
                    } catch (MalformedURLException e) {
                        e.printStackTrace();
                        handler.sendEmptyMessageDelayed(WHAT_DB_CHECK_ERROR, 1000);
                    } catch (IOException e) {
                        e.printStackTrace();
                        handler.sendEmptyMessageDelayed(WHAT_DB_CHECK_ERROR, 1000);
                    }
                }
            });
            thread.start();
        }
        // 사용한 DB와 커서를 닫아준다.
        if (cursor != null) { 
            cursor.close();
        }
        if (db != null) { 
            db.close();
        }
    // 파일이 없을 경우 DB 최초 설치 과정으로 진행한다.
    } else {
        showDialog(DIALOG_DB_FIRST);
    }
}


handler가 호출하는 메서드에 들어가는 WHAT_DB_SKIP 이나 WHAT_DB_UPDATE같은 값들은 상수이며, 개발자가 임의의로 정하여 사용하면 된다.

본인의 경우, WHAT_DB_CHECK_ERROR일 때에는 그냥 정상 진행하는 것으로 처리하였다.
이는 이미 앞서 DB의 존재유무를 파악했기 때문에 서버에 접속할 수 없더라도 (만약 서버에 DB파일이 갱신되었다 하더라도) 이전 버전의 DB로 앱을 사용할 수 있도록 한 것이다.

DB가 갱신되었는지 비교 판단하는 방법으로 본인은 SharedPreferences(이하 SP)를 사용하였다.
일단 처음 설치시에는 DB에 대한 아무런 정보가 없기에 SP에 아무런 내용도 기록되어 있지 않겠지만,
일단 DB를 한 번이라도 설치했다면 DB의 수정된 날짜를 SP에 기록한 후 추후 서버의 수정된 날짜와 비교하여 업데이트를 판단하는 것이다.
그래서 위의 checkDB()가 끝나고 핸들러가 메세지를 받으면 각 메세지에 담겨진 what 값에 따라
대화상자를 띄우게 된다. (업데이트를 해야 하는지, 새로 설치해야 하는지)

이제 DB를 본격적으로 다운로드 받는 방법에 대해 알아보자.
위 소스를 보면 "처음 설치 과정"과 "업데이트", "복구" 과정이 있는데,
이 들의 차이는 간단하다.
다운로드 받기 전 나타나는 메세지에 차이가 있을 뿐이다.
1. 처음 설치한 경우
현재 저장된 DB가 없습니다.
앱을 사용하기 위해서 DB를 설치해야 합니다.
지금 설치하시겠습니까?
(본 과정은 네트워크를 사용하므로 와이파이 환경에서 진행하시길 권장합니다.)
[다운로드]    [종료]
2. DB 갱신

DB가 업데이트 되었습니다.

이전 버전 : 2012년 2월 18일 18시 16분
최신 버전 : 2012년 2월 22일 11시 43분
[다운로드]    [나중에]


3. DB 복구

DB가 손상되었습니다.
앱을 사용하기 위해서 DB를 복구해야 합니다.
지금 복구하시겠습니까?
(본 과정은 네트워크를 사용하므로 와이파이 환경에서 진행하시길 권장합니다.)
[다운로드]    [종료]


그리고 다운로드를 누르면 같은 메서드로 진입한다. 종료를 누르면 앱이 종료된다.
(대화상자에서 [다운로드] 버튼을 누르면 아래 메서드로 진입하도록 작성하면 된다.)
그럼 이제 DB를 내려받아보자.
/* downloadDB() */
 {
    // 내려받기 진행상황을 보여주기 위한 ProgressDialog를 띄운다.
    showDialog(DIALOG_DB_DOWNLOAD);
    Thread thread = new Thread(new Runnable() {
        
        @Override
        public void run() {
            InputStream is = null;
            FileOutputStream fos = null;
            BufferedOutputStream bos = null;
            URL url;
            URLConnection conn;
            try {
                // 서버의 DB 경로로 URL 객체를 생성한다.
                url = new URL(K.DB_URL);
                // 해당 URL로 연결한다.
                conn = url.openConnection();
                // 서버에 있는 DB 파일의 용량을 받아온다.
                int lengthOnServer = conn.getContentLength();
                // 용량이 0보다 크면 제대로 연결되었다고 판단하고 정상 진행한다.
                if (lengthOnServer > 0) {
                    dbDialog.setMax(lengthOnServer);
                } else {
                    // 용량이 0보다 크지 않을 경우 오류임을 표시한다.
                    handler.sendEmptyMessageDelayed(WHAT_DB_DOWNLOAD_ERROR, 1000);
                    return;
                }
                if (lastModified == 0)
                    lastModified = conn.getLastModified();
                }
                // 다운로드 받은 파일을 시스템에 저장하기 위해 InputStream을 얻는다.
                is = conn.getInputStream();
                
                // DB 파일을 저장할 경로로 File 객체를 생성한다. (폴더 경로만)
                // 본인의 경우 K.getDbDirPath()를 호출하면 아래 경로를 반환하도록 작성하였다.
                // Environment.getDataDirectory().getAbsolutePath() + "/data/" + getPackageName() + "/mydb.sqlite"
                File dir = new File(K.getDbDirPath(Activity.this));
                // 만약 폴더가 없다면 생성한다.
                if (!dir.exists()) {
                    dir.mkdir();
                }
                // 폴더경로에 파일명까지 붙인 경로로 File 객체를 생성한다.
                File target = new File(K.getDbAbsolutePath(Activity.this));
                // 파일이 존재한다면 삭제한다.
                if (target.exists()) {
                    target.delete();
                }
                // 그리고 새로운 파일을 생성한다.
                target.createNewFile();
                
                // 아래는 생성한 파일에 서버의 DB를 쓰는 과정이다.
                fos = new FileOutputStream(target);
                bos = new BufferedOutputStream(fos);
                int bufferLength = 0;
                <font color="#009e25">// 다운로드가 진행되는동안 다운로드된 크기를 축적하는 맴버 변수다.</font>
                totalLength = 0;
                // 버퍼 크기가 클 수록 다운로드는 빨라진다. 하지만 메모리를 많이 잡고 있다는 것을 명심하자.
                byte[] buffer = new byte[1024];
                while((bufferLength = is.read(buffer)) > 0) {
                    // bos.write()로 파일을 쓸 수 있다.
                    bos.write(buffer, 0, bufferLength);
// 총 받은 양을 기록한다.
                    totalLength += bufferLength;
                    runOnUiThread(new Runnable() {
                        
                        @Override
                        public void run() {
                            // 진행 상태 대화창에 진행량을 증가시킨다.
                            dbDialog. setProgress(totalLength);
                        }
                    });
                    // sleep()없이 진행해보시면 아시겠지만, 없을 때 속도는 빠를지라도 사용자에게 명시적으로 보여주기 힘든 상황이 발생한다. 진행 과정이 부드럽게 보여지지 않는다. 하지만 없으면 다운로드가 빨라진다.
                    Thread.sleep(1);
                }

                // 내려받은 용량과 서버에 있는 DB 파일의 용량이 같지 않으면 오류를 표시한다.
                if (totalLength != lengthOnServer) {
                    handler.sendEmptyMessageDelayed(WHAT_DB_DOWNLOAD_ERROR, 1000);
                    return;
                }

                // 마지막으로 100% 달성을 보여주기 위해 진행상태를 최대치로 입력한다.
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        dbDialog.setProgress(dbDialog.getMax());
                    }
                });

                // 다운로드가 완료된 후 행동을 취하기 위해 Handler에 Message를 전달한다.
                handler.sendEmptyMessageDelayed(WHAT_DB_DOWNLOADED, 1000);
            } catch (MalformedURLException e) {
                e.printStackTrace();
                handler.sendEmptyMessageDelayed(WHAT_DB_DOWNLOAD_ERROR, 1000);
            } catch (IOException e) {
                e.printStackTrace();
                handler.sendEmptyMessageDelayed(WHAT_DB_DOWNLOAD_ERROR, 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                // 최종적으로 사용했던 Stream들을 정리한다.
                try {
                    if (bos != null) bos.close();
                    if (fos != null) fos.close();
                    if (is != null) is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    });
    thread.start();
}

다음 소스를 같이 살펴보도록 하자.

runOnUiThread(new Runnable() {
    @Override
    public void run() {
        // 진행 상태 대화창에 진행량을 증가시킨다.
        dbDialog.setProgress(totalLength);
    }
});
runOnUiThread는 UI작업을 할 때 사용하는 메서드다. 이 메서드를 UIThread가 아닌 Thread에서 호출하면 해당 작업을 UI Thread의 event queue에 집어넣는다.
* 자세한 Thread관련 내용은 다음 블로그를 참고하면 많은 도움이 될 듯 싶다.

하지만 아직까지 밝혀지지 않은 미스테리가 있다. runOnUiThread()를 사용하든, Handler의 post()를 사용하든 ProgressDialog의 ProgressBar와 Progress값이 서로 일치하지 않는다는 것이다. Bar는 덜 찼는데 값은 100%라거나, 반대의 경우가 생기기도 한다.
해서 본인이 생각한 방법이 바로 Thread.sleep(1);인 것이다. 주기적으로 쉬어주면 진행상태도 자연스럽고, 위와 같은 미스테리도 어느정도 해결된다.

진행 중 인터넷이 끊어지거나 용량이 상이한 경우
handler.sendEmptyMessageDelayed(WHAT_DB_DOWNLOAD_ERROR, 1000);
가 호출되는데, 이 때 오류를 알리는 AlertDialog를 띄우고 재시도를 하거나 종료할 수 있도록 하였다.

DB 다운로드가 끝났으면, 다운로드 받은 DB의 수정된 날짜를 SP에 저장하도록한다.
(Handler에서 Message 처리)
 // DB를 다 받고 시스템에 저장했으므로 SharedPreferences에 최신 날짜로 저장한다.

SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(Activity.this);
SharedPreferences.Editor editor = pref.edit();
editor.putLong("savedDBTime", lastModified);
editor.commit();

그리고 앱의 다음 과정으로 진행하면 된다.

여기까지가 서버에 있는 DB를 단말에 내려받아서 설치하는 과정이다.

다음으로 내려받은 DB를 어떻게 활용할 수 있는지 간단하게나마 소개하겠다.
그리고 내려받는 도중 3G, WiFi간 전환시 어떻게 대처할 수 있는지 개인적은 의견을 포스팅하겠다.





내용이 이상하거나, 잘못됐거나, 이해가 안되는 부분은 꼭 댓글로 남겨주시라~! 본인이 답변도 드리고, 추후에 방문한 분들께 더 나은 포스트를 제공해드리는데 큰 도움이 된다.

댓글 1개:

  1. Are you looking to earn money from your visitors with popunder ads?
    In case you do, did you take a look at exoClick?

    답글삭제