현재 진행 중인 Salina 프로젝트에서 나름 국제화를 적용하겠다고 상수 스트링을 values 리소스로 빼두고

final 키워드를 제거 후 멤버 변수 위치 그대로 두고 getString(int resourceId)를 호출하니 NullPointerException 발생


프로그래밍계의 지식인  StackOverflow느님께서 역시 해결책을 주셨다.


원인은 액티비티가 초기화되기 전에 리소스를 사용하려고 하면 NullPointerException이 발생할 수 있다고 한다.


즉 액티비티에서 사용하는 getString()은 Resources 객체의 getString()을 래핑한 메서드이고.

Resources는 Context 객체로부터 얻을 수 있는데, getString()을 풀어보면

getContext().getResources().getString()이 되는 셈이다.


따라서 해결책은 멤버 필드에서 바로 초기화를 하는 대신 onCreate 이 후 시점에 해야한다.

그냥 onCreate 안에서 하면 된다.

혹은 Application 클래스를 상속한 커스텀 Application을 만들고 거기에 static 메서드를 이용해서 컨텍스트를 통해 리소스를 가져오는 방법이다.

액티비티는 해당 액티비티가 onCreate 되어서야 초기화 후에 context를 갖지만 애플리케이션 단위에서는 처음 앱이 실행될 때 정보를 가지고 있기 때문에 가능하다.






http://www.makelinux.net/kernel_map/




자바 웹 프로그래밍을 하면서 ORM을 당연하게 사용해왔는데,

안드로이드에서 SQLite DB 프로그래밍을 하려니.. ORM 쓰던게 습관이 되어있어서인지

귀찮은 점이 한두가지가 아니네요.


먼저 ORM에 대해 간략하게 소개하자면 Object Relational Mapping으로 객체와 릴레이션(RDB의 테이블)간에 매핑을 해 주는 역할을 합니다. 쉽게 말하자면 Book 이라는 객체가 있다면 이 객체를 ORM을 통해 RDB에 넣거나(Insert), 가져오거나(Select), 지우거나(Delete), 고칠 수(Update)도 있고, 기본적인 CRUD 외에도 스키마를 다루거나 조인 연산등 RDB의 기능을 객체지향적으로 다룰 수 있도록 도와줍니다.



ORM

이미지 출처 : 4 Benefits of Object-Relational Mapping (ORM)


SQLite를 대상으로 하는 안드로이드 기반 ORM 라이브러리를 몇개 찾아보았습니다. 아래와 같은 것들이 있네요.

  1. ActiveAndroid(https://www.activeandroid.com/)
  2. SqliteORM(https://github.com/kremerk/SqliteORM/wiki)
  3. ORMLite(http://ormlite.com)
  4. Storm(https://code.google.com/p/storm-gen/)
  5. Green Dao(http://greendao-orm.com)
  6. Sugar ORM(https://github.com/satyan/sugar)


글 제목에서도 알 수 있지만 Sugar ORM이 간단하게 쓰기에는 쉽게 쓸 수 있도록 되어있네요. 모두 사용해 볼 수는 없어서 각 라이브러리들의 Getting Start에서 사용 방법을 눈으로만 살펴봤는데, DB 핸들링 관련 코드를 만들기 위한 Generater와 사용을 위한 라이브러리가 따로 있는가 하면, 빌드 패스에 이것 저것 추가하고 설정도 하는 것도 있고, 이래 저래 사용하기 복잡해서 좀 귀찮아도 그냥 Helper 구현해서 쓰고 말지 하는 생각이 드는 것들이 대부분이었네요. (사실 새로운 걸 익힌다는 것에 대한 귀찮음이 맞지만요 :D )


어쨌거나 Sugar ORM이 개중에서는 가장 쓰기 쉽도록 되어있네요.

간략하게 사용 순서를 풀어보면

1. 안드로이드 프로젝트 libs 폴더에 라이브러리를 추가한다.

2. 매니페스트에 Application 클래스를 설정한다(보통은 설정되어있지 않고 디폴트로 사용합니다.)

3. 매니페스트에 meta-data 태그를 이용하여 4개의 프로퍼티를 추가한다.

4. 쓴다.






코드로 써 봐야 아는 것에 구구절절 글을 늘어놓는 것 보다 백문이 불여일견 백견이 불여일행이라 하였으니 해보도록 하겠습니다. 개발 환경은 Windows 7, Eclipse Indigo + Android SDK Plug-in입니다.


가장 먼저 Sugar ORM GitHub 다운로드 페이지에서 라이브러리 파일을 다운로드 받습니다.

https://github.com/satyan/sugar/blob/master/dist/sugar-1.1.jar

(글을 쓰는 13.03.04 기준 7개월 전에 업데이트 되었네요)


그런 다음 이클립스를 켜고 새 안드로이드 애플리케이션 프로젝트를 생성해 줍니다.



프로젝트 이름이나 패키지는 본인의 입맛에 맞게 설정하시면 되겠습니다.

다만 Package Name은 기억해 두도록 하세요. 저는 nnoco.example.sugar 로 기본 패키지 경로를 정했습니다.

나중에 Manifest.xml 파일에 속성값으로 쓸 일이 있습니다.


Next -> Next -> ... Finish를 하면 프로젝트가 짠! 하고 만들어졌습니다.


ADK 플러그인 버전에 따라서 libs 폴더를 자동으로 생성해주기도 하고, 없는 경우도 있는데요, libs 폴더는 기본적으로 안드로이드 패키지에서 라이브러리가 있는 클래스패스로 인식을 합니다. 외부 라이브러리 파일의 경우 이곳에 복사하시면 클래스패스에 따로 추가하지 않으셔도 자동으로 잡히게 됩니다.

따라서 프로젝트 루트에 libs가 없는 분은 libs 폴더를 하나 추가해주시면 되겠습니다. 그럼 아래와 같은 프로젝트 구조가 보이게 됩니다. (환경에 따라 조금씩 다를 수 있지만 여기서는 libs 폴더만 있으면 됩니다.)



그럼 저 libs 폴더 안에 프로젝트를 만들기 전에 다운로드 받은 sugar-1.1.jar 파일을 복사해서 넣어줍니다.




여기까지 되었다면 이제 Manifest.xml 파일을 편집해 보겠습니다.

해야할 것은 Custom Appication 클래스를 지정하는 것과, meta-data의 추가입니다.


<manifest xmlns:android="http://schemas.android.com/apk/res/android"

    package="nnoco.example.sugar"

    android:versionCode="1"

    android:versionName="1.0" >


    <uses-sdk

        android:minSdkVersion="8"

        android:targetSdkVersion="15" />


<!-- Custom Application 지정 -->

    <application

        android:name="com.orm.SugarApp"

        android:icon="@drawable/ic_launcher"

        android:label="@string/app_name"

        android:theme="@style/AppTheme" >

        <activity

            android:name=".MainActivity"

            android:label="@string/title_activity_main" >

            <intent-filter>

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


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

            </intent-filter>

        </activity>

        <!-- 메타 데이터 추가 -->

        <meta-data android:name="DATABASE" android:value="sugar_example.db"/>

        <meta-data android:name="VERSION" android:value="2"/>

        <meta-data android:name="QUERY_LOG" android:value="true"/>

        <meta-data android:name="DOMAIN_PACKAGE_NAME" 

                      android:value="nnoco.example.sugar"/> <!-- 본인의 패키지로 설정-->

    </application>


</manifest>



저렇게 두 부분만 (라인으로는 5줄!) 추가하면 사용할 준비는 끝입니다.

이제 사용 해보도록 하겠습니다. 만들고자 하는 엔티티는 다음과 같습니다.



간단합니다. 책 엔티티로 제목(title)과 저자(author)를 가지고 있습니다.

여기에 맞는 모델 클래스를 만들어야겠죠? 원하는 위치에 Book 클래스를 하나 만들어보도록 하겠습니다.

모델 클래스를 만들 때 Sugar ORM을 사용하면 하나의 제약사항이 있습니다. SugarRecord를 상속해서 구현해야 한다는 점과, Context를 매개변수로 받는 생성자가 필요합니다. 사실 SugarRecord를 상속 받는 건 제약이라기 보다 편의쪽에 가깝다고 봐야겠네요.




public class Book extends SugarRecord {

private String title;

private String author;


public Book(Context context) {

super(context);

}


public Book(Context context, String title, String author) {

super(context);

this.title = title;

this.author = author;

}


public String getTitle() {

return title;

}


public void setTitle(String title) {

this.title = title;

}


public String getAuthor() {

return author;

}


public void setAuthor(String author) {

this.author = author;

}

}


Context 객체를 받는 생성자와 title, author를 포함해서 받는 생성자, getter/setter를 만들어준 모습입니다.



여기까지 되었으면 메인 액티비티로 이동해서 onCreate 메서드에서 간단하게 테스트를 해보도록 하겠습니다.

// Book 인스턴스 생성

Book book = new Book(this, "논어", "공자");

        

// DB에 저장

book.save();

        

// 수정

book.setTitle("論語");

book.save();

        

// DB로부터 ID 가져오기

long id = book.getId();

        

// ID를 이용해서 객체 가져오기

Book foundBook = Book.findById(Book.class, id);

        

// 로그로 확인

Log.d("Found Book Info", foundBook.getTitle() + ", " + foundBook.getAuthor());

        

// 삭제

book.delete();


api는 참 쉽죠? save()는 없다면 생성하고, 있다면 수정합니다. delete()는 삭제구요.

static 메서드에는 find(...), 나 findById(...)과 같이 select의 기능을 하는 메서드가 있고,

listAll(...) 테이블의 모든 레코드를 가져와 리스트로 반환하는 메서드,

deleteAll(...) 과 같이 테이블의 모들 레코드를 삭제하는 메서드가 있습니다.


이외에도 1:1 관계의 표현, 1:N관계의 표현등에 대한 내용이 있지만 간단하게 소개하는 글이므로 여기까지 쓰도록 하고

더 자세한 내용은 Sugar ORM의 위키를 통해 확인하실 수 있습니다.(http://satyan.github.com/sugar/getting-started.html)


레이아웃을 어느정도 만들어서 EditText 뷰에 속성값을 넣고 추가하고 리스트뷰에서 보여주고 하는 식으로 하려다가 단순한 기능 때문에 오히려 다른 부분(리스트뷰 어댑터 만들고, 이벤트 추가하는 등의)의 작업이 많아지면 의미없는 일이라고 생각하여 그 부분은 제외하였습니다. 

리스트 뷰를 사용하여 DB내용을 확인하는 예제는 첨부하여 올리니 확인하실 분들은 다운로드 받으셔서 확인하시면 됩니다.


Android ORM Test.zip





HTTP 요청 메시지

Full request = 

요청 라인(Request-Line) : <요청 메서드> <요청 URI> <HTTP 버전>

일반헤더(General-Header)

요청헤더(Request-Header)

엔티티헤더(Entity-Header)

CRLF

엔티티본문(Entity-Body)


HTTP 메서드 종류

OPTIONS

GET - REST API에서 자료를 조회하는 기능

HEAD

POST - REST API에서 자료를 생성하는 기능

PUT - REST API에서 자료를 생성/수정하는 기능 (없다면 생성)

DELETE - REST API 자료를 삭제하는 기능

TRACE

CONNECT


HTTP 응답 메시지

Full-Response =

상태라인(Status-Line) : <HTTP 버전> <상태 코드><이유문구> CRLF

일반헤더(General-Header)

응답헤더(Response-Header)

엔티티헤더(Entity-Header)

CRLF

엔티티본문(Entity-Body)\


URL의 구성요소

scheme://<user>:<password>@<host>:<port>/<url-path>?query_string$fragment_id

Scheme : 'http', 'ftp'와 같이 프로토콜을 나타낸다

Host : DNS의 인터넷 주소 규정에 따라 서버를 가리킨다.

Port: TCP 포트번호를 나타낸다. Http 프로토콜의 경우 별도 포트번호를 명시하지 않는다면 디폴트는 80

Path : 구체적인 자원의 위치

Query : 서버에서 작동하는 애플리케이션에 전달되는 매개변수

Fragment id : 참조 또는 자원의 상세한 부분을 의미

이클립스 안드로이드 레이아웃에 EditText 뷰만 넣으면 아래와 같은 예외가 발생하면서

java.util.LinkedHashMap.eldest()Ljava/util/Map$Entry;

Exception details are logged in Window > Show View > Error Log

GUI 화면에서는 제대로 레이아웃이 보이지 않는 문제가 발생했다.


모놀로그님의 글을 통해 1차적인 문제는 해결했으나, EditText에 멀티라인이 적용되지 않는 것이다.


scrollHorizontally="false" 속성을 추가해 보기도 하고

singleLine="false" 로 해보기도 하고

lines="5" 와 같이 line 수를 하드코딩도 해보았으나 모두 적용이 되지 않는다.


1차적인 해결 방법은 EditText에 inputType="textNoSuggestions" 속성을 추가하는 것이었고, 그로 인해 파싱 오류는 없어졌다. 허나 textNoSuggestion 속성값은 IME의 엔터키에 엔터를 완료로 바꾸는 것이라(좀 더 정확히는 사전 기반의 단어 추천을 하지 않게 하는 기능이나 외부적으로는 IME에 완료 버튼을 나타나게 함) EditText 뷰 자체가 싱글라인으로 인식이 된다.


혹시나 싶어 inputType 속성의 값을 textMultiline으로 하니 원래 지정한 대로 scrollHorizontally 속성으로 수평 방향 스크롤도 없어졌으며 Gson에서 pretty print로 얻은 JSON 스트링도 정상적으로 줄바꿈이 되어 표시가 된다.


그러나 파싱 오류는 다시 발생해서 레이아웃 확인을 Graphical Layout에서 확인할 수는 없게 되었다.

textNoSuggestions 말고 근본적인 파싱 오류의 원인을 찾아봐야겠다.





참고 : inputType 속성 값에 대한 특성은 서동규님의 블로그 글에서 확인할 수 있음

팀 형이 작성한 코드를 내 쪽에서 돌리면 아래와 같은 에러메시지가 뜬다. 


"The _imaging C module is not installed"


해당 모듈은 PIL에 포함된 모듈이고, 분명 PIL은 설치했었다,

그리고 imaging.py 파일도 잘 있다.

검색해보니 비슷한 경우가 많은지 스택오버플로에 많이 올라와있었다.


문제는 x64 환경에서 pip나 easy_install을 사용하여 라이브러리를 설치하는 경우에 x86으로 컴파일이 된다고 한다.

친절히도 (검증되진 않았지만) 많이 사용되는 라이브러리의 바이너리 버전 설치파일 링크가 있다.


http://www.lfd.uci.edu/~gohlke/pythonlibs/


여기서 Ctrl-F 키를 눌러 원하는 라이브러리의 이름을 입력하고 자신의 운영체제 버전에 맞는 설치파일을 받아서

설치만 하면 끝난다.





또 다른 문제로 MySQL 라이브러리를 설치하는데...


"Unable to find vcvarsall.bat"


컴파일 시 VC(Visual C++) 컴파일러를 사용하도록 되어있는데 배치파일을 찾을 수 없다는 메시지라고 한다.

해결하기 위한 여러가지 방법이 있었으나..(설정 파일 변경, 쉘 파일 추가 등등..)

귀찮아서 잠시 미뤄뒀다가. 역시 컴파일 문제니.. 이것도 바이너리 버전의 라이브러리를 설치하면 되지 않을까하고

위의 바이너리 라이브러리 모음 사이트에 들어가서 MySQL 바이너리 설치파일을 받아 설치하니, 문제 해결.


속성으로 하다보니 기본적인 설정에서부터 애로사항이 많다..

WEB-INF 폴더 내의 web.xml에 서블릿으로 Spring의 DispatcherServlet을 사용하는 경우에


<servlet>

<servlet-name>servletname</servlet-name>

<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

<servlet>


위와 같이 설정하는데, 이 때 서버에서는 해당 서블릿의 컨텍스트 파일을 읽게 되는데, 파일 명은

<servlet-name>에 들어간 값에 -servlet 접미사가 붙은 xml파일을 WEB-INF 폴더로부터 찾게 된다.

따라서 서블릿 이름과 서블릿 컨텍스트 설정 파일의 이름(접미사를 제외한)은 같아야 한다.



만약 서블릿 이름을 foo로 했다면 web.xml의 설정과 컨텍스트 설정 파일 이름은 아래와 같다.



WEB-INF/web.xml

... 생략 ...

<servlet>

<servlet-name>foo</servlet-name>

<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

</servlet>


... 생략 ...



위 설정에 따른 서블릿 컨텍스트 설정 파일 : WEB-INF아래 foo-servlet.xml




에러 내용은 아래와 같았다.



오류 내용은 iBatis 매핑 관련 설정 파일인 SqlMapConfig.xml에서 발생했고,

오타가 문제였다. 책 샘플 중의 오타...


<settings

        cacheModelsEnabled="true"

        enhancementEnabled="true"

        lazyLoadingEnabled="true"

        maxRequests="32"

        maxSessions="10"

        maxTransactions="5"

        userStatementNamespaces="true"

        />


Sql문 매핑 시 네임스페이스를 사용할 지 말지를 설정하는 부분에서

user가 아닌(어쩌면 당연하게도)useStatementNamespace였다.


아직 예제를 보면서 따라하는 수준이다 보니 에러가 나는 저 부분을 삭제하고 서버를 다시 실행했는데,

이번엔 템플릿을 통해 sql문을 실행할 때 매핑에 설정해둔 구문의 이름을 찾을 수 없다고 뜬다.


이름은 네임스페이스를 줘놓고, 설정에서는 네임스페이스를 사용하지 않는다고 했으니 당연한 에러다.


만약 네임스페이스를 사용하지 않는다면 모든 Statement에 유일한 이름을 부여하면 되겠다.


[그림 1] ListBox의 빈 공간


ListBox에서 항목을 더블 클릭하여 선택된 항목에 대한 이벤트를 처리하고 싶은데,

MouseDoubleClick 이벤트나 DoubleClick 이벤트를 이용하면 [그림 1]의 하늘색으로 표시한 곳과 같이 

리스트 박스 내 항목이 없는 빈공간에서 더블클릭을 해도 이벤트가 발생합니다.

보통은 빈 공간에 더블클릭을 했을 때는 이벤트가 발생하지 않기를 원하겠죠?


이를 해결하기 위해서는 ListBox의 IndexFromPoint 메서드를 사용하면 됩니다.


메서드 시그너쳐는 다음과 같습니다.


  • int IndexFromPoint(Point p)
    • Point 타입의 p 좌표에 위치한 항목의 인덱스 반환

  • int IndexFromPoint(int x, int y)
    • x, y 좌표에 위치한 항목의 인덱스 반환


위 메서드는 매개변수로 전달된 좌표에 항목이 존재하면 해당 항목의 인덱스를 반환하고,

빈 공간인 경우는 -1을 반환합니다.


이벤트 핸들러로 전달된 MouseDoubleClick 객체에는 이벤트가 발생한 마우스의 좌표값을 가지고 있으므로

이 좌표값과 IndexFromPoint 메서드를 이용하면 더블클릭한 항목을 알 수 있습니다.



[그림 1]의 폼을 이용하여 리스트 박스에서 더블클릭한 항목을 얻는 예를 보겠습니다.

먼저 [그림 1]과 같이 리스트 박스와 텍스트 박스를 배치하고, 리스트 박스에 적당한 항목을 추가합니다.

'언어 목록'에서 더블클릭한 항목을 '선택한 언어'란에 표시하도록 해보겠습니다.


리스트 박스를 선택한 후 속성 창에서  모양을 눌러 이벤트 연결 탭으로 이동 후

MouseDoubleClick 항목의 오른쪽 빈 공간을 더블클릭 해서 이벤트 핸들러를 생성해 줍니다.

(리스트 박스를 더블클릭 하면 리스트 박스의 기본 이벤트인 SelectedIndexChanged 핸들러가 생성됩니다.)


[그림 2] MouseDoubleClick 이벤트 핸들러 추가



[그림 3] 생성된 MouseDoubleClick 이벤트 핸들러



이벤트 핸들러가 생성되면 아래와 같이 코드를 입력합니다.

여기서 제가 만든 리스트 박스의 이름은 lbLanguages 이고 텍스트 박스는 tbSelectedLanguage 입니다.


private void lbLanguages_MouseDoubleClick(object sender, MouseEventArgs e)
{
      // 인덱스를 저장할 변수
      int selectedIndex = -1;

      // 마우스 포인터의 위치
      Point point = e.Location;

      // 리스트 박스의 IndexFromPoint 메서드 호출
      selectedIndex = lbLanguages.IndexFromPoint(point);

      if(selectedIndex != -1) // 빈 공간이 아닌 곳을 더블클릭 했을 때.
      {
          // 선택된 항목 저장
          string selectedItem = lbLanguages.Items[selectedIndex] as string;

          // 선택한 항목으로 텍스트 대입
          tbSelectedLanguage.Text = selectedItem;
      }
 }


소스 코드를 다 쓰신 후 Ctrl + F5 키를 눌러 실행해서 리스트 박스에서 더블클릭 해 보면

빈 공간에서는 아무런 처리를 하지 않고, 항목이 있는 곳에서만 해당 항목을 텍스트 박스에 표시합니다.


[그림 4] 실행 결과



'밤을 지새다' 카테고리의 다른 글

Google Drive를 이용한 파일 버전 관리  (0) 2013.05.24
Linux Kernel Map  (0) 2013.03.24
네이트온 광고와 팝업 없애기  (3) 2012.05.16
파일의 확장자를 보는 방법  (0) 2012.05.16
Gmail 실행파일 첨부 문제  (0) 2012.05.16

+ Recent posts