'Hangul2English'에 해당되는 글 5건

  1. 2010.09.16 [강좌A14] 안드로이드 실전 개발 - Notification, 알림메세지 구현
  2. 2010.09.03 [강좌A13] 안드로이드 실전 개발 - Hangul2English 최종소스 공개 (한글 자모 분리, Manifest) (6)
  3. 2010.09.01 [강좌A12] 안드로이드 실전 개발 - Main UI 소스, ListAdapter (1)
  4. 2010.09.01 [강좌A10] 안드로이드 실전 개발 - 데이터베이스 : Part2 (6)
  5. 2010.09.01 [강좌A09] 안드로이드 실전 개발 - 데이터베이스 : Part1 (2)
2010.09.16 22:39

[강좌A14] 안드로이드 실전 개발 - Notification, 알림메세지 구현




오래간만에 포스팅을 하게 되었습니다. 그동안 정신없이 바쁜 나날이었습니다.

오늘은 Hangul to English에 업그레이드 해야 될 내용이 있어서 내용 보충합니다.
Hangul to English는 앱 자체로 유용하기 보다는 다른 웹사이트나 앱에서 패스워드 입력시 도와주는 앱입니다. 그러나 App 실행 중에 다시 Hangul to English를 화면에 띄우는 게 귀챦아서 알림영역에 항상 나타나 쉽게 App을 호출할 수 있도록 하는 기능을 추가하겠습니다.

전체 강좌 목차

[강좌A01] Moteodev Studio를 이용한 안드로이드 개발 환경 구축 가이드
[강좌A02] 안드로이드 개발 참고 서적 소개
[강좌A03] Android 실전 개발 - 아이디어 / 기획 / Wireframe
[강좌A04] 안드로이드 실전 개발 - 아이콘 제작
[강좌A05] 안드로이드 실전 개발 - 레이아웃 및 리소스 : Part1
[강좌A06] 안드로이드 실전 개발 - 레이아웃 및 리소스 : Part2
[강좌A07] 안드로이드 실전 개발 - 리소스 해킹
[강좌A08] 안드로이드 실전 개발 - SQLite

[강좌A09] 안드로이드  실전 개발 - 데이터베이스 : Part1
[강좌A10] 안드로이드  실전 개발 - 데이터베이스 : Part2
[강좌A11] 안드로이드  실전 개발 - 데이터베이서 : Part3 (Motodev database)
[강좌A12] 안드로이드  실전 개발 - Main UI 소스, ListAdapter
[강좌A13] 안드로이드  실전 개발 - Hangul2English 최종 소스 공개 (한글 자모 분리, Manifest)

[강좌A14] 안드로이드 실전 개발 - Notification, 알림 

Notification 을 적용한 결과 화면은 다음과 같습니다.


먼저, Notification Service에 대한 개략적인 설명을 먼저 하도록 하겠습니다.

일반적으로 앱에서는 특정 이벤트 발생시에 사용자에게 이를 알려줄 수 있도록 개발 되어야 합니다. 여러가지 방법이 있겠지만 대표적인 방법이 폰 상단 상태바에 아이콘이나 점멸, 진동등을 함께 이용하여 표시하는 방법이 있습니다.
안드로이드에서 알림 메시지를 표시하는 가장 기본적인 방법은 알림에 사용할 정보를 Notification 객체에 설정 한 후 NotificationManager 클래스를 이용하여 화면에 표시햐면 됩니다.

샘플 코드를 보겠습니다.


String ns = Context.NOTIFICATION_SERVICE;
NotificationManager mNotificationManager = (NotificationManager) getSystemService(ns);


위 코드는 getSystemService(NOTIFICATION_SERVICE) 메소드를 이용해 NotificationManager 클래스의 인스턴스를 얻어오는 코드 입니다. 실제 상태바에 표시 및 해제 하기 위해서는 NotificationManager 인스턴스를 이용해야 합니다.


int icon = R.drawable.notification_icon;   
CharSequence tickerText = "Hello";
long when = System.currentTimeMillis();

Notification notification = new Notification(icon, tickerText, when);

위 코드는 기기의 상단 상태바에 표시될 Notification 객체 설정 부분입니다.
Icon은 상태바에 표시될 아이콘으로 hdpi에서는 38*38 사이즈가 적당하며, mdpi에서는 25 * 25 아이콘 사이즈가 적당합니다.
tickerText는 상태바에 표시되는 텍스트입니다.
When은 Notification을 표시할 시각입니다.


Context context = getApplicationContext();
CharSequence contentTitle = "My notification";
CharSequence contentText = "Hello World!";
Intent notificationIntent = new Intent(this, MainActivity.class);
PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
notification.setLatestEventInfo(context, contentTitle, contentText, contentIntent);

위의 코드는 상태바를 드래그해서 펼쳤을 때 나타나는 내용에 대한 설정 부분입니다.
contentTitle은 Notification List에 표시될 제목이구요.
contentText는 Notification List에 표시될 내용입니다.
notificationIntent는 실제 알림 항목을 클릭했을 때 실행할 Activity를 지정하는 부분입니다.

생성한 Notification을 Notification List에 표시하기 위해서는 setLatestEventInfo() 메소드로 추가 설정을 해야 합니다.

setLatestEventInfo 메소드의 인자중에 PendingIntent의 인스턴스를 넘기게 되어 있습니다. PendingIentent는 특정 Component(Activity / Service 등) 가 Intent 를 생성한 후, 해당 Intent 를 바로 사용하는 대신, 나 대신 다른 Component 가 해당 Intent 를 사용 할 수 있도록 할 때 사용하는 클래스입니다. 다른 말로 표현하면 PendingIetnet는 Intent를 전송하고자 하는 ‘송신자’가 인텐트를 하나 생성한 후, 별도의 컴포넌트에게 ‘나중에 이 인텐트를 나 대신 보내 주시게’ 하고 전달하고자 할 때 (일종의 위임이네요) 사용되는 클래스입니다.

PendingIntent.getActivity 는 PendingIntent 를 만드는 팩토리 함수입니다.
한가지 추가로 pendingintent 를 실행해주는 다른 component 가 그 intent 를 실행할때는 최초에 그 pendingintent 를 만든 component 가 갖고있는 권한으로 (각종 permission, 등) 실행됩니다.

mNotificationManager.notify(HELLO_ID, notification);

끝으로 Notification 을 표시하기 위해서 생성한 NotificationManager 인스턴스에서 notify를 호출하면 됩니다. 이때 해당 Notification을 구별할 수 있는 ID값도 함께 넘기는데, 이 ID로 해당 Activity에서 Notification을 해제할 수도 있습니다.

개략적인 사용법을 알았으니, 이제 Hangul to English App 적용해 보도록 하겠습니다.

public class MainActivity extends ListActivity {

    public static String CLASSNAME = MainActivity.class.getSimpleName();

    private static final int NOTIFY_ID = 3333;



클래스 내부에 NOTIFY_ID 상수를 지정합니다.

public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.main);

       

        .. 중략

        //Notify

        notifyMessage();

    }



그리고 Activity 시작 부분에 Notify를 호출하도록 메소드를 호출합니다.


/**

     * App 실행시시 알림영역에 아이콘 실행정보를 표시

     * 알림영역 선택시 어플 실행.

     * 알림영역 삭제는 어플 종료 이전에는 불가능 하도록 처리해야 .

     */

    private void notifyMessage() {

        String notiTitle = this.getString(R.string.app_name);

        String notiContent = this.getString(R.string.msg_notify_content);

        final NotificationManager notiMgr = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);

        Notification noti = new Notification(R.drawable.ic_notify, notiTitle , System.currentTimeMillis());

        noti.flags |= Notification.FLAG_NO_CLEAR;

       

        PendingIntent i = PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class),0);

       

        noti.setLatestEventInfo(this, notiTitle, notiContent, i);

       

        notiMgr.notify(NOTIFY_ID, noti);

    }

실제 Notification 객체 및 NotificationManager 객체를 생성하여 알림을 상태바에 표시합니다.
저희 Hangul to English 앱은 사용자가 앱의 프로세스를 죽이지 않는 한 알림메세지를 지우지 못하도록 하기 위해서 추가적으로 FLAG_NO_CLEAR 플래그를 설정하였습니다.

noti.flags
|= Notification.FLAG_NO_CLEAR;
이렇게 추가하시면 제가 생성한 Notification에 대해서는 제거 버튼이 보이지 않게 됩니다.

Notification 기능이 업그레이드 된 소스를 첨부합니다. 필요하신 분은 다운받으셔서 활용하시면 됩니다.


Trackback 1 Comment 0
2010.09.03 15:02

[강좌A13] 안드로이드 실전 개발 - Hangul2English 최종소스 공개 (한글 자모 분리, Manifest)




차주쯤 강좌를 쓸려고 했는데, 오늘 일이 한가한 바람에 시간이 남아서 마지막 소스 부분을 마무리 할까 합니다.
오늘의 강좌는 소스코드의 마지막 부분인 한타 -> 영타 변환 소스 부분과 메니페스트 부분에 대해서 설명하겠습니다. 오늘 강좌가 끝나면 전체 소스코드가 다 오픈되는 군요.

전체 강좌 목차

[강좌A01] Moteodev Studio를 이용한 안드로이드 개발 환경 구축 가이드
[강좌A02] 안드로이드 개발 참고 서적 소개
[강좌A03] Android 실전 개발 - 아이디어 / 기획 / Wireframe
[강좌A04] 안드로이드 실전 개발 - 아이콘 제작
[강좌A05] 안드로이드 실전 개발 - 레이아웃 및 리소스 : Part1
[강좌A06] 안드로이드 실전 개발 - 레이아웃 및 리소스 : Part2
[강좌A07] 안드로이드 실전 개발 - 리소스 해킹
[강좌A08] 안드로이드 실전 개발 - SQLite

[강좌A09] 안드로이드  실전 개발 - 데이터베이스 : Part1
[강좌A10] 안드로이드  실전 개발 - 데이터베이스 : Part2
[강좌A11] 안드로이드  실전 개발 - 데이터베이서 : Part3 (Motodev database)
[강좌A12] 안드로이드  실전 개발 - Main UI 소스, ListAdapter
[강좌A13] 안드로이드  실전 개발 - Hangul2English 최종 소스 공개 (한글 자모 분리, Manifest)


먼저, 한타를 영타로 변환하는 부분입니다.
저는 앞선 강좌에서 onTextChanged 이벤트를 잡아서 한글을 영문으로 변환 하도록 구성했습니다. 한글을 영문으로 변환하기 위해서는 여러 방법이 있겠지만, 저는 한글을 자소(초성/중성/종성) 분리하여 각 글자에 해당되는 영문을 찾아서 변환해 주는 방식을 사용했습니다.

먼저, 한글 자소 분리에 대한 기본 이론을 살펴보도록 하겠습니다. 이 이론을 잘 이해하시면 초성검색 구현도 별 어려움 없이 처리할 수 있습니다. 자소 분리나 초성검색은 이미 많으신 분들이 웹사이트에 구현코드를 많이 올려 놓으셨기 때문에 별도로 언급하지 않을까 하다가, 정리하는 차원에서 함께 설명합니다.

한글 유니코드 자소(초성,중성,종성) 분리 원리.

www.unicode.org/charts 에 들어가시면 East Asian Scripts 하위의 Hangul syllables 를 클릭하시면 한글 완성형 유니코드가 나옵니다. 첫번째 “가”는 유니코드가 0xAC00 이군요. 한글 완성형 코드는 0xAC00 ~ 0xD7A3까지의 코드 범위에 존재합니다.

유니코드에서 한글은 초성 19개, 중성 21개, 종성 28개의 모든 경우의 수에 따라 조합순으로 코드가 배열되어 있습니다.

초성  "ㄱ", "ㄲ", "ㄴ", "ㄷ", "ㄸ", "ㄹ", "ㅁ", "ㅂ", "ㅃ", "ㅅ", "ㅆ", "ㅇ", "ㅈ", "ㅉ", "ㅊ", "ㅋ", "ㅌ", "ㅍ", "ㅎ"

중성  "ㅏ", "ㅐ", "ㅑ", "ㅒ", "ㅓ", "ㅔ", "ㅕ", "ㅖ", "ㅗ", "ㅘ", "ㅙ", "ㅚ", "ㅛ", "ㅜ", "ㅝ", "ㅞ", "ㅟ", "ㅠ", "ㅡ", "ㅢ", "ㅣ"

종성  "", "ㄱ", "ㄲ", "ㄳ", "ㄴ", "ㄵ", "ㄶ", "ㄷ", "ㄹ", "ㄺ", "ㄻ", "ㄼ", "ㄽ", "ㄾ", "ㄿ", "ㅀ", "ㅁ", "ㅂ", "ㅄ", "ㅅ", "ㅆ", "ㅇ", "ㅈ", "ㅊ", "ㅋ", "ㅌ", "ㅍ", "ㅎ"

한글 유니코드를 초성,중성,종성으로 분리해 보면 다음과 같은 산식이 나옵니다.

0xAC00 + ( 초성순서 * 21 * 28 ) + ( 중성순서 * 28 ) + 종성순서 = 한글유니코드값.

다르게 표현하면

0xAC00 + ( ( 초성순서 * 21) + 중성순서 ) * 28 + 종성순서 = 한글유니코드값.

유니코드 표를 보시면 한글 “가”는 0xAC00 입니다. 즉, (초성 0번째 + 중성 0번째 + 종성 0번째) 로 구성되어 있습니다. 각 순서는 0번 인덱스부터 시작합니다.

“난” 이란 글자를 한번 분해해 보도록 하겠습니다.
한글 “난”은 초성 두번째(2) + 중성 0번째 + 종성 4번째로 구성되어 다음과 같은 산식이 성립됩니다.

“난” = ( 2[초성순서] * 21 * 28 ) + ( 0[중성순서] * 28 ) + ( 4[종성순서] ) + 0xAC00
    = 1180(십진수) + 0xAC00
    = 0x049C  + AC00
    = 0xB09C

즉 “난”이란 글자의 유니코드는 0xAC00 부터 1180번째 글자라 볼 수 있습니다.

이 수식을 기본으로 한글 유니코드를 초성/중성/종성으로 분리하도록 하겠습니다.

종성
종성은 한글유니코드값에서 0xAC00을 뺀 값에서 28로 나눈 나머지 값이 종성 순서 값이 됩니다.

종성 = (한글유니코드 – 0xAC00 ) % 28

“난”의 종성 “ㄴ”은 0xB09C – 0xAC00 = 1180(십진수) % 28 = 4
중성 배열 중 4번째 값인 “ㄴ”을 잦을수 있습니다.

중성

중성은 한글유니코드값에서 0xAC00을 빼고 거기서 다시 종성값을 뺀 후 28로 나눈 몫을 21로 나눈 나머지가 중성이 됩니다. 말로 하니 복잡하군요.

중성 = ( ( 한글유니코드 – 0xAC00 – 종성 ) / 28 ) % 21

“난” 의 중성 “ㅏ”는
“ㅏ” = ( ( 0xB09C – 0xAC00 – 4 ) / 28 ) % 21
    = ( 1176(십진수) / 28 ) % 21
    = 42 % 21 = 0
즉 중성 0번째 글자인 “ㅏ”로 분해됩니다.

초성

초성도 마찬가지로 산식을 이용하여 구할 수 있습니다.
초성 = ( ( ( 한글유니코드 – 0xAC00 – 종성 ) / 28 ) – 중성 ) ) / 21

“ㄴ”  = ( ( (0xB09C – 0xAC00 – 4 ) / 28 ) – 0 ) ) / 21
     = ( 1176(십진수) / 28 ) / 21
     = 42 / 21
     = 2
즉 초성 2번째 글자인 “ㄴ” 값을 구할 수 있습니다.

각 한글 자모 각각의 유니코드 값은 www.unicode.org/charts 에서 Hangul Compatibility Jamo 부분을 클릭하시면 pdf로 나오는데, 0x3131 ~ 0x318E 값의 범위를 가지고 있습니다.



이제 소스코드를 보겠습니다.
기본 자모 분석에 관한 소스는 웹에서 구한 소스입니다. 초성검색 때문에 많은 곳에 소스가 퍼져있고, 원 저작자 출처가 불분명해서 누가 최초로 만든 것이지 알 수는 없었습니다. 또한, 저도 소스를 사용한지 오래 되어 어느곳에서 가져다 왔는지 조차 기억이 안나는 군요. 가져온 소스에다가 제가 필요한 부분이 더 추가된 소스입니다.

//Hangul.java

package com.overoid.hangul2english.util;

 

public class Hangul {

   

                                //                                   

                                //                                   

                                //                                
                                //

final static char[] ChoSung   = { 0x3131, 0x3132, 0x3134, 0x3137, 0x3138, 0x3139,

                                  0x3141, 0x3142, 0x3143, 0x3145, 0x3146, 0x3147,

                                  0x3148, 0x3149, 0x314a, 0x314b, 0x314c, 0x314d,
                                  0x314e };

   

    final static String[] ChoSungEng = {"r", "R", "s", "e", "E", "f",

                                        "a", "q", "Q", "t", "T", "d",

                                        "w", "W", "c", "z", "x", "v", "g"};

 

                                   //                                   

                                   //                                 

                                   //                              

                                   //           

   final static char[] JwungSung = { 0x314f, 0x3150, 0x3151, 0x3152, 0x3153, 0x3154,

                                     0x3155, 0x3156, 0x3157, 0x3158, 0x3159, 0x315a,

                                     0x315b, 0x315c, 0x315d, 0x315e, 0x315f, 0x3160,

                                     0x3161, 0x3162, 0x3163 };

   

    final static String[] JwungSungEng = {"k", "o", "i", "O", "j", "p",

                                          "u", "P", "h", "hk", "ho", "hl",

                                          "y", "n", "nj", "np", "nl", "b",

                                          "m", "ml", "l"};

 

                                   //                                     

                                   //                             

                                   //                             

                                   //                                 

                                   //                   

   final static char[] JongSung  = { 0,      0x3131, 0x3132, 0x3133, 0x3134, 0x3135,

                                     0x3136, 0x3137, 0x3139, 0x313a, 0x313b, 0x313c,

                                     0x313d, 0x313e, 0x313f, 0x3140, 0x3141, 0x3142,

                                     0x3144, 0x3145, 0x3146, 0x3147, 0x3148, 0x314a,

                                     0x314b, 0x314c, 0x314d, 0x314e };

   

    final static String[] JongSungEng = {"", "r", "R", "rt", "s", "sw",

                                          "sg", "e", "f", "fr", "fa", "fq",

                                          "ft", "fx", "fv", "fg", "a", "q",

                                          "qt", "t", "T", "d", "w", "c",

                                          "z", "x", "v", "g"};

   

   

    public static String hangulToJaso(String s) {

       

        int a, b, c; // 자소 버퍼: 초성/중성/종성

        String result = "";

 

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

            char ch = s.charAt(i);

 

            if (ch >= 0xAC00 && ch <= 0xD7A3) { // "AC00:" ~ "D7A3:" 속한 글자면 분해

                c = ch - 0xAC00;

                a = c / (21 * 28);

                c = c % (21 * 28);

                b = c / 28;

                c = c % 28;

 

                result = result + ChoSung[a] + JwungSung[b];

                if (c != 0) result = result + JongSung[c] ; // c 0 아니면, 받침이 있으면

            } else {

                result = result + ch;

            }

        }

        return result;

    }

   

    /**

     * 한글기준의 문자열을 입력받아서 한글의 경우에는 영타기준으로 변경한다.

     * @param s 한글/영문/특수문자가 합쳐진 문자열

     * @return 영타기준으로 변경된 문자열값

     */

    public static String convertToEnglish(String s) {

        // *****************************************

        // 0xAC00 + ( (초성순서 * 21) + 중성순서 ) * 28 + 종성순서 = 한글유니코드값

        // ( (초성순서 * 21) + 중성순서 ) * 28 + 종성순서 = 순수한글코드

        // 순수한글코드 % 28 = 종성

        // ( (순수한글코드 - 종성) / 28 ) % 21 = 중성

        // ( ( ( 순수한글코드 - 종성) / 28) - 중성) ) / 21 = 초성

        // *******************************************

       

        int a, b, c; // 자소 버퍼: 초성/중성/종성

        String result = "";

 

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

            char ch = s.charAt(i);

 

            if (ch >= 0xAC00 && ch <= 0xD7A3) { // "AC00:" ~ "D7A3:" 속한 글자면 분해

                c = ch - 0xAC00;

                a = c / (21 * 28);

                c = c % (21 * 28);

                b = c / 28;

                c = c % 28;

 

                result = result + ChoSungEng[a] + JwungSungEng[b];

                if (c != 0) result = result + JongSungEng[c] ; // c 0 아니면, 받침이 있으면

            } else {

                result = result + ch;

            }

        }

       

        return result;

    }

   

    /*

     * 완성되지 않은 한글의 경우 영문 변환이 제대로 되지 않는다.

     * 잘못된 글자인 경우에도 영문으로 변환이 가능하도록 추가적으로 처리하는 함수

     * 글자가 초성, 중성, 종성을 구성하는 글자 배열을 루프돌면서 같은글자가 있는지

     * 확인한 해당 영문으로 변환함.

     */

    public static String convertToEnglishforSingleChar(String s) {

        String result = "";

        String temp = null;

       

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

            char ch = s.charAt(i);

           

            if(ch >= 0x3131 && ch <= 0x3163) {

                temp = findChoSung(ch);

                if(temp != null) {

                    result = result + temp;

                } else {

                    temp = findJwungSung(ch);

                    if(temp != null) {

                        result = result + temp;

                    } else {

                        temp = findJongSung(ch);

                        if(temp != null) {

                            result = result + temp;

                        } else {

                            result = result + ch;

                        }

                    }

                }

            } else {

                result = result + ch;

            }

           

        }

       

        return result;

    }

   

    private static String findChoSung(char c) {

        String result = null;

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

            if(ChoSung[i] == c) {

                result = ChoSungEng[i];

                break;

            }

        }

        return result;

    }

   

    private static String findJwungSung(char c) {

        String result = null;

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

            if(JwungSung[i] == c) {

                result = JwungSungEng[i];

                break;

            }

        }

        return result;

    }

   

    private static String findJongSung(char c) {

        String result = null;

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

            if(JongSung[i] == c) {

                result = JongSungEng[i];

                break;

            }

        }

        return result;

    }

}



위 한글 자모 분석 로직을 이해하셨으면 소스는 이해가 쉽게 되실겁니다.

 

hangulToJaso 메소드는 카피한 소스에 있던 메소드인데, 한글 자모를 분리해서 string으로 리턴해주는 메소드입니다. 저희 소스에는 불필요한 부분이지만 나중에 쓸일이 있을지도 몰라서 그냥 그대로 두었습니다.

 

convertToEnglish 메소드가 저희 앱에서 사용하는 메소입니다. 한글을 초성/중성/종성으로 분리하여 순서값에 해당되는 영문자 배열을 리턴하면 간단히 변환이 끝났니다. 하지만, 사용자가 패스워드용 한글을 정상적인 완성형 한글만 사용하는 아니라 하ㄱㅏㅏ라고 자모만을 이용해서 만들 수도 있으므로 convertToEnglish 메소드는 이런 자모만으로 구성된 비정상적인 글자를 영문으로 변환할 수가 없어서 부분은 처리를 위한 convertToEnglishforSingleChar() 메소드를 추가로 두었습니다. 한글 자모 배열을 뒤져서 매칭되는 자모를 찾고, 동일한 배열값의 영문자를 리턴하는 메소드입니다.

 

Hangul Class 유틸성이라서 인스턴스 없이 메소드 호출을 하기 위해 모두 static으로 구현했습니다.

 

이로써 저희 Hangul2English 소스가 완성되었습니다.

 

끝으로 메니페스트 파일을 보도록 하겠습니다.


//AndroidManifest.xml

<?xml version="1.0" encoding="UTF-8"?>

<manifest android:versionCode="1" android:versionName="1.0"

          package="com.overoid.hangul2english" xmlns:android="http://schemas.android.com/apk/res/android">

    <application android:icon="@drawable/icon"

                 android:label="@string/app_name"

                 android:debuggable="false">

        <activity android:label="@string/app_name"

                  android:name=".MainActivity"

                  android:theme="@android:style/Theme.NoTitleBar"

                  >

            <intent-filter>

                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>

            </intent-filter>

        </activity>

    </application>

    <uses-sdk android:minSdkVersion="5"/>

</manifest>



다른 부분은 별개 없고,
Acticity의 속성으로 android:theme="@android:style/Theme.NoTitleBar" 를 기술하시면 앱 실행시 상단에 나오는 회색 타이틀 바가 없어집니다. 별도로 Custom TitleBar를 만드는 방법도 있긴 하지만, 그냥 디폴트 타이틀 바를 위 처럼 없애고 Layout 작성시 타이틀 처럼 보이는 레이아웃을 추가하면 될 것 같습니다. 저희도 별도로 타이틀 바 비스무리한게 있어서 디폴트 타이틀바는 빼버렸습니다.

Hangul2English를 실행해 보셨는지 모르겠습니다만, Hangul2English는 가로 회전시에도 디자인이 별 문제없이 잘 돌아갑니다. 만일, 개발하시려는 앱이 특정 가로나 세로방향만 지원해야 한다면 메니페스트 파일에 android:screenOrientation="portrait"  로 기술하시면 세로방향만, landscape로 지정하시면 가로방향으로만 앱이 실행됩니다.

그리고 application entity에 설정되어 있는 android:debuggable="false" 속성은 어플리케이션 개발 및 테스트 완료 후에 배포용 패키지를 만드실 때  설정하시면 됩니다. 부가적으로 배포용 패키지 만드실 때는 로그 기능을 빼야 합니다. 런타임시에 불필요한 로그 코드가 나오는 것은 안 좋겠죠.

로그에 대해 간단히 알려드리면 로그는 ERROR, WARN, INFO, DEBUG, VERBOSE 이 다섯개 레벨로 나눌 수 있습니다. Verbose로 설정된 로그(Log.v)는 개발용 컴파일이 아닌 배포용 패키지를 만드실 때 컴파일 단계에서 빠져버립니다. 디버그 레벨 로그는 컴파일은 되지만 런타임시에 빠져서 절대 런타임시에 출력될 일은 없습니다. 나머지 에러나 warning, info 레벨 로그는 개발시나 런타임시나 출력이 됩니다. 아주 중요한 정보만 ERROR, WARN, INFO 레벨 로그로 남기시고, 가능한 개발시에는 Verbose 레벨을 사용하시면 런타임시 부하를 줄일 수 있습니다.

아. 정말 코드가 끝났습니다.
그동안 많은 도움이 되었는지 모르겠습니다.

완성된 소스코드 첨부합니다. 개발하시는데 많은 도움이 되셨으면 좋겠네요^^



앞으로 남은 안드로이드 실전강좌는 다 만든 앱을 시장에 배포하는거에 관련된 강좌가 남은 것 같습니다. 그럼, 즐거운 하루 되시기 바랍니다.

Trackback 1 Comment 6
  1. zzl986 2011.03.24 22:15 신고 address edit & del reply

    강의 해주신내용에. 감동이였구요!
    필요한 내용이든 그렇치않은.내용이든.고생하신. 모습에. 박수드립니다!
    아자..화이팅!

  2. 김영호 2011.04.18 13:43 신고 address edit & del reply

    강의 잘 보았습니다 ^^+

    얕은 지식에 많은 보탬이 되었습니다.

    감사합니다. __+

  3. BlueSkygogo 2011.06.25 17:54 신고 address edit & del reply

    정말 좋은 내용으로 가득하네요...!
    개발하는 데 큰 도움이 될것 같습니다.
    감사드립니다....^^

  4. 민짜 2011.10.06 13:24 신고 address edit & del reply

    폰에서 글자를 입력받아 실시간으로 서버로 전송하는 프로그램을 개발중입니다. 자음/모음 분리하는것때문에 좌절하고 있었는데 올려주신 Hangul소스를 바탕으로 해결했습니다.정말 감사합니다.

  5. 미래소년 2012.04.05 23:39 신고 address edit & del reply

    감사합니다.. 강의 정말 잘 봤습니다.. 무지한 저도 이해할 수 있을만큼 잘 설명해 주셨네요..
    그런데 제가 지금 만들고 싶은 어플은 영타를 한글로 바꿔주는 것입니다..
    이 강좌와 반대로 말이죠.. 의외로 한글을 초성중성종성으로 분해할 필요가 없어 쉬울 것 같은데..
    코드 좀 부탁드려도 될까요.. 머리에서 돌기는 하는데 사실 자신이 없어서요..

  6. 2012.07.05 16:36 address edit & del reply

    비밀댓글입니다

2010.09.01 15:21

[강좌A12] 안드로이드 실전 개발 - Main UI 소스, ListAdapter




이제 메인 UI – MainActivity 소스 부분입니다. 강좌의 소스 부분은 거의 막바지에 다다른 것 같습니다. 너무 쉬운 부분을 별다른 설명을 하지는 않겠습니다. 소스를 보시면 잘 아시리라 믿습니다.

전체 강좌 목차

[강좌A01] Moteodev Studio를 이용한 안드로이드 개발 환경 구축 가이드
[강좌A02] 안드로이드 개발 참고 서적 소개
[강좌A03] Android 실전 개발 - 아이디어 / 기획 / Wireframe
[강좌A04] 안드로이드 실전 개발 - 아이콘 제작
[강좌A05] 안드로이드 실전 개발 - 레이아웃 및 리소스 : Part1
[강좌A06] 안드로이드 실전 개발 - 레이아웃 및 리소스 : Part2
[강좌A07] 안드로이드 실전 개발 - 리소스 해킹
[강좌A08] 안드로이드 실전 개발 - SQLite

[강좌A09] 안드로이드  실전 개발 - 데이터베이스 : Part1
[강좌A10] 안드로이드  실전 개발 - 데이터베이스 : Part2
[강좌A11] 안드로이드  실전 개발 - 데이터베이서 : Part3 (Motodev database)
[강좌A12] 안드로이드  실전 개발 - Main UI 소스, ListAdapter

곧바로 코드를 보도록 하겠습니다. 메인 Activity Class 전체 코드입니다.

//MainActivity.java

package com.overoid.hangul2english;

 

 

 

import java.util.ArrayList;

import java.util.List;

 

 

 

import com.overoid.hangul2english.data.DataDao;

import com.overoid.hangul2english.data.DataDao.DataTo;

 

 

import android.app.AlertDialog;

import android.app.ListActivity;

import android.content.Context;

import android.content.DialogInterface;

import android.os.Bundle;

import android.text.ClipboardManager;

import android.text.Editable;

import android.text.TextUtils;

import android.text.TextWatcher;

import android.util.Log;

import android.view.LayoutInflater;

import android.view.View;

import android.view.ViewGroup;

import android.view.View.OnClickListener;

import android.widget.ArrayAdapter;

import android.widget.Button;

import android.widget.EditText;

import android.widget.ImageButton;

import android.widget.ImageView;

import android.widget.ListView;

import android.widget.TextView;

import android.widget.Toast;

 

public class MainActivity extends ListActivity {

    public static String CLASSNAME = MainActivity.class.getSimpleName();

   

    private DataDao dao;

    private DataListAdapter mListAdapter;

    private ArrayList<DataTo> mDatas;

   

    //UI관련 변수

    private EditText mKorText;

    private EditText mEngText;

    private Button mSaveButton;

    private ImageButton mRemoveButton;

    private ImageButton mCopyButton;

    private ImageView mInfoButton;

    private ListView mListView;

    private TextView mEmpty;

   

    @Override

    public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.main);

       

        // Dao Instance

        dao = new DataDao(getApplicationContext());

       

        // UI Init

        mKorText = (EditText)findViewById(R.id.text_password);

        mEngText = (EditText)findViewById(R.id.text_english);

        mSaveButton = (Button)findViewById(R.id.button_save);

        mRemoveButton= (ImageButton)findViewById(R.id.button_remove);

        mCopyButton = (ImageButton)findViewById(R.id.button_copy);

        mInfoButton = (ImageView)findViewById(R.id.button_info);

       

        //ListActivity 사용하면 Resource ListView id "@+id/android:list" 사용해야 .

        mListView = getListView(); 

        mListView.setItemsCanFocus(true);

        mListView.setEmptyView(mEmpty);

        mListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);

        mEmpty = (TextView)findViewById(R.id.list_empty);

       

        //Event Handler

       

        // Info Button Click

        mInfoButton.setOnClickListener(new OnClickListener() {

           

            @Override

            public void onClick(View v) {

                infoAlertDialogShow(MainActivity.this);

            }

 

        });

       

        // Hangul Text Change

        mKorText.addTextChangedListener(new TextWatcher() {

           

            @Override

            public void onTextChanged(CharSequence s, int start, int before, int count)  {

                Log.v("**", "TextChanged");

                if(!TextUtils.isEmpty(mKorText.getText())) {

                    //mEngText.setText(Hangul.convertToEnglishforSingleChar(Hangul.convertToEnglish(mKorText.getText().toString())));

                } else {

                    mEngText.setText("");

                }

               

            }

           

            @Override

            public void beforeTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) {

                // TODO Auto-generated method stub

               

            }

           

            @Override

            public void afterTextChanged(Editable arg0) {

                // TODO Auto-generated method stub

               

            }

        });

       

        // Remove Button Click

        mRemoveButton.setOnClickListener(new OnClickListener() {

           

            @Override

            public void onClick(View arg0) {

                mKorText.setText("");

                mEngText.setText("");

            }

        });

       

        // Copy Button Click

        mCopyButton.setOnClickListener(new OnClickListener() {

           

            @Override

            public void onClick(View v) {

                if(mEngText.getText().length() > 0) {

                    copyClipboard(mEngText.getText().toString());

                }

            }

        });

       

        // Save Button Click

        mSaveButton.setOnClickListener(new OnClickListener() {

           

            @Override

            public void onClick(View v) {

                if(mKorText.getText().length() > 0 && mEngText.getText().length() > 0) {

                   

                    DataTo to = new DataTo(0, mKorText.getText().toString(), mEngText.getText().toString());

                    Log.i(Constants.LOG_TAG,MainActivity.CLASSNAME + "insert:" + to.toString());

                    dao.insert(to);

                    Toast.makeText(MainActivity.this, R.string.msg_insert_success, Toast.LENGTH_SHORT).show();

   

                    //새로 바인딩해야 .   

                    populateList();

                } else {

                    Toast.makeText(MainActivity.this, R.string.msg_insert_no_data, Toast.LENGTH_SHORT).show();

                }

 

            }

        });

       

        //로딩시 데이터를 가져와 리스트에 뿌린다.

        populateList();

    }

   

    @Override

    protected void onPause() {

        Log.i(Constants.LOG_TAG,MainActivity.CLASSNAME + "- onPause()");

        super.onPause();

    }

 

    @Override

    protected void onResume() {

        Log.i(Constants.LOG_TAG,MainActivity.CLASSNAME + "- onResume()");

        super.onResume();

 

    }

   

   

   

    @Override

    protected void onDestroy() {

        dao.close();

        super.onDestroy();

    }

   

    /****

     * Clipboad 선택한 텍스트 복사.

     */

    private void copyClipboard(String s) {

        ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); 

        clipboard.setText(s);

        Toast.makeText(MainActivity.this, R.string.msg_clipboard_copy_success, Toast.LENGTH_SHORT).show();

    }

   

    /****

     * 정보창을 띄운다.

     * @param context

     */

    private void infoAlertDialogShow(Context context) {

        LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);

        View infoView = inflater.inflate(R.layout.info, null);

 

        String alertTitle = getResources().getString(R.string.app_name);

        String buttonMessage = getResources().getString(R.string.msg_info_close_button);

       

        new AlertDialog.Builder(context)

        .setTitle(alertTitle)

        .setView(infoView)

        .setNeutralButton(buttonMessage, new DialogInterface.OnClickListener() {

           

            public void onClick(DialogInterface dlg, int sumthin) {

                //기본적으로 창이 닫히며, 추가작업은 없다.

            }

        }).show();

    }

   

    /****

     * populateList     : table data 새로 가져와 화면에 뿌린다.

     */

    private void populateList() {

       

        Log.v(Constants.LOG_TAG, MainActivity.CLASSNAME + "- populateList");

       

        mDatas = (ArrayList<DataTo>)dao.get();

       

        if(mDatas == null) {

            mEmpty.setText("");

            setListAdapter(null);

        } else {

            mListAdapter = new DataListAdapter(MainActivity.this,R.layout.list_item, mDatas);

            setListAdapter(mListAdapter);

            Log.v(Constants.LOG_TAG, MainActivity.CLASSNAME + "- populateList, mListView Binding End");

           

        }

    }

   

    class DataListAdapter extends ArrayAdapter<DataTo> {

        private static final String CLASS = "DataListAdapter";

        private Context mContext;

        private List<DataTo> mDataList;

       

        public DataListAdapter(Context context, int textViewResourceId, List<DataTo> items) {

            super(context, textViewResourceId, items);

            mContext = context;

            mDataList = items;

        }

 

        @Override

        public View getView(int position, View convertView, ViewGroup parent) {

            View row = convertView;

           

            if(row == null) {

                LayoutInflater inflator = (LayoutInflater)mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

                row = inflator.inflate(R.layout.list_item, null);

            }

           

            final DataTo to = mDataList.get(position);

            Log.i(Constants.LOG_TAG, CLASS + " getView:pos" + position + "data:" + to.toString());

            Log.i(Constants.LOG_TAG, CLASS + " korText:" + to.getKorText().toString() + ",engText:" + to.getEngText().toString());

           

            if(to != null) {

                TextView korText = (TextView)row.findViewById(R.id.korText);

                korText.setText(to.getKorText());

               

                TextView engText = (TextView)row.findViewById(R.id.engText);

                engText.setText(to.getEngText());

               

                ImageView copyClipboadButton = (ImageView)row.findViewById(R.id.copyClipboard);

                copyClipboadButton.setOnClickListener(new OnClickListener() {

                   

                    @Override

                    public void onClick(View v) {

                        copyClipboard(to.getEngText().toString());

                    }

                });

               

                ImageView deleteButton = (ImageView)row.findViewById(R.id.deleteImage);

                deleteButton.setOnClickListener(new OnClickListener() {

                   

                    @Override

                    public void onClick(View v) {

                       

                        Log.i(Constants.LOG_TAG, CLASS + " delete:" + to.toString());

                        dao.delete(to.getId());

                        Toast.makeText(mContext, R.string.msg_delete_success, Toast.LENGTH_SHORT).show();

                       

                        //새로 바인딩해야 .   

                        populateList();

                       

                    }

                });

            }

            return (row);

        }

 

    }

 


먼저 MainActivity는 일반적인 Activity 클래스에서 상속받지 않고 Activity의 하위 클래스인 ListActivity에서 상속을 받았습니다. UI에 List가 있을 경우 ListActivity에서 상속받으면 좀 더 편리합니다.

onCreate() 메소드는 액티비티가 처음 실행될 때 호출되는 메소드로 안드로이드 내부의 액티비티 초기화 작업을 진행할 수 있게 반드시 가장 먼저 해당 이벤트를 상위 클래스에 전달해야 합니다.
super.onCreate(savedInstanceState);

그 이후에 각 UI Component들을 findViewById() 메소드를 통해 이미 만들어진 인스턴스에 대한 레퍼런스를 가져옵니다.

리스트뷰의 인스턴스의 경우 ListActivity 클래스에서 제공하는 getListView() 메소드를 통해서 가져옵니다. mListView = getListView();

각 UI Component 인스턴스 생성 후에 각각의 View에 Event Handler를 할당합니다.

Info button click시에는 프로그램 정보를 볼 수 있는 창을 띄우는데…


private void infoAlertDialogShow(Context context) {

        LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);

        View infoView = inflater.inflate(R.layout.info, null);

 

        String alertTitle = getResources().getString(R.string.app_name);

        String buttonMessage = getResources().getString(R.string.msg_info_close_button);

       

        new AlertDialog.Builder(context)

        .setTitle(alertTitle)

        .setView(infoView)

        .setNeutralButton(buttonMessage, new DialogInterface.OnClickListener() {

           

            public void onClick(DialogInterface dlg, int sumthin) {

                //기본적으로 창이 닫히며, 추가작업은 없다.

            }

        }).show();

    }

AlertDialog에서 저희가 만든 View를 Add 하기 위해 inflater.inflate() 메서드로 별도로 만든 레이아웃 파일을 동적으로 로드 한 후 AlertDialog Buider에 add 합니다.

한글 입력창에 한글이 입력되면 자동으로 영타로 변환해서 영문창에 나타내어야 합니다. 구현방법은 여러가지 있겠지만, 저는 TextWatcher 를 사용해서 onTextChanged 이벤트에서 한글을 자소 분리하여 영문으로 변환하도록 구현했습니다. 한글 자소분리에 대해서는 다음번 포스트에서 상세히 알려드리도록 하겠습니다.

copyClipboard() 메소드는 입력된 문자열을 클립보드에 복사하는 메소드입니다.
ClipboardManager 인스턴스를 getSystemService() 메서드를 통해 얻은 후 문자열을 setText로 지정하면 클립보드에 복사가 됩니다.

저장버튼 클릭시에는 데이터를 담을 DataTo 객체를 생성 후 한글/영문을 저장하고 DataDao 인스턴스의 insert의 인자로 넘겨주면 DB에 저장이 됩니다.

그리고 각각의 중요한 처리마다 Toast 클래스를 이용하여 메시지를 표현했습니다.

Toast 사용법은 단순합니다. makeText() 메소드에 context와 문자열 및 표시될 시간 상수를 넘겨주면 됩니다. 물론 Toast도 좀 근사하게 만들 수 있습니다. Toast 클래스를 new 키워드로 직접 생성한 후 setView 메서드를 사용해서 원하는 뷰를 지정하시면 됩니다. 그리고  setDuration() 메소드로 화면에 표시될 시간을 지정한 후 show() 메서드를 호출하면 화면에 나타납니다.

다음은 ListAdapter 부분입니다. 위 코드는 Custom List를 만드는 전형적인 코드 패턴입니다.
즉, Adapter 클래스를 상속받아 우리 입맞에 맞게 클래스를 만든 후 getView() 메소드를 오버라이드 해서 처리하는 방식입니다. 저는 ArrayAdapter에서 상속을 받았습니다. 리스트에 뿌려야 될 내용이 DB에서 받아온 ArrayList이므로 ArrayAdpater와는 아주 궁합이 잘 맞습니다.

DataListAdapter 클래스를 MainActivity의 중첩클래스로 만들었습니다. 내부 클래스로 만들면 MainActivity의 자원을 맘껏 가져다 쓸 수 있으므로 편리합니다. DataListAdapter 클래스 생성자에 Context와 DB에서 조회한 ArrayList 를 인자로 넘겨줍니다. getView() 메소드에서는 리스트의 한 항목 UI 레이아웃을 우리가 별도로 제작한 R.layout.list_item 을 inflate 헤서 메모리에 로드한 후 ArrayList에 담긴 내용으로 값을 설정합니다. Inflation이란 XML 레이아웃에 정의된 내용을 분석해 view 객체의 트리 구조를 만들어 내는 작업을 말합니다. 실제 동적으로 특정 XML 레이아웃을 로딩할 때 사용하시면 됩니다.

getView() 내에서 각종 UI View에 이벤트를 할당 할 수도 있습니다. 위 코드는 삭제 버튼과 클립보드 복사 이미지에 클릭 이벤트를 할당하여 삭제하거나 클립보드에 복사하도록 한 소스입니다.

getView()내에서 convertView의 값을 체크하여 null 인 경우에만 inflate 하도록 코드가 작성되어 있습니다. 일반적으로 성능개선을 위해서 위와 같이 사용하기도 하며, 더 나은 성능을 위해서 findViewById() 메서드의 호출을 줄이기 위한 Holder 패턴을 사용하기도 합니다. 홀더 패턴을 간단히 말하면 각 View마다 setTag()와 getTag() 메서드를 사용하며 나중에 사용할 내부 위젯을 캐시해 두는 기법으로 각 행의 View에 필요한 정보를 담은 객체를 태그로 저장해 두고, 사용할 때 getTag()로 꺼내어 findViewById() 메서드의 호출을 최소화 하는 기법입니다. 보다 상세한 사항을 알고 싶으시면 구글 문서나 책들을 보시면 될 것 같습니다.

대략적으로 MainActivity 소스에 대해서 살펴보았습니다.

이제 코드에서 남은 부분은 한타를 영타로 변환하는 한글유틸 클래스만 작성되면 Hangul2English 앱은 개발이 완료 될 것 같습니다.

이번 강좌까지 개발된 소스를 첨부합니다. 필요하신 분은 다운받으셔서 사용하시기 바랍니다.


그럼, 다음 강좌는 일주일쯤 후에 ..

Trackback 4 Comment 1
  1. 뉴이 2011.04.01 19:08 신고 address edit & del reply

    정말 감사합니다^^

2010.09.01 12:53

[강좌A10] 안드로이드 실전 개발 - 데이터베이스 : Part2




안드로이드 실전 개발 데이터베이스편 파트2 입니다. 전편에 이어 바로 시작하도록 하겠습니다.

전체 강좌 목차

[강좌A01] Moteodev Studio를 이용한 안드로이드 개발 환경 구축 가이드
[강좌A02] 안드로이드 개발 참고 서적 소개
[강좌A03] Android 실전 개발 - 아이디어 / 기획 / Wireframe
[강좌A04] 안드로이드 실전 개발 - 아이콘 제작
[강좌A05] 안드로이드 실전 개발 - 레이아웃 및 리소스 : Part1
[강좌A06] 안드로이드 실전 개발 - 레이아웃 및 리소스 : Part2
[강좌A07] 안드로이드 실전 개발 - 리소스 해킹
[강좌A08] 안드로이드 실전 개발 - SQLite

[강좌A09] 안드로이드  실전 개발 - 데이터베이스 : Part1
[강좌A10] 안드로이드  실전 개발 - 데이터베이스 : Part2

다음으로 살펴볼 클래스는 General DatabaseHelper Class 입니다. 이 클래서는 SQLiteOpenHelper를 상속받아서 Database 생성 및 업그레이드, 연결 등의 작업을 담당하고 있으며, 외부 파일에서 SQLiteDatabase instance를 직접 핸들링 하지 않게 하기 위해서 database C/R/U/D 작업을 Wrapping 하고 있습니다. 또한 여러 Activity에서 DB Connection 관련 문제를 해결하기 위해서 Singleton으로 만들었습니다.

//DatabaseHelper.java


package com.overoid.hangul2english.data;

 

import com.overoid.hangul2english.Constants;

import com.overoid.hangul2english.data.H2eDatabaseCreator;

 

import android.content.ContentValues;

import android.content.Context;

import android.database.Cursor;

import android.database.SQLException;

import android.database.sqlite.SQLiteDatabase;

import android.database.sqlite.SQLiteDatabase.CursorFactory;

import android.database.sqlite.SQLiteException;

import android.database.sqlite.SQLiteOpenHelper;

import android.util.Log;

 

public class DatabaseHelper extends SQLiteOpenHelper {

 

    private static final String CLASSNAME = DatabaseHelper.class.getSimpleName();

    private static final String KEY_COLUMN = "_id";

   

    private static DatabaseHelper mInstance;

    private static SQLiteDatabase db; 

   

    /***

     * 생성자

     *

     * @param context   : app context

     * @param name      : database name

     * @param factory   : cursor Factory

     * @param version   : DB version

     */

    private DatabaseHelper(Context context, String name, CursorFactory factory, int version) { 

        super(context, name, factory, version); 

        Log.v(Constants.LOG_TAG,  DatabaseHelper.CLASSNAME + "Create or Open database : "+name);

    }

   

    /***

     * 생성자

     *

     * @param context   : app context

     */

    private DatabaseHelper(final Context context) {

        super(context, DatabaseCreator.DB_NAME , null, DatabaseCreator.DB_VERSION);

        Log.v(Constants.LOG_TAG,  DatabaseHelper.CLASSNAME + "Create or Open database : "+ DatabaseCreator.DB_NAME);

    }

   

    /***

     * Initialize method

     *

     * @param context       : application context

     */

    private static void initialize(Context context) { 

        if(mInstance == null) { 

 

            Log.i(Constants.LOG_TAG, DatabaseHelper.CLASSNAME + "Try to create instance of database (" + DatabaseCreator.DB_NAME + ")");

            mInstance = new DatabaseHelper(context);

           

            try {              

                Log.i(Constants.LOG_TAG, "Creating or opening the database ( " + DatabaseCreator.DB_NAME + " ).");               

                db = mInstance.getWritableDatabase();          

            } catch (SQLiteException se) {               

                Log.e(Constants.LOG_TAG, "Cound not create and/or open the database ( " + DatabaseCreator.DB_NAME + " ) that will be used for reading and writing.", se);      

            }

            Log.i(Constants.LOG_TAG,  DatabaseHelper.CLASSNAME + "instance of database (" + DatabaseCreator.DB_NAME + ") created !");

        } 

    }

   

    /***

     * Static method for getting singleton instance

     *

     * @param context       : application context

     * @return              : singleton instance

     */

    public static final DatabaseHelper getInstance(Context context) { 

        initialize(context); 

        return mInstance; 

    } 

   

    /***

     * Method to close database & instance null

     */

    public void close() {       

        if(mInstance != null) {           

            Log.i(Constants.LOG_TAG, DatabaseHelper.CLASSNAME + "Closing the database [ " + DatabaseCreator.DB_NAME + " ].");           

            db.close();           

            mInstance = null;       

        }   

    }

   

    /***

     * Method for select table

     * db.query wrapper 

     * @param table     : table name

     * @param columns   : column name array

     * @return          : cursor

     */

    public Cursor get(String table, String[] columns){       

        return db.query(table, columns, null, null, null, null, null);   

    }       

   

    /***

     * Method for select table

     * @param table     : table name

     * @param columns   : column name array

     * @param id        : record id (pk 컬러명은 "_id" 가능함)

     * @return          : cursor

     */

    public Cursor get(String table, String[] columns, long id){       

        Cursor cursor = db.query(true, table, columns, KEY_COLUMN + "=" + id, null, null, null, null, null);       

        if (cursor != null) {          

            cursor.moveToFirst();       

        }       

        return cursor;   

    }

   

    /****

     * Method for select statements

     * @param sql       : sql statements

     * @return          : cursor

     */

    public Cursor get(String sql) {

        return db.rawQuery(sql, null);

    }

   

    /***

     * Method to insert record

     * @param table     : table name

     * @param values    : ContentValues instance

     * @return          : long (rowid)

     */

    public long insert(String table, ContentValues values) {

        return db.insert(table, null, values);

    }

   

    /***

     * Method to update record

     * @param table     : table name

     * @param values    : ContentValues instance

     * @param id        : record id

     * @return          : int

     */

    public int update(String table, ContentValues values, long id) {       

        return db.update(table, values, KEY_COLUMN + "=" + id, null);   

    }

   

    /***

     * Method to update record

     * @param table         : table name

     * @param values        : ContentValues instance

     * @param whereClause   : Where Clause

     * @return              ; int

     */

    public int update(String table, ContentValues values, String whereClause) {

        return db.update(table, values, whereClause, null);

    }

   

    /***

     * Method to delete record

     * @param table         : table name

     * @param whereClause   : Where Clause

     * @return              : int

     */

    public int delete(String table, String whereClause) {

        return db.delete(table, whereClause, null);

    }

   

    /***

     * Method to delete record

     * @param table         : table name

     * @param id            : record id

     * @return              : int

     */

    public int delete(String table, long id) {

        return db.delete(table, KEY_COLUMN + "=" + id, null);

    }

   

    /***

     * Method to run sql

     * @param sql

     */

    public void exec(String sql) {

        db.execSQL(sql);

    }

   

    /****

     * logCursorInfo    : Cursor 리턴받는 Result 로깅하는 메소드

     * @param c

     */

    public void logCursorInfo(Cursor c) {

        Log.i(Constants.LOG_TAG, "*** Cursor Begin *** " + "Results:" +

                c.getCount() + " Colmns: " + c.getColumnCount());

       

        // Column Name print

        String rowHeaders = "|| ";

        for(int i=0; i<c.getColumnCount(); i++) {

            rowHeaders = rowHeaders.concat(c.getColumnName(i) + " || ");

        }

       

        Log.i(Constants.LOG_TAG, "COLUMNS " + rowHeaders);

        // Record Print

        c.moveToFirst();

        while(c.isAfterLast() == false) {

            String rowResults = "|| ";

            for(int i=0; i < c.getColumnCount(); i++) {

                rowResults = rowResults.concat(c.getString(i) + " || ");

            }

           

            Log.i(Constants.LOG_TAG, "Row " + c.getPosition() + ": " + rowResults);

           

            c.moveToNext();

        }

        Log.i(Constants.LOG_TAG, "*** Cursor End ***");

    }

   

    @Override

    /***

     * Method to create database

     * 데이터베이스 생성. 최초 한번만 실행됨.

     * @param db        :SQLiteDatabase instance

     */

    public void onCreate(SQLiteDatabase db) {

        DatabaseCreator mCreator = new H2eDatabaseCreator();

        String[] tableCreateStmt = mCreator.getCreateTablesStmt();

        String[] indexCreateStmt = mCreator.getCreateIndexStmt();

        String[] initDataDml = mCreator.getInitDataInsertStmt();

       

        try {

            if(tableCreateStmt != null && tableCreateStmt.length > 0) {

                Log.v(Constants.LOG_TAG, DatabaseHelper.CLASSNAME + " - onCreate() : Table Creation");

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

                    db.execSQL(tableCreateStmt[i]);

                }

            }

           

            if(indexCreateStmt != null && indexCreateStmt.length > 0) {

                Log.v(Constants.LOG_TAG, DatabaseHelper.CLASSNAME + " - onCreate() : Index Creation");

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

                    db.execSQL(indexCreateStmt[i]);

                }

            }              

           

            if(initDataDml != null && initDataDml.length > 0) {

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

                    Log.v(Constants.LOG_TAG, DatabaseHelper.CLASSNAME + " - onCreate() : Data Load" + initDataDml[i]);

                    db.execSQL(initDataDml[i]);

                }

                Log.v(Constants.LOG_TAG, DatabaseHelper.CLASSNAME + " - onCreate() : Init Data Load");

            }

           

        } catch(SQLException e) {

            Log.e(Constants.LOG_TAG, DatabaseHelper.CLASSNAME + " - onCreate() : Table Creation Error", e);

        }

 

    }

 

    @Override

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

        Log.v(Constants.LOG_TAG, DatabaseHelper.CLASSNAME + " - onUpgrade() : Table Upgrade Action");

 

    }

 

}

 


소스는 보시면 내용은 많으나 찬찬히 보시면 크게 복잡하거나 어려운 부분은 없는 것 같습니다.
SQLiteOpenHelper에서 상속을 받아 구현한 DatabaseHelper 클래스는 onCreate(), onUpgrade() 같은 SQLiteOpenHelper 콜백 메소드를 구현하고 있습니다. 데이터베이스가 Open 될 때 데이터베이스가 존재하지 않으면 onCreate() 메소드 부분이 실행됩니다. onCreate()내에서는 H2eDatabaseCreator Class에서 정의한 DDDL문을 가져와 실행합니다.
onUpgrade() 메소드는 현재 설치된 DB버전과 코드의 버전을 비교해서 서로 다르면 실행되는 메소드입니다. 저희는 초기 버전이라 별 다른 액션없이 로그만 기록했습니다.

DatabaseHelper의 private 생성자 및 getInstance() 메소드는 싱글톤 패턴의 전형적인 모습입니다.
멀티스레드 환경에서는 싱글톤을 만들 때 synchronized 키워드로 동기화를 하긴 하지만, 스마트폰 앱 환경에서는 그럴 필요가 없어서 부하를 주는 synchronized 키워드 없이 심플하게 구성하였습니다.

싱글톤 관련 메서드 하위의 메서드들은 SQLiteDatabase의 인스턴스가 직접 DB에 레코드 추가,삭제,수정,조회 등의 메서드를 코드 외부에서 SQLiteDatabase의 인스턴스를 직접 핸들링하지 않도록 캡슐화(일종의 wrapper method) 하고 있습니다.
.
logCursorInfo() 메소드는 Cursor로 리턴받은 Result를 log에 기록하는 유틸성 메소드 입니다.
우리코드에서 별 사용할 일은 없겠지만, Contents Provider에서 제공하는 기능을 이용하여 개발할 때, 디버깅시에 편리하게 사용할 수 있습니다.

이제 우리 App의 데이터를 핸들링하는 Dao 클래스를 보겠습니다.

//DataDao.java

package com.overoid.hangul2english.data;

 

import java.util.ArrayList;

import java.util.Iterator;

import java.util.List;

 

 

import com.overoid.hangul2english.Constants;

import com.overoid.hangul2english.data.H2eDatabase.DataTable;

 

import android.content.ContentValues;

import android.content.Context;

import android.database.Cursor;

import android.database.SQLException;

import android.util.Log;

 

 

 

public class DataDao {

    private static final String CLASSNAME = DataDao.class.getSimpleName();

    private DatabaseHelper db;

   

    public DataDao(Context context) {

        db = DatabaseHelper.getInstance(context);

       

    }

   

    public void close() {

        db.close();

    }

    /***

     * Inner Class - TO Objects

     * @author jinook.lee

     *

     */

    public static class DataTo {

        private int id;

        private String korText;

        private String engText;

       

        public DataTo() {}

 

        public DataTo(int id, String korText, String engText) {

            this.id = id;

            this.korText = korText;

            this.engText = engText;

        }

 

        @Override

        public String toString() {

            return "DataTo [id=" + String.valueOf(id) + ", korText=" + korText + ", engText=" + engText + "]";

        }

 

        public int getId() {

            return id;

        }

 

        public void setId(int id) {

            this.id = id;

        }

 

        public String getKorText() {

            return korText;

        }

 

        public void setKorText(String korText) {

            this.korText = korText;

        }

 

        public String getEngText() {

            return engText;

        }

 

        public void setEngText(String engText) {

            this.engText = engText;

        }

    }

 

    /****

     * insert       : table 추가하기

     * @param to    : DataTo object

     */

    public void insert(final DataTo to) {

        ContentValues values = new ContentValues();

 

        values.put(DataTable.COLUMN_KOR_TEXT, to.getKorText());

        values.put(DataTable.COLUMN_ENG_TEXT, to.getEngText());

 

        Log.v(Constants.LOG_TAG, DataDao.CLASSNAME + " insert - korText:" + to.getKorText());

        long rowId = db.insert(DataTable.TABLE_NAME, values);

        if(rowId < 0) {

            throw new SQLException("Fail At Insert");

        }

    }

  

    /****

     * update       : UI 사용될 일은 없을듯.

     * @param to    : DataTo object

     */

    public void update(final DataTo to) {

       ContentValues values = new ContentValues();

 

       values.put(DataTable.COLUMN_KOR_TEXT, to.getKorText());

       values.put(DataTable.COLUMN_ENG_TEXT, to.getEngText());

      

       Log.v(Constants.LOG_TAG, DataDao.CLASSNAME + " update - _id:" + String.valueOf(to.getId()));

       db.update(DataTable.TABLE_NAME, values, to.getId());

      

   }

   

    /****

     * delete       : record delete

     * @param id    : record id

     */

    public void delete(final int id) {

        Log.v(Constants.LOG_TAG, DataDao.CLASSNAME + " delete - _id:" + String.valueOf(id));

        db.delete(DataTable.TABLE_NAME, id);

    }

   

    /****

     * get          : select * from table

     * Cursor ArrayList 담아서 리턴한다.

     *

     * @return      : ArrayList DataTo

     */

    public List<DataTo> get() {

        Cursor c = null;

        ArrayList<DataTo> ret = null;

        

        String sql = "SELECT * FROM " + DataTable.TABLE_NAME + " ORDER BY 1";

       

        try {

            Log.v(Constants.LOG_TAG, DataDao.CLASSNAME + " get - All");

            c = db.get(sql);

           

            //db.logCursorInfo(c);

           

            ret = setBindCursor(c);

        } catch (SQLException e) {

            Log.e(Constants.LOG_TAG, DataDao.CLASSNAME + " getList ", e);

        } finally {

            if (c != null && !c.isClosed()) {

                c.close();

            }

        }

       

        return ret;

    }

   

   

    /****

     * SQLite Result Cursor 데이터를 Array List 넣고 리턴하는 메서드.

     * @param c     : cursor

     * @return      : ArrayList<DataTo>

     */

    private ArrayList<DataTo> setBindCursor(final Cursor c) {

        ArrayList<DataTo> ret = new ArrayList<DataTo>();

       

        int numRows = c.getCount();

       

        c.moveToFirst();

       

        // SQL문에서 Join 사용시 테이블명. 사용하면 컬럼명이 틀려지므로 getColumnIndex

        // Exception 낸다. 반드시 alias 사용해 컬럼명을 동일하게 맞춰야 한다.

        // 값이 null 경우 getInt() 0 반환할까? - 반환함.

       

        for(int i=0; i < numRows; i++) {

            DataTo to = new DataTo();

            to.setId(c.getInt(c.getColumnIndex(DataTable.COLUMN_ID)));

            to.setKorText(c.getString(c.getColumnIndex(DataTable.COLUMN_KOR_TEXT)));

            to.setEngText(c.getString(c.getColumnIndex(DataTable.COLUMN_ENG_TEXT)));

 

            ret.add(to);

            c.moveToNext();

        }

       

        return ret;

    }

   

    /****

     * List<DataTo> 내용을 로깅하는 메소드

     * @param to

     */

    public void logListInfo(List<DataTo> to) {

        Log.i(Constants.LOG_TAG,"*** List Begin *** " + "Results:" + to.size());

       

        Iterator<DataTo> itr = to.iterator();

        while (itr.hasNext()) {

            String msg = ((DataTo)itr.next()).toString();

            Log.i(Constants.LOG_TAG, "DATAS: " + msg);

        }

        Log.i(Constants.LOG_TAG,"*** List End ***");

    }

}

 



DataDao는 Activity에서 이용하는 클래스입니다.
테이블마다 혹은 비즈니스 단위마다 하나씩 Dao클래스를 만들어 사용하면 됩니다. 클래스명은 <테이블식별자> + “Dao”로 이루어져 있습니다.
역시 소스는 크게 어려운 코드는 없습니다.

DataDao 클래스는 내부에 중첩클래스로 Transfer Object로 사용할 클래스(Bean)를 담고 있습니다. TO 클래스는 Dao 클래스의 메소드 인자로 사용될 클래스입니다. <테이블 식별자> + “To”로 클래스명이 만들어 졌으며, 테이블의 컬럼에 해당되는 필드와 getter/setter로 이루어져 있습니다.

나머지 DataDao 클래스 소스 부분은 생성자에서 DatabaseHelper 클래스의 인스턴스를 얻어서 멤버변수에 저장하는 로직과, 실제 데이터 핸들링을 위한 get/insert/update/delete 메소드들이 존재합니다. 저희 App에서는 Cursor를 직접 핸들링 하지 않고 자바 개발자들이 많이 사용하는 ArrayList에 결과를 담아서 처리하도록 하겠습니다. 이렇게 작성하면 UI용 Adapter 작성시에도 ArrayAdapter를 사용할 수 있습니다. 실은 제가 익숙해서 사용하는 것입니다. 직접 작성하실 때는 Cursor를 직접 리턴하도록 코드를 작성하셔도 무방합니다.

setBindCursor() 메소드에서는 커서를 ArrayList로 변환하는 기능을 담당하고 있습니다.

logListInfo() 메소드는 ArrayList에 담긴 데이터를 log에 기록해주는 util method 입니다. 디버깅할 때 편리하게 사용할 수 있습니다.

Insert/update 시에는 ContentValues 객체를 사용했습니다. ContentValues 객체는 처리할 컬럼의 이름과 값의 쌍을 담는 객체입니다. 필요한 자료를 ContentValues 객체에 설정한 후 insert/update DatabaseHelper 인스턴스의 insert/update 메소드의 인자로 넘겨주면 됩니다.

위의 DataDao 및 DatabaseHelper에서는 단순한 쿼리에 대해서만 처리가 가능하도록 클래스가 구성되었습니다. 복잡한 쿼리는 SQLiteQueryBuilder Class를 이용해서 구성은 가능합니다만, 저는 그냥 Raw SQL문을 직접 실행하는 것이 쿼리 작성도 편리하고 좋은 것 같아서 SQL문을 실행할 수 있는 메소드만(get(sql), exec(sql) DatabaseHelper에 메소드로 제공하고 있습니다.

이로서 강좌에서 개발하는 Hangul2English App의 DB 부분 소스를 모두 살펴보았습니다.

이 DB 관련 클래스 사용법에 대해서 정리하자면,

H2eDatabase.java 
개발하려는 App의 테이블 구조를 static inner class로 정의(변경) 하시고 클래스명 및 파일명을 변경하시면 됩니다.

DatabaseCreator.java
인터페이스이므로 그대로 소스 복사하시면 됩니다. 패키지 구조만 수정하시면 될 것 같네요.

H2eDatabaseCreator.java
개발하려는 App의 DDL문을 상수로 정의하시고, 관련 메소드내에 포함시키시면 됩니다.

DatabaseHelper.java 
수정할 내용은 거의 없으며, onCreate() 메소드 내에서 인스턴스를 생성하는 클래스명 부분(DatabaseCreator mCreator = new H2eDatabaseCreator();) 만 자신의 DatabaseCreator클래스명으로 수정하시면 됩니다.

DataDao.java 
이 부분은 실제 안쪽 코드를 자신의 앱에 맞게 모두 수정하셔야 합니다.

처음에 제가 계획했던, Copy & Paste로 최소 노력으로 다른 App 개발에 적용하려는 목적은 대략 달성한 것 같습니다. 물론 별도의 library로 만들 수도 있지만, 그러려면 좀 더 다양한 케이스를 지원하도록 많은 코드도 추가되어야 괜챦은 library가 나올 것 같아서.. 그 부분은 차차 해보도록 하겠습니다.

이제 이 DB코드를 이용하는 UI 코드 부분을 살펴 보도록 하겠습니다.

UI코드를 살펴보기 전에 MotoDev의 Database 툴 기능을 편하게 리뷰하고 가도록 하겠습니다.
이번에 포스트 쓰면서 사용해본 기능인데, 소스 코드 Generation 기능이 상당히 맘에 들어 소개합니다.
그럼, Part 3에서 뵙겠습니다.


Trackback 4 Comment 6
  1. 이유식 2010.09.05 14:52 신고 address edit & del reply

    많은걸 배우고 갑니다.

  2. 선지헌 2010.09.29 20:12 신고 address edit & del reply

    많이 배우고 있습니다. 좋은 강좌 감사합니다. 질문이 있어서 글 남기는데요 DatabaseHelper 클래스를 싱글턴으로 만드셨는데요 여러 Activity에서 DB에 접근하는 문제때문이라고 하셨는데 조금 더 자세한 설명을 부탁드리고 싶습니다. 현재 제가 ContentProvider를 이용해 DB접근을 하는 앱을 만들고 있는데 이럴경우에도 싱글턴을 써야 하는 것인지요?

    • 보고픈 2010.09.30 15:48 신고 address edit & del

      제가 직접 해보지는 않아서 정확히는 모르겠으나, ContentProvider로 구현할때는 크게 문제가 되지는 않을듯 싶긴 합니다. 기존 구글쪽 소스를 보더라도 큰 문제는 안되는것 같습니다.

      여러 Activity에서 db 사용할때는 다른 화면에서 db 사용할때 이전 화면에서 db를 close 하지 않으면 에러가 발생합니다.
      싱글톤을 사용하지 않더라도 db를 close() 후 새 activity에서 open()하면 문제가 되지 않습니다. 저는 왠지 그 작업이 부하가 걸릴듯 싶어 싱글톤으로 구현한 것입니다.

  3. 리칼 2010.12.10 17:48 신고 address edit & del reply

    정말 볼수록 감탄합니다.. 감사합니다..

  4. jordan retro 12 2012.03.05 17:58 신고 address edit & del reply

    총괄 은 미국 의 인기 는 패션 여성복 브랜드 나인 의 창시자 토 리 버 치 (총괄) 여사.

  5. 맹순이 2014.10.28 00:34 신고 address edit & del reply

    좋은 코드 리뷰 잘 했습니다. 한 가지 궁금한 점이 있습니다. close 부분에서 helper 인스턴스 까지 close 해버리면 문제가 생기지 않을까 해서요. 만일 멀티 스레드 환경에서 A 액티비티에서 백그라운드 작업을 하는중에 B 액티비티로 넘어간 후 A가 끝나지 않은 상황에서 B에서 close를 해버리는 상황같이요. 한참 전 글인데 보실려나 모르겠네요 ^^

2010.09.01 12:39

[강좌A09] 안드로이드 실전 개발 - 데이터베이스 : Part1




안녕하십니까? 오늘 강좌에서부터는 저희가 개발하고 있는 Hangul2English App에 Database 코드를 붙이도록 하겠습니다.

전체 강좌 목차

[강좌A01] Moteodev Studio를 이용한 안드로이드 개발 환경 구축 가이드
[강좌A02] 안드로이드 개발 참고 서적 소개
[강좌A03] Android 실전 개발 - 아이디어 / 기획 / Wireframe
[강좌A04] 안드로이드 실전 개발 - 아이콘 제작
[강좌A05] 안드로이드 실전 개발 - 레이아웃 및 리소스 : Part1
[강좌A06] 안드로이드 실전 개발 - 레이아웃 및 리소스 : Part2
[강좌A07] 안드로이드 실전 개발 - 리소스 해킹
[강좌A08] 안드로이드 실전 개발 - SQLite

[강좌A09] 안드로이드  실전 개발 - 데이터베이스 : Part1

일반적으로 Android database 개발은 다음과 같이 구분할 수 있습니다.

데이터베이스 생성 방식에 따라
1.Code에서 DDL문 실행 – 일반적인 방식. 간단하고 초기 적재해야 될 데이터가 적은 경우.
2.Assets 폴더에 DB를 생성한 후 실행시에 DB 파일을 복사해서 처리하는 방식 – 초기 데이터가 많거나 DB구조가 복잡할 때 주로 사용함.

데이터베이스 관련 소스 처리 방식에 따라
1.Contents Provider – DB를 다른 어플과 공유해야 할 때, 혹은, 여러명이 개발할 때 주로 사용하는 방식으로 DB 처리를 별도의 Provider로 제공하는 방법
2.직접 DB Access – 직접 DB에 query를 실행하여 처리하는 방식

저희가 개발하는 App은 단 하나의 테이블만 필요하고 초기 데이터로는 샘플로 2~3개의 레코드만 등록한 채 개발할 것이므로 Code에서 직접 DDL을 실행하는 방식으로 개발하도록 하겠습니다. 또한, 혼자서 개발하며, 타 App과 DB를 공유해야 할 필요가 없어서 Content Provider로 개발하지 않고, 개발하기 편리한 직접 Access 방식으로 개발 하도록 하겠습니다.

먼저, 저희가 생성할 데이터베이스 스키마 정보는 다음과 같습니다.

Database Name : han_to_eng_db

/* table ddl */
CREATE TABLE TB_DATA (
_ID INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, /*시퀀스, 자동채번 */
C_KOR_TEXT TEXT, /*한글 저장용*/
C_ENG_TEXT TEXT /*영문 저장용*/
);

/* unique index */
CREATE UNIQUE INDEX TB_DATA_PK ON TB_DATA (_ID);

한글/영문을 저장할 수 있는 테이블 하나와 유니크 인덱스를 하나 만들겁니다. _id 컬럼명은 안드로이드에서 BaseColumn Interface에 정의되어 있는 컬럼명으로 대부분의 코드에서 integer key column name으로 많이 사용합니다. 그래서 저도 _id를 사용하긴 했으나, 맘에 드시는 다른 컬럼명을 사용하셔도 좋습니다. 컬럼명의 “C_”는 컬럼을 구분하기 위한 prefix인데, 없어도 무방합니다. 암튼 저는 위의 DDL문을 기준으로 테이블 및 인덱스를 생성하겠습니다.

먼저, src/ 디렉토리에 다음과 같은 Constants.java 파일을 생성합니다. 이 Constants 클래스는 모든 로그에 들어갈 공통 상수를 정의하겠습니다.

//Constants.java

package com.overoid.hangul2english;

 

public class Constants {

 // Log Class 인자로 넘겨줄 Tag 정의

    public static final String LOG_TAG = "Hangul2English";

}



다음은 데이터베이스쪽 코드입니다.
일반적으로 대부분의 책 및 Android Source Sample에 나타나는 SQL 관련 코드 구조는 다음과 같습니다. 많이들 보셨을 겁니다.


public class DBHelper {

 

/* public static final DB Name, Table Name, Column Name 등을 정의하는 부분 */

 

    private SQLiteDatabase db;

    private final DBOpenHelper dbOpenHelper;

 

   

    public static class <To클래스> {

    /* Transfer Object 사용될 Bean Class 정의*/

    }

   

    /* 데이터베이스 생성. 업데이트, 연결 등의 기능을 제공하는 SQLiteOpenHelper 상속 클래스 정의*/

    private static class DBOpenHelper extends SQLiteOpenHelper {

 

        public DBOpenHelper(final Context context) {

            super(context, DBHelper.DB_NAME, null, DBHelper.DB_VERSION);

        }

 

        @Override

        public void onCreate(final SQLiteDatabase db) {

        /* 테이블 생성, 인덱스 생성, 초기 데이터 적재 코드 */

        }

 

        @Override

        public void onOpen(final SQLiteDatabase db) {

            super.onOpen(db);

        }

 

        @Override

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

        /* 업그레이드시에 처리해야 DDL 실행 */

        }

    }

 

    public DBHelper(final Context context) {

        this.dbOpenHelper = new DBOpenHelper(context);

        establishDb();

    }

 

    private void establishDb() {

        if (this.db == null) {

            this.db = this.dbOpenHelper.getWritableDatabase();

        }

    }

 

    /* 데이터 조회/등록/수정/삭제 관련 코드 왕창 추가*/

}

 


위 코드는 대부분의 안드로이드 샘플이나 책에서 소개하는 데이터베이스 코드 스타일입니다. DBHelper Class 안에 Transfer Object로 사용할 Bean Class와 데이터베이스 생성, 업그레이드, 연결등을 담당할 SQLiteOpenHelper 클래스를 상속하는 클래스 정의, 실제 사용자 DB에서 조회하고 등록,수정, 삭제 등을 처리할 메소드를 모두 DBHelper 클래스 안에 두어 이 파일 하나만 만들면 DB 관련 모든 작업을 할 수 있습니다.

제가 처음 안드로이드 개발할 때 이 방식을 사용했었는데, 단점은 한 파일의 소스코드가 무지 길어진다는 점과 여러 개의 Activity 에서 DB 처리를 해야 할 경우 DB가 Close 되지 않은 상태에서 다른 액티비티에서 Open할 때 에러가 나서 Activity 상태 변화 이벤트 처리 부분이나 select/insert/update/delete 등의 메소드 처리 부분에서 db를 close/open을 해 줘야 에러를 피할 수 있다는 점입니다.

그래서 저는 이 소스 패턴을 조금 변경하도록 하겠습니다.

소스는 향후 재사용성(Copy & Paste)을 높이는데 초점을 두겠습니다. 또한, 여러 Acticity에서 DB Handling시에 Connection 관련 에러를 막기 위해서 SQLiteDatabase 객체를 Singleton으로 만들겠습니다.
또한, DDL문과 Table, Column 정의 부분을 별도의 클래스로 작성하도록 하겠습니다.

먼저 데이터베이스 스키마를 정의하는 클래스 소스입니다. 클래스명은 데이터베이스명(Hangul2English = H2e) + “Database”라고 명명하겠습니다.

//H2eDatabase.java

package com.overoid.hangul2english.data;

 

public final class H2eDatabase {

   

    H2eDatabase() {}

   

    /* Table이나 View마다 중첩 클래스를 만듭니다.

     * 테이블명, 컬럼명 정보를 상수로 정의합니다.

     */

    public static final class DataTable {

        private DataTable() {}

       

        public static final String TABLE_NAME = "TB_DATA";

       

        public static final String COLUMN_ID = "_id";

        public static final String COLUMN_KOR_TEXT = "c_kor_text";

        public static final String COLUMN_ENG_TEXT = "c_eng_text";

       

        public String[] getColumnNames() {

            String[] columnNames = {COLUMN_ID,COLUMN_KOR_TEXT,COLUMN_ENG_TEXT};

            return columnNames;

        }

    }

}



H2eDatabase Class 내에 중첩 클래스로 각 테이블마다 클래스를 만듭니다. 저희는 하나의 테이블이라서 DataTable Class 하나만 내부에 존재하면 됩니다.
클래스명은 <테이블명 식별자> + “Table” 이라고 짓습니다. 그리고 TABLE_NAME 상수와 각 컬럼명을 “COLUMN “ + “_” + <컬럼명 식별자> 로 기술하며, SELECT문에서 사용하기 편리하도록 컬럼명 배열을 리턴하는 getColumnNames 메소드를 추가합니다.

다음은 DDL문 생성을 위한 interface를 하나 만들겠습니다. 구지 interfaces는 없어도 상관없지만, 상속 받는 클래스에서 코드도 편리하게 할 수 있고, 일관성을 유지할 수 있기 때문에 interface를 사용했습니다. Interface의 내용은 별거 없습니다. 생성할 DB명과 DB버전 상수정보와, 각각의 DDL문 배열을 리턴하는 메소드가 선언되어 있습니다.

//DatabaseCreator.java

package com.overoid.hangul2english.data;

 

public interface DatabaseCreator {

   

    public static final String DB_NAME = "han_to_eng_db";

    public static final int DB_VERSION = 1;

   

    /* Table Definition Statement */

    public String[] getCreateTablesStmt();

   

    /* Index Definition Statement */

    public String[] getCreateIndexStmt();

   

    /* View Definition Statement */

    public String[] getCreateViewStmt();

   

    /* Trigger Definition Statement */

    public String[] getCreateTriggerStmt();

   

    /* Initial Data Insert Statement */

    public String[] getInitDataInsertStmt();

}



다음은 실제 DDL문을 리턴하는 DatabaseCreator 인터페이스를 구현하는 클래스입니다.

//H2eDatabaseCreator.java

package com.overoid.hangul2english.data;

 

import com.overoid.hangul2english.data.H2eDatabase.DataTable;

 

public class H2eDatabaseCreator implements DatabaseCreator {

 

   

    /* Table Creation DDL */

    private final String TABLE_CREATE_DATATABLE = "CREATE TABLE "

        + DataTable.TABLE_NAME + " ( "

        + DataTable.COLUMN_ID + " INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "

        + DataTable.COLUMN_KOR_TEXT + " INTEGER, "

        + DataTable.COLUMN_ENG_TEXT + " TEXT); ";

       

   

    /* Index Create DDL */

    private final String INDEX_CREATE_DATATABLE = "CREATE UNIQUE INDEX "

        + DataTable.TABLE_NAME + "_pk ON "

        + DataTable.TABLE_NAME + " (" +  DataTable.COLUMN_ID + " );";

   

    @Override