펌위치 : http://blog.naver.com/legendx/40153264388
내용이 이상하거나, 잘못됐거나, 이해가 안되는 부분은 꼭 댓글로 남겨주시라~! 본인이 답변도 드리고, 추후에 방문한 분들께 더 나은 포스트를 제공해드리는데 큰 도움이 된다.
서버와 DB를 활용한 원격 판올림은 서버에 SQLite 데이터베이스(이하 DB)를 올려 놓은 후 앱에서 다운로드 받아 새로운 DB로 갱신하여 앱을 판올림하는 것이다.
이는 안드로이드보다는 아이폰에 더 필요한 사항으로, 검수가 약 일주일이 걸리는 현실을 고려해 최근 여러 앱에 도입되는 추세다. (물론 이 전에도 지속적으로 도입되고 있었다. 예로 버스, 지하철 앱)
다행인 것은 안드로이드도, iOS도 SQLite를 지원한다는 것이다.
그러므로 우리는 이미지가 들어있지 않는 한 하나의 DB를 가지고 두 플랫폼을 모두 지원할 수 있다.
(현재 두 플랫폼 레이아웃에 맞춰봤을 때 더 큰 이미지를 탑재해 하나의 DB로 관리하고 있다. 아이폰 또는 안드로이드(xhdpi) 이미지를 탑재해서 둘 중 하나의 플랫폼에서 크기를 조절하여 쓰는 것도 한 방법이다.)
웹에서 찾아보면 "SQLite 활용하기", "DB 서버에서 다운받아 설치하기", "SQLite 테이블 생성" 등을 볼 수 있다. 본 포스트는 이러한 내용들을 통합한 것이라고 보면 된다. 참고로 본 포스트에서는 테이블 또는 레코드의 생성(테이블), 삭제, 수정, 삽입(레코드) 등은 다루지 않는다.
본 포스트에서 설명할 "서버에 업로드된 DB를 단말에서 받아 설치"하는 내용은 형규님의 포스트를 참고하였다.
형규님 블로그 : Android apk 내 대용량 파일 처리 방법(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() {@Overridepublic 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 에 대해서는 다음을 참고하자.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() {@Overridepublic 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() {@Overridepublic 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() {@Overridepublic 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간 전환시 어떻게 대처할 수 있는지 개인적은 의견을 포스팅하겠다.
내용이 이상하거나, 잘못됐거나, 이해가 안되는 부분은 꼭 댓글로 남겨주시라~! 본인이 답변도 드리고, 추후에 방문한 분들께 더 나은 포스트를 제공해드리는데 큰 도움이 된다.
Are you looking to earn money from your visitors with popunder ads?
답글삭제In case you do, did you take a look at exoClick?