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

전체 강좌 목차

[강좌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 레벨을 사용하시면 런타임시 부하를 줄일 수 있습니다.

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

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



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

  1. zzl986 2011.03.24 22:15 신고

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

  2. 김영호 2011.04.18 13:43 신고

    강의 잘 보았습니다 ^^+

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

    감사합니다. __+

  3. BlueSkygogo 2011.06.25 17:54 신고

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

  4. 민짜 2011.10.06 13:24 신고

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

  5. 미래소년 2012.04.05 23:39 신고

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

  6. 2012.07.05 16:36

    비밀댓글입니다


안드로이드 실전 개발 데이터베이스편 파트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에서 뵙겠습니다.


  1. 이유식 2010.09.05 14:52 신고

    많은걸 배우고 갑니다.

  2. 선지헌 2010.09.29 20:12 신고

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

    • 보고픈 2010.09.30 15:48 신고

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

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

  3. 리칼 2010.12.10 17:48 신고

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

  4. jordan retro 12 2012.03.05 17:58 신고

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

  5. 맹순이 2014.10.28 00:34 신고

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


안녕하십니까? 오늘 강좌에서부터는 저희가 개발하고 있는 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

    public String[] getCreateTablesStmt() {

        String[] tableStmt = {TABLE_CREATE_DATATABLE};

        return tableStmt;

    }

 

    @Override

    public String[] getCreateIndexStmt() {

        String[] indexStmt = {INDEX_CREATE_DATATABLE};

        return indexStmt;

    }

 

    @Override

    public String[] getCreateViewStmt() {

        // TODO Auto-generated method stub

        return null;

    }

 

    @Override

    public String[] getCreateTriggerStmt() {

        // TODO Auto-generated method stub

        return null;

    }

 

    @Override

    public String[] getInitDataInsertStmt() {

        String[] initData = new String[2];

        String[][] initValue = {{"사랑","tkfkd"},

                                {"깍쟁이","Rkrwoddl"}};

 

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

            initData[i] = "INSERT INTO " + DataTable.TABLE_NAME + "("

                         + DataTable.COLUMN_KOR_TEXT + ","

                         + DataTable.COLUMN_ENG_TEXT + " ) "

                         + " VALUES( "

                         + "'" + initValue[i][0] + "','" + initValue[i][1] + "');";

        }

        return initData;

    }

 

}

 

각각의 DDL문을 상수로 정의한 다음 DatabaseCreator 인터페이스 구현 메소드에서 각각의 DDL 문장 배열을 리턴하도록 작성합니다.

나머지 소스들이 내용이 많아서 다음 파트에서 나머지 내용을 계속 진행하도록 하겠습니다.

  1. jordan retro 13 2012.03.05 17:58 신고

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

  2. jordan retro 12 2012.03.05 17:59 신고

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


두번째 앱 "Hangul to English"가 드디어 TStore 심사를 통과하고 시장에 나왔습니다.

네~네. 지금 제가 열심히 개발 강좌 쓰고 있는 그 앱입니다.
강좌가 끝날때에는 제 소스가 여러분들에게 모두 오픈 될 것입니다.
뭐~ 대단하지도 않은 소스라 큰 도움은 안되시겠지만, 제가 강좌를 통해 Touch Call 개발하면서, Hangul To English 개발하면서 느꼈던 어려움이나 고생했던 부분들에 대해 책에 잘 안나오는 것들을 중심으로 가능한 많이 설명을 하려고 하니 제 블로그 강좌에 많은 관심 가져 주시기 바랍니다.

[App 소개]

스마트폰 사용하시다가 특정 사이트 패스워드를 한글로 만들었기 때문에 대응하는 영문 자판을 몰라서 제대로 로그인 하지 못한적은 없으십니까? 자판으로 칠 때는 자판을 손이 외우고 있으니 아무 생각없이 패스워드를 입력했는데.. 스마트폰에서는 자판 배열도 틀려서 제대로 한글에 해당 되는 영문 자판을 모르실때 사용할 수 있는 어플입니다.

다시말해 Hangul to English는 한글로 만들어진 텍스트에 해당되는 영문 자판 글자를 찾아주는 App입니다.

[App 특장점]

Hangul To English 는 쉽게 한타를 영타로 변환해 줍니다. 또한, 자주 사용하시는 단어를 DB에 저장할 수 있으며 버튼 클릭으로 Clipboard에 복사가 가능하므로 쉽게 타 프로그램이나 웹사이트에 로그인 할 수 있도록 도와 줍니다.

 
 
 Hangul To English 메인 화면  Hangul To English 소개 화면


[App 사용 가이드]

1. 한글 입력창에 한글을 입력하시면 대응되는 영문 자판 글자가 영문 입력창에 나타납니다.

2. 한글 입력창 옆의 버튼은 입력한 글자를 지워주는 기능 버튼입니다.

3. 영문 입력창 옆의 버튼은 영문으로 나타난 글자를 클립보드에 복사해 주는 기능입니다. 간단히 복사해서 입력하고자 하는 화면에서 텍스트 창을 길게 누르시면 선택옵션에서 붙여넣기를 하시면 됩니다.

4. 텍스트 저장 버튼은 화면에 나타난 한글/영문 텍스트를 DB에 저장해 주는 기능입니다.

5. 저장된 텍스트 목록은 하단 리스트에 나타나며, 리스트에서도 클립보드에 복사하실 수 있으며, X 버튼 클릭으로 바로 삭제도 가능합니다.

6. 메인 타이틀 옆의 Info 버튼을 클릭하시면 저작자 블로그 및 저작자에게 문의할 수 있는 이메일 주소가 나타납니다. HTML 태그를 클릭하시면 폰에서 메일을 작성하거나, 폰으로 블로그 컨텐츠롤 볼 수 있습니다.

[App 다운로드]

Hangul To English는 TStore 에 등록되어 있습니다. 가격은 무료입니다. 앱 소스와 실전 개발 가이드는 블로그를 통해 제공될 예정입니다. 앱에 대한 문의는 블로그나 메일로 보내주시면 됩니다.


다운은 요기서 (http://www.tstore.co.kr/userpoc/game/viewProduct.omp?insDpCatNo=DP04003&insProdId=0000031822&prodGrdCd=PD004401&t_top=DP000504)



[개발후기]

본 앱은 순전히 제가 필요해서 만든 앱입니다.
처음에 만들때는 강좌는 생각지도 않았었고, 네. 그때는 블로그도 없었지요.
한 일주일이면 개발할 수 있을 것 같아 쉽게 시작을 했는데.. 거의 마무리 단계에서 회사 업무가 갑자기 몰리고 일이 한가해 질때 휴가 다녀오느라..개발 완료가 지체되어 버렸습니다.

개발할 당시 TStore에 비슷한 앱이 없었는데, 등록할려고 보니 거의 유사한 앱이 이미 등록이 되어 버렸더군요.
그것도 무료로.. 말입니다.

그래도 유료로 한 500원에 팔면 저희같이 가난한 개발자들이야 S/W에는 돈 100원도 아까워서 잘 구매 안하지만, 일반인들 중에서 별로 푼돈에 연연 안하시는 분들도 많이 계셔서 한 100개 정도는 팔 수도 있겠지만..

그냥 오픈하기로 했습니다. 광고도 빼버리고..
거기다가 오픈하면서 아예 소스랑 앱 실전 개발 가이드까지...



 

'앱스' 카테고리의 다른 글

Touch Call - New Version Released!  (7) 2010.10.18
두번째 App - "Hangul To English" 출시  (3) 2010.08.14
첫번째 Andorid App - Touch Call (터치콜)  (3) 2010.07.31
  1. starcrazy 2010.08.14 01:34 신고

    맞아요 정말 개발일하면 오히려 S/W는 더 안사게 되는듯...특히 회사에서는 간단한거면 만들어 쓰라고 하기도 하고...^^;

  2. acdam 2010.09.05 01:32 신고

    Hangul To English 앱이 이런 사연이있었군요
    덕분에 저희는 앱 실전으로 배울수있어서 다행이네요 ㅎㅎ

  3. 사자고구미 2010.09.21 18:17 신고

    안드로이드사이드 강좌에서 링크타고 왔습니다. 배우기 전 먼저 감사인사드립니다. ^^*

+ Recent posts