iphone 4의 주요 기능 중 하나가 폴더 기능이죠. 안드로이드 OS에서는 벌서 부터 폴더 기능을 지원했었는데, 의외로 사용하시는 분들이 이 기능을 잘 모르고 계신것 같아서 팁으로 알려드립니다.

물론, iPhone은 안드로이드 처럼 Live Folder란 개념이 없고 프로그램 목록만 나타나므로, 프로그램 목록에 폴더 기능이 추가되었지만, 안드로이드에서는 Live Folder에 폴더를 위젯처럼 추가하실 수 있습니다.

 ㅁㅁ

위 그림은 제 폰에 게임 폴더를 하나 만든 후 게임을 넣어 둔 모습입니다.

Live Folder에 폴더를 만들기 위해서는  아래 그림을 따라 하시면 됩니다.

 

먼저, 홈 화면에서 폴더 를 추가하고자 하는 화면을 이동한 후에 폰의 좌측 메뉴버튼을 클릭합니다.
여기서 추가 메뉴를 선택하시면 아래 그림과 같이 홈화면에 추가 팝업이 뜹니다.

 

여기서 폴더를 선택합니다. 그러면, 여러가지 폴더 목록이 나옵니다.


 

그중에서 새 폴더를 선택하시면 홈 화면에 "폴더"란 이름으로 폴더가 추가됩니다.

 

폴더의 이름을 변경하거나, 폴더에 어플을 추가하시려면 반드시 폴더를 클릭하여 아래 그림과 같이 여셔야 합니다.

 

열린 상태에서 폴더 이름을 변경하려면 열린 대화창의 상단 "폴더"이름을 길게 누르시면 폴더이름변경 창이 나타납니다. 여기서 이름을 수정하시고 확인 누르시면 폴더 명이 변경됩니다. 저는 폴더 이름을 "임시"로 변경해 보겠습니다.

 

폴더명이 변경되었습니다. 폴더에 어플을 추가하기 위해서는 아래 그림처럼 폴더가 열린 상태에서 하단의 "메인메뉴" 버튼을 눌러 어플 목록이 나타난 화면으로 이동합니다. 여기서 일반적으로 홈화면에 어플 추가하듯이 폴더에 추가할 어플을 길게 누르시면 어플을 이동할 수 있는 상태로 변경이 되며, 이 상태로 열린 폴더에 드래그 & 드랍 하시면 폴더에 어플이 추가됩니다.

 

아래 화면은 세계시각 어플을 임시 폴더에 드래그 하는 화면입니다.

 

아래 그림은 2개의 어플을 폴더에 추가한 화면입니다.

 

폴더 삭제는 홈화면에 등록된 어플 삭제와 동일합니다. 길게 선택하시고, 휴지통으로 드래그 & 드랍 하시면 삭제됩니다. 물론, 폴더가 삭제된다고 해서 폴더내의 어플이 삭제되는 건 아니니, 걱정안하셔도 됩니다.



초보자 분들 도움이 되셨는지 모르겠습니다.

'안드로이드 Tip & Review' 카테고리의 다른 글

안드로이드 사용 Tip - 폴더 기능  (3) 2010.09.20
  1. 오자서 2010.09.20 12:45 신고

    좋은정보 감사합니다...

  2. 2011.01.25 11:36

    비밀댓글입니다

    • 보고픈 2011.01.26 11:44 신고

      Android에서 flash를 구동할 수 있는 방법은 모바일 브라우저의 Flash Player위에서 구동되거나 Air (일종의 모바일 VM) 위에서 구동이 가능합니다.
      원하시는 바가? 자바 어플안에 삽입이 가능한지? 이시면 원하는 화면만 WebView로 만드셔서 넣으시면 될 것이고, 그게 아니고 일반 안드로이드 UI안에 삽입을 원하시면.. 그 부분은 기본적으로 Android에서 지원은 하지 않는걸로 알고 있습니다. 혹시 외부 라이브러리 같은게 있을수도 있으니 그 부분은 구글링 해보시기 바랍니다.
      또 Air로 전체 Android 앱을 개발할 수 있으므로 (해외 원서는 관련 책들이 나오고 있으니 참고하시면 됩니다) Flash로 앱 전체를 만드는것도 좋은 방법이라고 생각합니다. Air안에 Sqlite도 있고, 구글맵도 되고... 좋은것 같습니다.
      Air로 개발하시면 런타임 환경에서 Android Native Code로 변환되어 실행되므로 속도도 별 문제가 없을걸로 생각됩니다.
      충분한 답변이 되었나 모르겠네요.


오늘은 안드로이드 실전 강좌의 마지막 포스트로 개발한 소스를 Release시 검토내용 및 apk 생성 방법에 대해서 알아보도록 하겠습니다.

전체 강좌 목차

[강좌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, 알림 
[강좌A15] 안드로이드 실전 개발 - Release (apk 생성)

 
1. Application Title & Main Icon 설정.

이 부분은 이미 개발할 때 다 설정한 부분이므로 최종 점검을 하시면 될 것 같습니다.

2. Application Version 설정.

최초 Release에서는 버전정보가 디폴트값인 1이므로 크게 중요하지는 않지만, 업그레이드용 제품 Release에서는 버전 정보가 중요합니다. 안드로이드는 설치되는 응용프로그램이 업데이트인지 아닌지는 응용프로그램의 메니페스트 파일에 설정된 버전코드(android:versionCode="1")를 보고 판단합니다. 버전코드의 실제 값은 중요하지 않으며, 이전 Release 버전보다 큰 값을 지정하면 됩니다.

3. 디버깅 및 로깅 기능 비활성화

다음으로는 디버깅 및 로깅 기능을 꺼야 합니다. 디버깅을 끄기 위해서는 AndroidManifest.xml 파일의 <application> 엘리먼트의 android:debuggable 속성의 값을 false로 설정하면 됩니다.
로깅 기능은 Log.v() 메소드의 경우 배포용 패키지를 만들 때 컴파일 단계에서 빠지므로 별 처리가 필요없으며, Log.d() 메소드의 경우 컴파일은 되지만 런타임시에 빠지기 때문에 출력될 일은 없습니다. 하지만, error, warn, info 레벨의 로그의 경우 런타임시에 출력이 되므로, 꼭 필요한 정보만 로깅을 하도록 해야 하며, 초기 개발시 부터 로그 출력여부를 판단하는 상수를 하나 두고 if문으로 체크하도록 로그 함수를 만드신다면, 배포 패키지 생성시 간단하게 조작할 수 있어 편리할 것입니다.

4. 응용프로그램 권한 확인.

이 부분이 꼭 필요합니다. 애뮬레이터에서 모든 권한을 점검하거나 강제하지 않기 때문에 필요한 권한 목록을 확인하시고, 최종 폰 테스트도 권한 때문에 수행해야 될 것입니다. 불필요한 권한을 부여하지 않고 꼭 필요한 권한만 추가하는 게 가장 좋겠죠.

5. 패키지 작성.

안드로이드 폰의 패키지 관리자는 디지털 서명이 없는 패키지는 설치를 하지 않습니다.  디지털 서명 생성시 유효기간을 설정하는데, 유효기간은 설치 시점에만 점검을 합니다. 일단, 설치가 되면 디지털 서명의 유효기간이 지나도 어플리케이션은 정상 작동을 합니다. 그리고 어플리케이션의 업그레이드나 업데이트는 반드시 이전에 만든 키 (동일한 키)로 서명을 해야 합니다. 그렇지 않으면 제대로 업그레이드가 되지 않습니다.

디지털 서명과 배포패키지를 cmd 명령어로 할 수도 있지만, 툴에서 워낙 잘 지원하므로 저는 Motodev를 이용하여 만들도록 하겠습니다.

5-1. 서명키 생성.

먼저, 배포판을 만들 패키지를 Eclipse의 Package Explorer에서 선택하시고, 우측 마우스 클릭.
Android Tools > Export Signed Application Package .. 를 선택합니다.



그림처럼 배포판을 만들 패키지가 선택되어 나타납니다. Next 클릭



다음은 생성할 키가 저장될 위치 및 키 파일 이름을 지정하는 창이 뜹니다. 만일, 업그레이드용 배포판이라면 이전에 만들어둔 키 파일을 선택하시면 됩니다. 신규 생성인 경우, 특정 폴더를 만들고, 해당 폴더 및 파일명을 지정한다음, 패스워드를 지정하시고 Next 클릭.



키 생성에 필요한 정보를 입력합니다. 유효기간은 50년 정도 설정하시고, 국가코드를 ko로 설정하심 후 Next 클릭.



최종 생성될 apk 파일명과 디렉토리를 지정하시고 Finish 누르시면 apk 배포파일이 생성됩니다.

6. 마켓에 배포.

이제 마켓에 생성한 apk 파일을 등록하시면 됩니다.
국내는 SK Tstore, KT Show, LG U+ 모두 등록이 가능합니다만, 조건들이 모두 틀립니다. 현재는 SK가 사용자가 가장 많고, 완전 무료인지라 SK에 등록하시고, Google Market에 등록 하시면 될 것 같습니다. Google Market에 등록하는 방법은 모든 책마다 다 있는 내용이고, 국내 업체에 등록하는 것도 회원 가입하시고 관련 자료 찾아보시면 그리 어렵지 않게 하실 수 있을 겁니다.

이로서 안드로이드 실전강좌가 끝이 났습니다. 당분간은 앱 개발하면서 좋은 자료나 제가 공부한내용을 Android Tip & Tech 시리즈로 포스트 할 예정이며, 실전강좌로 할만한 소스가 나오면 그때 실전강좌 시리즈를 포스트 하도록 하겠습니다.


 

  1. 리칼 2010.12.10 17:51 신고

    혹시 안사의 오버로이드님?? ㅋㅋ 맞으신가요?? 제로사우스님이 알려주시더라구요~~

    맞으시면 ~~ 저는 리칼입니다 ㅎㅎ


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

오늘은 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 기능이 업그레이드 된 소스를 첨부합니다. 필요하신 분은 다운받으셔서 활용하시면 됩니다.



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

전체 강좌 목차

[강좌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

    비밀댓글입니다


이제 메인 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 앱은 개발이 완료 될 것 같습니다.

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


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

  1. 뉴이 2011.04.01 19:08 신고

    정말 감사합니다^^


이번의 강좌는 잠깐 쉬어가는 편입니다. Database 개발 관련하여 글을 쓰다가 Motodev의 Database 기능을 사용해 봤는데, 생각보다 너무 좋습니다. 유용한 기능이 많아서 소개할까 합니다. 대부분 그림이니 맘 편하게 보시고 한 5분 정도만 투자하셔서 해 보시면 될 것 같습니다.

전체 강좌 목차

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

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

1. Database 생성하기. 테이블 생성하기.

먼저, Motodev를 실행하고, Window > Open Perspective > Motodev Database 퍼스펙티브를 실행합니다.



왼쪽의 Motodev Database Explorer에서 우리 프로젝트인 Hangul2English를 선택한 후 우측마우스 > Create database.. 를 선택합니다.



데이터베이스명을 hangul2english.db라고 입력한 후 OK. 데이터베이스가 생성됩니다.
실제 데이터베이스는 프로젝트 디렉토리의 assets\ 하위에 생성됩니다. 테이블 생성을 위해서 생성한 DB를 아래 그림 처럼 선택하고 Connect 를 선택하여 DB에 연결합니다.



데이터베이스가 연결되면 파란색 아이콘이 생성됩니다. 우측마우스 > Create Table .. 선택합니다.



테이블명 및 컬럼 정보를 입력합니다.



테이블 정보 입력 완료된 화면입니다. Finish를 누르면 테이블이 만들어집니다.

2. DDL문 Generate 하기.

Motodev Database Explorer에서 생성한 db를 선택한 후 우측마우스 클릭 > Generate DDL을 선택하여 DDL문을 만들 수 있습니다.

3. DML 실행 및 테스트

Motodev Database에서는 SQL문도 처리가 가능합니다. Motodev Database Explorer에서 아래 그림과 같이 Open SQL Scrapbook을 선택하여 실행합니다.



열린 SQL Scrapbook 에디터에서 SQL문을 실행하면 하단 Grid에 결과가 나타납니다.<아래그림>


4. Create Database Management Classes..

정말 좋은 기능은 이 기능입니다. Database 관련 class를 자동으로 만들어 줍니다.
아래 그림처럼Motodev Database Explorer에서 Create Database Management Classess.. 를 실행합니다.



필요한 정보를 수정한 후 Finish 누르면……짜자잔~


2개의 class가 만들어 졌습니다. Generate 된 소스는 길어서  캡처하지 않았습니다. 한 번 해보시기 바랍니다.

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

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

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

위에서 준 옵션으로 Class를 Generate 했을 때는 테이블명 + ContentProvider.java라는 이름의 Content Provider용 자바 코드가 만들어지며, 데이터베이스는 생성했기 때문에 asets\하위에 생성된 db 파일을 복사해서 db를 생성하도록 코드가 만들어졌습니다.

필요한 코드를 Motodev에서 다 만들어주니 일이 한결 수월해 지는 군요.
그리고 앞서 제가 개발한 DB소스에서 다루지 않은 두 부분으로만 코드가 만들어 졌으니 정말 딱입니다.

생성된 클래스 파일을 지우고, Generate Content Providers for each table 체크를 끄고 다시 코드를 Generate 해보니, Content Provider Code만 생성이 안되고 나머지 처리는 동일하게 진행이 되었습니다.

끝으로 Motodev Database를 이용하면 애뮬레이터나 폰에서 테스트 할 때 DB에 붙어서 Query를 실행하고 DDL문을 Generate하는 식의 작업이 가능합니다.

다음 화면은 제 애물레이터의 telephony provider의 mmssms.db의 테이블 스키마를 Motodev Database Explorer에서 확인한 화면입니다.



이로서 안드로이드 실전강좌 데이터베이스 편을 마칩니다. 곧 이어 Main UI 소스 부분도 오픈 됩니다.


  1. 이유진 2010.09.25 18:18 신고

    안드로이드를 공부하면서 어플을 만들고 있는 학생입니다.질문이 하나 있는데요. 따로 디비를 만든것은 SQLite용으로 변환하여 사용 할려고 하는데 그방법을 모르겠습니다. 관련 자료나 소스 있으시면 kodog1022@naver.com으로 메일하나 날려주시면 고맙겠습니다 ^^;

  2. kenny 2010.11.19 08:15 신고

    보고픈 글쓴이님 Eclipse에도 똑같은 기능을 쓸수있을까요? motodev만된다면, 갈아타야겠네요. 은근히 좋은 옵션인거같아보여요.

    • 보고픈 2010.11.20 10:09 신고

      Motodev를 이클립스 플러그인으로 설치하셔서 사용하시면 됩니다. 다운은 http://developer.motorola.com/docstools/motodevstudio/download/ 요기서 하시면 됩니다.

  3. kenny 2010.11.23 10:06 신고

    보고픈님 감사합니다.저도 강좌를 보다 마음이 급해서 플러그인을 설치하고야말았어요.
    꽤 gui가 많이 변하거같으면서도 변한거는없이 플러그인설치로인해 옵션이 더많이늘어났네요. ^^
    자주얘기해요 보고픈님.

  4. ugg boots kids 2010.11.25 17:48 신고

    Nothing is impossible to a willing heart。

  5. ugg boots kids 2010.11.25 17:50 신고

    Nothing is impossible to a willing heart。


안드로이드 실전 개발 데이터베이스편 파트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() {}