posted by 프띠버리 2013. 11. 15. 11:32

지역 저장소에 대한 훨씬 더 정교하면서 가장 강력한 방법은 SQLite 데이터베이스를 사용하는 것이다. 모든 애플리케이션은 자신의 SQLite

데이버베이스를 장착하고 있어 애플리케이션 내의 어떠한 클래스에든 접근할 수 있지만, 외부 애플리케이션에서는 그렇지 못하다.

복잡한 쿼리나 코드 블록으로 넘어가기에 앞서 SQLite 데이터베이스가 무엇인지 간단히 알아보자.

 

SQL(Structured Query Language)은 관계형 데이터베이스의 데이터를 관리하기 위한 목적으로 설계된 프로그래밍 언어다. 관계형 데이터베이스는

입력과 삭제, 수정, 쿼리를 수행할 수 있게 해주며, 또한 스킴(테이블로 생각하자)을 생성하거나 수정할 수 있게 해준다. SQLite는 단순히

MSSQL과 PostgreSQL 등의 다른 유명한 데이터베이스 시스템의 축소판이라 할 수 있다. SQLite는 완전히 독립적이고 서버 기능을 제공하지 않으며,

트랜잭션을 지원하며 쿼리를 수행할 수 있는 표준 SQL 언어를 사용한다. 독립적이며 실행가능하다는 즉성으로 인해 SQLite는 매우 효율적이고

유연하며 광범위한 플랫폼에 걸쳐 다양한 분야의 프로그래밍 언어에서 접근 가능하다.

 

이제 다음 예제 코드를 통해 새로운 SQLite 데이터베이스 스킴 인스턴스와 간단한 테이블을 생성하는 과정을 가볍게 살펴보자.

package jwei.apps.dataforandroid.ch1;

 

import android.content.Context;

import android.database.sqlite.SQLiteDatabase;

import android.database.sqlite.SQLiteOpenHelper;

import android.util.Log;

 

public class SQLiteHelper extends SQLiteOpenHelper {

 

    private static final String DATABASE_NAME = "my_database.db";

 

    // 테이블과 데이터베이스 업데이트를 위해 이 숫자를 토글한다.

    private static final int DATABASE_VERSION = 1;

 

    // 생성하고자 하는 테이블 이름

    public static final String TABLE_NAME = "my_table";

 

    // 일부 예제 필드

    public static final String UID = "_id";

 

    public static final String NAME = "name";

 

    SQLiteHelper(Context context) {

        super(context, DATABASE_NAME, null, DATABASE_VERSION);

    }

 

    @Override

    public void onCreate(SQLiteDatabase db) {

        db.execSQL("CREATE TABLE " + TABLE_NAME + " (" + UID + " INTEGER PRIMARY KEY AUTOINCREMENT," + NAME

                + " VARCHAR(255));");

    }

 

    @Override

    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

        Log.w("LOG_TAG", "Upgrading database from version " + oldVersion + " to " + newVersion

                + ", which will destroy all old data");

 

        // 업그레이드되면 이전 테이블을 제거한다.

        db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);

 

        // 테이블의 새 인스턴스를 생성한다.

        onCreate(db);

    }

}

여기서 주목해야 한 첫 번째는 사용자가 정의 가능한 데이터베이스 스킴을 생성하기 위해 반드시 SQLiteOpenHelper 클래스를 재정의해야 한다는 점이다.

SQLiteOpenHelper를 재정의한 후 onCreate() 메소드를 재정의해 테이블의 구조를 지시할 수 있다. 예제의 경우 두 개의 칼럼, 즉 ID 칼럼과 name 칼럼을

갖는 테이블을 생성한다. 쿼리는 SQL로 다음 명령을 실행하는 것과 동일하다.

CRATE TABLE my_table(_id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(255));

또한 ID 칼럼에 PRIMARY_KEY와 AUTOINCREMENT 속성이 지정돼 있음을 확인했을 것이다. 이는 안드로이드에서 생성된 모든 테이블에 대해 권장되며,

앞으로도 이 표준을 지켜나갈 것이다. 마지막으로 name 칼럼이 최대 255 길이의 문자를 갖는 string 타입으로 선언된 것을 확인할 수 있다.

 

onCreate() 메소드를 재정의한 후 onUpgrade() 메소드도 재정의할 수 있다. 이 메소드를 통해 테이블의 구조를 빠르고 간단하게 변경할 수 있다.

필요한 작업은 DATABASE_VERSION 정수형을 증가시키는 것이 전부이며, 다음에 SQLiteHelper 인스턴스가 생성될 때 자동으로 onUpgrage() 메소드가

호출되며, 이때 제일 먼저 구 버전의 데이터베이스를 삭제한 후 새로운 버전의 데이터베이스를 생성한다.

마지막으로 거의 뼈대만 있는 테이블에 아주 기본적인 값을 입력하거나 쿼리하는 방법을 간단히 살펴보자.

package jwei.apps.dataforandroid.ch1;

 

import jwei.apps.dataforandroid.R;

import android.app.Activity;

import android.content.ContentValues;

import android.database.Cursor;

import android.database.sqlite.SQLiteDatabase;

import android.os.Bundle;

import android.util.Log;

 

public class SQLiteExample extends Activity {

 

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.main);

 

        // SQLITE HELPER를 초기화한다.

        SQLiteHelper sqh = new SQLiteHelper(this);

 

        // 읽고 쓸 수 있는 데이터베이스를 가져온다.

        SQLiteDatabase sqdb = sqh.getWritableDatabase();

 

        // 방법 #1: CONTENTVALUE CLASS를 사용한 입력

        ContentValues cv = new ContentValues();

        cv.put(SQLiteHelper.NAME, "jason wei");

 

        // INSERT METHOD 호출

        sqdb.insert(SQLiteHelper.TABLE_NAME, SQLiteHelper.NAME, cv);

 

        // 방법 #2: SQL 쿼리를 사용해 입력

        String insertQuery = "INSERT INTO " + SQLiteHelper.TABLE_NAME + " (" + SQLiteHelper.NAME + ") VALUES ('jwei')";

        sqdb.execSQL(insertQuery);

 

        // 방법 #1: 래퍼 메소드를 사용한 쿼리

        Cursor c = sqdb.query(SQLiteHelper.TABLE_NAME, new String[] { SQLiteHelper.UID, SQLiteHelper.NAME }, null,

                null, null, null, null);

 

        while (c.moveToNext()) {

            // 칼럼 인덱스와 해당 칼럼의 값을 얻어온다.

            int id = c.getInt(c.getColumnIndex(SQLiteHelper.UID));

            String name = c.getString(c.getColumnIndex(SQLiteHelper.NAME));

            Log.i("LOG_TAG", "ROW " + id + " HAS NAME " + name);

        }

 

        c.close();

 

        // 방법 #2: SQL SELECT QUERY사용하기

        String query = "SELECT " + SQLiteHelper.UID + "," + SQLiteHelper.NAME + " FROM " + SQLiteHelper.TABLE_NAME;

        Cursor c2 = sqdb.rawQuery(query, null);

 

        while (c2.moveToNext()) {

            int id = c2.getInt(c2.getColumnIndex(SQLiteHelper.UID));

            String name = c2.getString(c2.getColumnIndex(SQLiteHelper.NAME));

            Log.i("LOG_TAG", "ROW " + id + " HAS NAME " + name);

        }

 

        c2.close();

 

        // 데이터베이스 연결 종료

        sqdb.close();

        sqh.close();

    }

}

이 예제에 주의를 기울이기 바란다. 이 예제는 이후 글에서도 사용될 것이다. 이 예제에서는 제일 먼저 SQLiteHelper를 초기화하고 쓰기 가능한

SQLiteDatabase객체를 얻는다. 그런 후 아주 편리한 래퍼 메소드인 ContentValues 클래스를 사용해 테이블에 행을 입력하거나 갱신, 삭제를

빨리 수행할 수 있다. 여기서 ID 칼럼을 AUTOINCREMENT 필도로 생성했기 때문에 행을 입력할 때 수작업으로 ID를 할당하거나 증가시킬 필요가

없다는 점을 기억해두자. 따라서 ID 필드 없이(예제에서는 name 칼럼) ContentValues를 전달하기만 하면 된다.

그런 다음 SQLiteDatabase 객체로 다시 돌아가 insert() 메소드를 호출한다. 첫 번째 인자는 단순한 데이터베이스 이름이며, 세 번째 인자는 방금

생성한 ContentValue다. 두 번째 인자는 좀 까다롭다. 기본적으로 비어있는 CententValue가 전달되는 경우에 SQLite 데이터베이스는 빈 행을

입력할 수 없기 때문에 두 번째 인자로 전달된 칼럼이 무엇이든지 SQLite 데이터베이스는 자동으로 해당 칼럼의 값을 null로 설정한다.

이렇게 함으로써 SQLite의 예외 발생을 회피할 수 있다.

 

추가적으로 execSQL() 메소드를 활용한 두 번째 방법처럼 기본 SQL 쿼리를 전달해 데이터베이스에 행을 입력할 수 있다.

최종적으로 이제 테이블에 두 개의 행을 입력했고, 이제 이 행을 가져와 읽는 연습을 해보자. 여기서도 두 가지 방법을 알아본다.

첫 번째는 SQLiteDatabase 도우미 메소드 query()를 사용하는 것이고, 두 번째는 기본 SQL 쿼리를 실행하는 것이다.

두 경우 모두 Cursor 객체가 반환된다. Cursor는 쿼리에 의해 반환된 하위 테이블의 행을 순차적으로 반복하는 것으로 생각할 수 있다.

        while (c.moveToNext()) {

            // 칼럽 인덱스와 관련 칼럼의 값을 얻어온다.

            int id = c.getInt(c.getColumnIndex(SQLiteHelper.UID));

            String name = c.getString(c.getColumnIndex(SQLiteHelper.NAME));

            Log.i("LOG_TAG", "ROW " + id + " HAS NAME " + name);

        }

원하는 Cursor를 확보했다면 나머지는 직관적이다. 커서는 while 루프로 전달해야할 각 행을 가져올 수 있게 반복자처럼 동작하기 때문에

각 루프내에서 행 단위로 커서를 아래로 이동시켜야 한다. 그 다음 while 루프 내에서 가져오고자 하는 데이터의 칼럼 인덱스를 얻어야 한다.

예제에서는 단순히 두 칼럼 모두 얻어왔지만, 실전에서는 주어진 시간에 특정한 칼럼으로부터 필요한 데이터만 가져오는 편이 좋다.

마지막으로 커서의 get() 메소드로 칼럼의 인덱스를 전달한 후 칼럼의 타입이 정수형인 경우 getInt() 메소드를 호출한다.

문자열이라면 getString() 메소드를 호출한다.

 

posted by 프띠버리 2013. 11. 15. 10:52

한편으로 외부 저장소는 데이터와 파일을 전화기의 외부 SD카드 Secure Digital Card로 저장한다. 내부/외부 저장소에 대한

개념은 비슷하다. 따라서 앞서 살펴본 내용, 즉 SharedPreferences 대비 외부 저장소의 장단점을 살펴보자.

SharedPreference는 오버헤드가 크지 않기 때문에 단순한  Map 객체 읽기/쓰기 작업은 디스크 읽기/쓰기 작업에 비해 훨씬

더 효과적이다. 하지만 SharePreference는 간단한 기본형 값으로 제한되며, 본질적으로 유연함을 효율성을 바꾸고 있는 셈이다.

내부/외부 저장소 매커니즘을 통해 훨씬 더 많은 데이터 청크(즉, 완전한 XML 파일)는 물론이고, 더욱 복잡한 형태의 데이터

(미디어 파일과 이미지 파일 등)를 저장할 수 있다.

 

내부 저장소 대비 외부 저장소는 어떤 차이가 있을까? 이 둘 사이의 장단점은 좀 더 미묘하다. 우선 저장소 공간의 크기(메모리)를

생각해보자. 사용자가 소유한 전화기에 따라 저장 공간은 다양하지만 내부 메모리 전체 크기가 아주 작을 수 있다.

외부 저장소는 어떤 SD카드를 쓰는지에 따라 크기가 달라진다.

 

내부 저장소의 경우 데이터는 자신의 애플리케이션에 의해서만 접근이 가능하며, 따라서 잠재적으로 악의적인 외부 애플리케이션으로부터

매우 안전한다. 장점은 애플리케이션이 삭제되면 내부 메모리 또한 정리된다는 점이다.

외부 저장소의 경우 본질적으로 읽기와 쓰기가 가능하기 때문에 저장된 모든 파일은 외부 애플리케이션과 사용자에게 노출된다.

따라서 파일이 안전하고 깨끗하게 유지될지 보장할 수 없다.

 

다음 예제를 통해 외부 SD 카드에 실제로 어떻게 접근할 수 있는지 살펴보자.

package jwei.apps.dataforandroid.ch1;

 

import java.io.BufferedWriter;

import java.io.File;

import java.io.FileWriter;

import java.io.IOException;

 

import jwei.apps.dataforandroid.R;

import android.app.Activity;

import android.os.Bundle;

import android.os.Environment;

import android.util.Log;

 

public class ExternalStorageExample extends Activity {

 

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.main);

 

        String fileName = "my_file.txt";

        String msg = "Hello World.";

 

        boolean externalAvailable = false;

        boolean externalWriteable = false;

        String state = Environment.getExternalStorageState();

 

        if (state.equals(Environment.MEDIA_MOUNTED)) {

            // 미디어는 사용 가능하며, 쓰기도 가능하다

            externalAvailable = true;

            externalWriteable = true;

        } else if (state.equals(Environment.MEDIA_MOUNTED_READ_ONLY)) {

            // SD 카드는 사용 가능하지만, 쓰기는 불가능하다

            externalAvailable = true;

        } else {

            // 여러 상황에 실패할 수 있다.

            // 특별한 동작은 없다.

        }

 

        if (externalAvailable && externalWriteable) {

            // API 레벨 7 혹은 그 이하 버전에서 SD 카드 디렉토리를 가져온다.

            File root = Environment.getExternalStorageDirectory();

            File f = new File(root, fileName);

            try {

                // 내부 저장소 쓰기 방법과 차이가 있을에 유의한다.

                FileWriter fWriter = new FileWriter(f);

                BufferedWriter out = new BufferedWriter(fWriter);

                out.write(msg);

                out.close();

            } catch (IOException e) {

                e.printStackTrace();

            }

        } else {

            Log.e("LOG_TAG", "SD CARD UNAVAILABLE");

        }

    }

 

}

 

위 코드를 실행하기 위해 매니페스트 파일에 WRITE_EXTERNAL_STORAGE 권한을 추가하는 것을 잊지 말자.

여기서 실제로 마운트되고 쓰기 가능한 외부 SD 카드를 탐지할 수 있게 Environment 클래스의 getExternalStorageState() 메소드를 호출했다.

이런 사전 확인 없이 파일을 읽거나 쓴다면 에러가 발생할 수 있다.

SD 카드가 마운트돼 실제로 쓰기가 가능한 상황이라면 API 레벨 7 혹은 그 이후에 getExternalStorageDirectory() 를 호출해 SD 카드의 루트

파일 경로를 가져올 수 있다. 이 관점에서 단순히 새로운 파일을 생성해 FileWriter와 BufferedWritter를 초기화하고 문자열을 팡리에 쓰고자 한다.

여기서 주목해야 할 부분은 외부 저장소를 처리할 때 디스크에 쓰는 방법이 이전에 살펴본 내부 저장소에 쓰는 방법과 다르다는 점이다.

이는 실제로 기억하고 이해해야 할 중요한 부분이며, 이러한 쓰기 메소드에 대해 특별히 강조했던 이유이기도 하다. 내부 저장소 예제에서는

Context 클래스의 openFileOutput() 메소드(두 번째 인자로 모드를 받는)를 호출해 FileOutputStream 개체를 얻었다.

MODE_PRIVATE를 전달하면 이면에서 FileOutStream으로 파일을 생성하고 쓸 때마다 해당 파일은 애플리케이션의 고유 ID로 암호화되고

서명된다. 따라서 외부 애플리케이션은 해당 파일의 내용에 접근할 수 없다.

하지만 외부 저장소에 파일을 생성하고 쓸 때는 기본적으로 어떠한 보안 적용 없이 파일이 생성되기 때문에 모든 애플리케이션은 이 파일을

읽거나 쓸 수 있따. 이는 내부 저장소가 아니라 외부 SD 카드에 쓸 때에는 표준 자바 메소드를 사용할 수 있는 이유이기도 하다.

마지막으로 유념해야 할 것은 이클립스의 DDMS perspective에서 새로 생성된 파일을 볼 수 있기 때문에 SD 카드가 설정돼 있따면

DDMS에서 새롭게 생성된 텍스트 파일을 쉽게 확인할 수 있으며 애플리케이션을 개발할 때 DDMS perspective를 잘 활요하면 빠르게 파일을

넣거나 뺄 수 있으며, 디스크에 쓰고 있는 파일을 모니터링할 수 있다.

API 레벨 8 이후에 소개된 외부 저장소 쓰기 관련 변경 사항에 대해 짧게 이야기하고자 한다. 실제로 이 변경에 대한 내용은

http://developer.android.com/reference/android/content/Context.html#getExternalFilesDir(java.lang.String)

문서를 참고하기 바란다.

하지만 API 레벨 8이나 이상 버전의 고수준 관점에서는 단순히 다음 두 가지 새로운 기본 메소드만 의미가 있다.

getExternalFilesDir(String type)

getExternalStoragePublicDirectory(String type)

이러한 메소드 각각에 대해 이제 type 매개변수를 전달할 수 있음을 확인할 수 있을 것이다. type 매개변수는 파일의 종류를 지정할 수 있게 하며,

올바른 하위 폴더로 구성되게 만단다. 첫 번째 메소드에서 반환된 외부 파일 디렉토리 루트는 애플리케이션마다 다르기 때문에 애플리케이션이

삭제될 때 관련된 모든 파일 역시 외부 SD로부터 제거된다. 두 번째 메소드에서 반환된 파일 디렉토리 루트는 공개돼 있는 것으로, 이 경로에

저장된 파일은 애플리케이션이 삭제되더라도 계속 남아있게 된다. 저장하고자 하는 파일의 종류에 따라 어떠한 것을 사용할지 결정하면 된다.

예를 들어 파일이 애플리케이션 내에서 재생된 미디어 파일이고, 사용자가 애플리케이션을 삭제하려고 한다면 사용자에게 이 파일은 더 이상

필요가 없을 것이다.

하지만 애플리케이션을 통해 사용자가 자신의 전화기에 배경 화면을 다운로드할 수 있게 허용한다면 이 경우에는 공개된 디렉토리로 이미지

파일을 저장하게 고려해야 한다. 그 결과로 사용자가 애플리케이션을 삭제한다고 해도 이 파일은 여전히 시스템에 의해 접근 될 수 있을 것이다.

지정할 수 있는 type 매개변수는 다음과 같다.

DIRECTORY_ALARMS

DIRECTORY_DCIM

DIRECTORY_DOWMLOADS

DIRECTORY_MOVIES

DIRECTORY_MUSIC

DIRECTORY_NOTIFICATIONS

DIRECTORY_PICTURES

DIRECTORY_PODCASTS

DIRECTORY_RINGTONES

지금까지 내부/외부 저장소 매커니즘에 대한 다소 장황한 글을 마쳤다.

 

 

posted by 프띠버리 2013. 11. 15. 10:10

안드로이드의 내부 저장소 매커니즘부터 시작해보자. 표준 자바 프로그래밍에 경험이 있는 사람이라면 이 절은 꽤나 친숙하게 느껴질 것이다.

안드로이드의 내부저장소는 단순히 각 애플리케이션의 내부 메모리와 관련된 파일을 읽고 쓸 수 있게 해준다. 이 파일은 애플리케이션에 의해

서만 접근 가능하며, 다른 애플리케이션이나 다른 사용자에 의해서는 접근이 불가능하다. 또한 애플리케이션이 삭제되면 저장돼 있던 파일

또한 자동으로 삭제된다.

다음은 애플리케이션의 내부 저장소에 접근하는 방법에 대한 간단한 예다.

package jwei.apps.dataforandroid.ch1;

 

import java.io.FileOutputStream;

import java.io.IOException;

 

import jwei.apps.dataforandroid.R;

import android.app.Activity;

import android.content.Context;

import android.os.Bundle;

 

public class InternalStorageExample extends Activity {

 

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.main);

 

        // 파일 이름

        String fileName = "my_file.txt";

 

        // 파일에 쓸 문자열

        String msg = "Hello World.";

        try {

            // 파일 생성 후 쓰기

            FileOutputStream fos = openFileOutput(fileName, Context.MODE_PRIVATE);

            fos.write(msg.getBytes());

            fos.close();

        } catch (IOException e) {

            e.printStackTrace();

        }

 

    }

 

}

여기서는 단순히 Context 클래스의 openFileOutput() 메소드를 사용한다.

openFileOutput() 메소드는 첫 번째 인자로 생성될(혹은 덮어 쓸) 파일 이름을 받고, 두 번째 인자로 파일의 가시성을 받는다.

그 다음 원하는 문자열을 byte형태로 변환해 출력 스트림의 write() 메소드로 전달한다. openFileOutput() 으로 지정될 수 있는 부가적인

모드에 대해 살펴보자. 즉, 다음과 같다.

MODE_APPEND 이 모드는 기존 파일을 열어 문자열을 기존 내용에 추가(다름 모드에서는 기존 내용이 삭제된다)할 수 있게 해준다.

여러분이 이클립스로 프로그래밍을 하고 있다면 DDMS로 이동해 애플리케이션의 내부 파일을 살펴볼 수 있다.

 

따라서 방금 생성한 텍스트 파일을 볼 수 있을 것이다. 터미널로 개발하는 사람들은 /data/data/{앱 경로}/files/my_file.txt 경로에서 찾아볼 수 있다.

불행히도 다시 파일을 읽어오는 작업은 좀 더 복잡하다. 이와 관련된 코드는 다음과 비슷하다.

package jwei.apps.dataforandroid.ch1;

 

import java.io.FileInputStream;

import java.io.IOException;

import java.io.InputStreamReader;

 

import jwei.apps.dataforandroid.R;

import android.app.Activity;

import android.os.Bundle;

import android.util.Log;

 

public class InternalStorageExample2 extends Activity {

 

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.main);

 

        // 파일 이름

        String fileName = "my_file.txt";

 

  // 이번에는 파일 입력 스트림을 연다.

        try {

            FileInputStream fis = openFileInput(fileName);

            InputStreamReader isr = new InputStreamReader(fis);

 

            // 임의의 길이로 문자열을 읽는다.

            StringBuilder sb = new StringBuilder();

            char[] inputBuffer = new char[2048];

            int l;

            // 버퍼에 데이터를 채운다.

            while ((l = isr.read(inputBuffer)) != -1) {

                sb.append(inputBuffer, 0, l);

            }

 

            // 바이트를 문자열로 변환한다.

            String readString = sb.toString();

            Log.i("LOG_TAG", "Read string: " + readString);

 

            // 파일을 삭제할 수도 있다.

            deleteFile(fileName);

        } catch (IOException e) {

            e.printStackTrace();

        }

    }

}

여기서 파일 입력 스트림을 열어 이를 스트림 리더에 전달했다. 이를 통해 read()메소드를 호출해 데이터를 바이트 형태로 읽어 StringBuyilder에 추가할

수 있었따. 내용 전체를 읽었다면 단순히 StringBuilder로부터 문자열을 반환한다. 그것이 전부다! 완전한 코드를 위해 마지막 부분에서 확인할 수 있듯이

Context 클래스는 내부 저장소에 저장된 파일을 삭제할 수 있는 간단한 메소드를 제공한다.