IT_Programming/Android_Java

[펌] Android apk 내 대용량 파일 처리 방법(SQLite DB 등)

JJun ™ 2014. 5. 10. 21:24


 출처: http://hyeongkyu.net/110090943249


 

 

제가 개발하던 버스 정보 Application은 최초에 DB를 사용하지 않았습니다.

모든 데이터를 실시간으로 Web에서 긁어오는 방식이었죠.

이 방식은 가장 큰 문제가 있었으니, 모든 기능을 수행하기위해 통신을 너무 자주해야했고,
정류소의 좌표를 Google Map에서 사용 가능한 WGS84(위경도 좌표계) 값으로 주는 버스 정보 시스템은 없었습니다. (광주가 유일함)

결국 버스 정류소의 좌표값을 변환된 상태로 저장한 SQLite DB 파일을 Application과 함께 배포할 수 밖에 없는 상황에 처하게 되었습니다.

처음에는 삽질을 참 많이 하였습니다. 원래 삽질 과정까지 글에 모두 작성하려 하였지만, 시간 관계상 현재에 어떻게 작업이 되어있는지만 알려드리도록하겠습니다.

SQLite DB 파일의 작성

보통 SQLite DB 파일을 편하게 만들기 위해서는 두 가지의 방법이 널리 알려져있습니다.
하나는 SQLite Browser를 이용하는 방법과, Firefox SQLite Manager Plug-in을 이용하는 방법입니다.
둘 다 유명하고, 무료이며, SQLite의 단순함만큼 구하기도 쉽고 사용하기도 쉽습니다.

여기서 DB에 대한 설명까지 하면 길어지므로, 대략 ANSI-SQL 문법 외의 함수들은 MySQL과 비슷하다고 보시면됩니다.

저 같은 경우는 버스 정보 시스템의 노선 데이터와 정류소 데이터를 완전히 긁어서 SQL 텍스트로 만들어버리는 Java Application을 지역마다 작성합니다.

실행을 하게 되면 대충,

insert into route (routeid, routeno, routedir, area) values ('1_0_0', '1', '남모산종점 - 창원역', '14');
insert into bus_stop (bstopid, shortbstopid, bstopnm, posx, posy, longitude, latitude, area) values ('2018', '2018', '남모산종점', '464889.280259742', '306815.951024357', '128.71183659395177', '35.35843264867199', '14');
insert into bus_stop (bstopid, shortbstopid, bstopnm, posx, posy, longitude, latitude, area) values ('68', '68', '남모산', '464981.906734798', '306709.523004789', '128.7128472875616', '35.35746734615375', '14');
insert into bus_stop (bstopid, shortbstopid, bstopnm, posx, posy, longitude, latitude, area) values ('73', '73', '남모산', '464975.026533867', '306544.376828632', '128.7127584814465', '35.35597925621511', '14');
insert into bus_stop (bstopid, shortbstopid, bstopnm, posx, posy, longitude, latitude, area) values ('108', '108', '송등', '464684.293161406', '305544.795169219', '128.70948077226703', '35.34698841880731', '14');
insert into bus_stop (bstopid, shortbstopid, bstopnm, posx, posy, longitude, latitude, area) values ('141', '141', '대산초교', '464626.845483208', '304629.961939972', '128.70877661755884', '35.33874630163844', '14');
insert into bus_stop (bstopid, shortbstopid, bstopnm, posx, posy, longitude, latitude, area) values ('153', '153', '가술', '464637.347987646', '304224.087577537', '128.70886013353038', '35.335087278237786', '14');
insert into bus_stop (bstopid, shortbstopid, bstopnm, posx, posy, longitude, latitude, area) values ('2017', '2017', '장등', '464305.152220811', '303584.826440713', '128.70515598110435', '35.329346707986986', '14');
insert into bus_stop (bstopid, shortbstopid, bstopnm, posx, posy, longitude, latitude, area) values ('2015', '2015', '상등', '463957.945190519', '302970.170271329', '128.70128918250109', '35.32382874979514', '14');
insert into bus_stop (bstopid, shortbstopid, bstopnm, posx, posy, longitude, latitude, area) values ('2013', '2013', '고등포', '463713.771340082', '302637.820258106', '128.6985779289293', '35.320848679697455', '14');
.... 후략 ...

이런 식의 엄청나게 긴 문자열이 출력되며 저는 이것을 별도의 파일로 저장한 후,  SQLite JDBC를 통해  DB에 입력합니다.
SQLite Browser나 SQLite Manager를 이용할 수도 있는데, 저만 그런건지 대량의 SQL문을 실행할 때는 속도도 무지 느리고(사용이 불가능할 정도로), 다운도 많이 되었습니다.

package net.hyeongkyu.sqlite;


import java.io.BufferedReader;

import java.io.File;

import java.io.FileReader;

import java.sql.Connection;

import java.sql.DriverManager;

import java.sql.Statement;



public class SqlExecutor {

/**

* @param args

*/

public static void main(String[] args) {

FileReader fr = null;

BufferedReader br = null;

Connection conn = null;

try{

// 두개의 부분을 바꾼다. dbUrl은 SQLite DB파일의 경로이고, fileDir은 실행시킬 SQL 문이 저장되어있는 txt 파일의 경로이다.

// SQL txt 파일에는 빈 행이 없어야한다.

//String dbUrl = "jdbc:sqlite:d:/database.db";

//String fileDir = "d:/sql.txt";

// 이 경로는 내 MAC OS X에서 사용하는 경로이다.

String dbUrl = "jdbc:sqlite:/Users/hyeongkyu/temp/database.db";

String fileDir = "/Users/hyeongkyu/temp/sql.txt";

Class.forName("org.sqlite.JDBC");

conn = DriverManager.getConnection(dbUrl);

fr = new FileReader(new File(fileDir));

br = new BufferedReader(fr);

int count = 0;

conn.setAutoCommit(false);

String sql = null;

Statement stmt = conn.createStatement();

while((sql=br.readLine())!=null){

stmt.executeUpdate(sql);

System.out.println(""+(++count));

}

conn.commit();

}catch(Exception e){

e.printStackTrace();

if(conn!=null) try{conn.rollback();}catch(Exception et){}

}finally{

if(br!=null) try{br.close();}catch(Exception e){}

if(fr!=null) try{fr.close();}catch(Exception e){}

}

}

}

 소스 코드가 썩 보기 좋진 않지만, 위의 소스를 참고하면, 쉽게 SQLite DB에 대량의 명령어를 날릴 수 있습니다. 수만행을 수행하는데도 1~2초면 됩니다. 아주 기초적인 JDBC 예제의 수준을 벗어나지 않습니다.

해당 코드를 수행하기 위해서는 반드시 SQLite JDBC Driver가 필요하며, 이 또한 무료이며 간단한 검색으로 무진장 쉽게 구할 수 있습니다.


저렇게 완성이 된 SQLite 파일이 용량이 작다면 assets 경로에 저장하여 배포를 하게 됩니다.

파일의 크기가 1024kb가 넘는다면 1024kb 단위로 쪼개어서 assets에 저장한 후, Application이 최초에 실행되는 시점에서 합쳐야합니다.

그러나! DB 파일이 너무 크다면 이것을 정말 assets에 포함시킬 것인지에 대해 고려해보아야합니다.


일단 파일이 작다고 가정을 하겠습니다. 파일을 분할하고, 합치는 방법은, 아래와 같습니다.

 

package net.hyeongkyu.file;


import java.io.BufferedInputStream;

import java.io.BufferedOutputStream;

import java.io.File;

import java.io.FileInputStream;

import java.io.FileOutputStream;


public class FileSeparator {

public static void main(String[] args){

// 아래의 두 상수는 분할할 파일의 경로와, 몇 바이트로 분할할 것인지를 정의합니다.

final String FILE_PATH = "d:\\database.db";

final long BYTE_OF_UNIT = 1048576;

File input = new File(FILE_PATH);

FileInputStream fis = null;

BufferedInputStream bis = null;

FileOutputStream fos = null;

BufferedOutputStream bos = null;

try{

fis = new FileInputStream(input);

bis = new BufferedInputStream(fis);

byte[] buf = new byte[4096];

int l = 0;

int acc = 0;

int count = 0;

while((l=bis.read(buf))>0){

if(acc%BYTE_OF_UNIT==0){

File output = new File(FILE_PATH+"."+(count++));

if(fos!=null){

bos.close();

fos.close();

}

fos = new FileOutputStream(output);

bos = new BufferedOutputStream(fos);

}

bos.write(buf, 0, l);

acc += l;

}

}catch(Exception e){

e.printStackTrace();

}finally{

if(bos!=null) try{bos.close();}catch(Exception e){}

if(fos!=null) try{fos.close();}catch(Exception e){}

if(bis!=null) try{bis.close();}catch(Exception e){}

if(fis!=null) try{fis.close();}catch(Exception e){}

}

}

}

 

 

/**

 * 이 메서드는 Android Context 내에 정의되어 있어야합니다.

 */

private void combineFiles(){

// INPUT_ASSET_NAMES 병합할 assets 의 파일명을 나열합니다.

final String[] INPUT_ASSET_NAMES = new String[]{

"database.db.0", "database.db.1", "database.db.2", "database.db.3"

};

// OUTPUT_PATH에는 병합한 결과로 생성될 파일의 경로를 할당합니다.

final String OUTPUT_PATH = Environment.getExternalStorageDirectory().getAbsolutePath()+"/data/net.hyeongkyu.android.incheonBus/database.db";

final File OUTPUT = new File(OUTPUT_PATH);

InputStream is = null;

BufferedInputStream bis = null;

FileOutputStream fos = null;

BufferedOutputStream bos = null;

try{

fos = new FileOutputStream(OUTPUT);

bos = new BufferedOutputStream(fos);

for(int i=0;i<INPUT_ASSET_NAMES.length;i++){

is = getApplicationContext().getAssets().open(INPUT_ASSET_NAMES[i]);

bis = new BufferedInputStream(is);

int l=0;

byte[] buf = new byte[4096];

while((l=bis.read(buf))>0) bos.write(buf, 0, l);

try{bis.close();}catch(Exception e){}

try{is.close();}catch(Exception e){}

}

}catch(Exception e){

e.printStackTrace();

}finally{

if(bos!=null) try{bos.close();}catch(Exception e){}

if(fos!=null) try{fos.close();}catch(Exception e){}

if(bis!=null) try{bis.close();}catch(Exception e){}

if(is!=null) try{is.close();}catch(Exception e){}

}

}

 

 


역시 기초적인 java.io 예제 수준을 벗어나지 않습니다.

저러한 식으로 파일을 분할하여 assets에 포함시켰다가,  Application이 실행될 때 다시 합쳐서 외부 저장공간(일반적으로 SD Card)으로 옮겨버리면 되는 것입니다.


그러나!

파일의 크기가 커지기 시작하면 DB 파일을 정말로 assets에 포함시킬 것인가에 대해 고민해볼 필요가 있습니다.  assets에 포함된 파일은 apk에 포함되어 배포가 됩니다.

결과적으로 apk 파일이 커지게 되고, assets에 포함된 파일은 Read only이므로 더 이상 사용하지 않는다하더라도 삭제가 되지 않습니다.

3MB 정도의 DB가 3개의 파일로 분리되어 assets에 포함되었다고 치면, 결국 이 파일은 병합되어 External Storage로 옮겨질 것입니다. 그리고 Application에서는 병합된 파일만을 참조할 것이지만, assets에 포함되어 있던... 이제는 더 이상 쓸모없는 파일은 삭제가 되지 않고 여전히 내부 저장공간을 차지하고 있다는 얘기죠.


이에 대한 훌륭한 대안으로, Google Code의 Project Hosting이라는 서비스가 있습니다.


http://code.google.com/hosting


이 서비스 내에는 Download hosting이라는 서비스도 포함되어 있어서, 배포하고 싶은 파일을 미리 올려놓을 수 있습니다. 

저 또한 이 서비스를 이용하여 버스 정보 DB를 배포하고 있습니다.


http://code.google.com/p/android-korea-bus-information


하지만, 여기에 올린 파일은 어떠한 보안도 적용되지 않고 누구나 열람할 수 있게 됩니다.

업로드, 삭제 등의 기능은 계정의 관리자만이 가지게 되지만, 다운로드의 기능은 누구나 가지게 된다는 것입니다. 이 점은 알아두셔야합니다.


이 호스팅에서 제공하는 Download 기능을 이용하면, 파일을 업로드 해놓고 URL을 얻어낼 수 있습니다.

게다가 알아보기 쉬운 주소로 나오게 되며, 파일명이 변하지 않을 경우에는 파일을 교체하더라도 URL이 변하지 않기 때문에 매우 효율적이죠.


 

 

 

// S.DB_DOWNLOAD_URL DB 파일을 다운로드하게 될 주소 예를 들어, "http://android-korea-bus-information.googlecode.com/files/db.zip"

// S.DB_DATE 현재 버젼의 DB 날짜, 예를 들어 20100728

// Utility.unzip() 압축 해제를 위해 따로 정의한 유틸리티 메서드

 

 


final Thread thread = new Thread(new Runnable(){

@Override

public void run() {

try{

URL url = new URL(S.DB_DOWNLOAD_URL);

URLConnection connection = url.openConnection();

int length = connection.getContentLength();

progress.setMax(length);

InputStream is = null;

FileOutputStream fos = null;

BufferedOutputStream bos = null;

boolean success = false;

try{

is = connection.getInputStream();

File dir = new File(Environment.getExternalStorageDirectory().getAbsolutePath()+"/data/net.hyeongkyu.android.incheonBus");

if(!dir.exists()) dir.mkdirs();

File target = new File(dir.getAbsolutePath()+"/database."+S.DB_DATE+".zip");

if(!target.exists()) target.createNewFile();

else{

target.delete();

target.createNewFile();

}

fos = new FileOutputStream(target);

bos = new BufferedOutputStream(fos);

byte[] buf = new byte[1024];

downloadFlag = true;

int l = 0;

while((l=is.read(buf))>0){

tmpLength += l;

Log.i(S.TAG, tmpLength+" received.");

if(!downloadFlag) break;

bos.write(buf, 0, l);

activity.runOnUiThread(new Runnable() {

public void run() {

progress.setProgress(tmpLength);

}

});

}

success = true;

}catch(Exception e){

e.printStackTrace();

}finally{

try{bos.close();}catch(Exception e){}

try{fos.close();}catch(Exception e){}

try{is.close();}catch(Exception e){}

dialog.dismiss();

wakeLock.release();

}

// 성공 시 압축 해제하고, 파일명 변경

if(success){

File dir = new File(Environment.getExternalStorageDirectory().getAbsolutePath()+"/data/net.hyeongkyu.android.incheonBus");

File target = new File(dir.getAbsolutePath()+"/database."+S.DB_DATE+".zip");

Utility.unzip(target, dir, false);

// 파일명을 변경한다.

File[] listFiles = dir.listFiles();

for(File file:listFiles){

if(file.getName().toLowerCase().endsWith(".db")){

file.renameTo(new File(dir.getAbsolutePath()+"/database."+S.DB_DATE+".db"));

}else{

file.delete();

}

}

activity.runOnUiThread(new Runnable(){

@Override

public void run() {

Toast.makeText(activity, S.MSG_DB_DOWNLOAD_COMPLETE, Toast.LENGTH_SHORT).show();

}

});

}

}catch(Exception e){

e.printStackTrace();

}

}

});

thread.start();

 

위의 코드는 미리 올려놓은 DB 파일을 내려받아 External Storage에 특정 디렉토리로 압축을 해제시키는 코드이며, 전국 버스 앱에서 사용하고 있는 코드입니다.


이렇게 내려받은 SQLite DB 파일은 Android에서 다음과 같이  Open 시킬 수 있습니다.

 

public class IncheonBusDbAdapter {

private Context mContext;

private SQLiteDatabase mDb;

public IncheonBusDbAdapter(Context context, int accessFlags) {

super();

this.mContext = context;

File dbCopy = new File(Environment.getExternalStorageDirectory().getAbsolutePath()+"/data/net.hyeongkyu.android.incheonBus/database."+S.DB_DATE+".db");

if(dbCopy.exists()){

SQLiteDatabase db = SQLiteDatabase.openDatabase(dbCopy.getAbsolutePath(), null, accessFlags);

this.mDb = db;

}

}

public void close(){

this.mDb.close();

}


// ... 후략 ...

 

그럼 모두 즐 Android Programming 하십시오!

감사합니다!