블로그 이미지
평범하게 살고 싶은 월급쟁이 기술적인 토론 환영합니다.같이 이야기 하고 싶으시면 부담 말고 연락주세요:이메일-bwcho75골뱅이지메일 닷컴. 조대협


Archive»


 
 

구글의 IOT 솔루션

클라우드 컴퓨팅 & NoSQL/M2M & IOT | 2017.03.10 10:31 | Posted by 조대협


구글의 IOT 솔루션


조대협 (http://bcho.tistory.com)


오늘 샌프란시스코 구글 NEXT 행사에서 IOT 솔루션에 대한 소개가 있었는데, 내용이 괜찮아서 정리를 해놓는다.



구글의 특징은 안드로이드 플랫폼, 클라우드 , 분석 플랫폼, 개발자 에코 시스템  등 End to End 에 걸쳐서 상당히 다양한 포트폴리오를 가지고 있다는 것이 장점인데 이를 잘 녹여낸 아키텍쳐 구성이다.

디바이스 OS

IOT는 라즈베리파이와 같은 임베디드 디바이스를 사용하는 것이 일반적인데, 이런 임베디드 시스템 운용에 어려운 점중의 하나가 보안이다.

장비에 따라서 보안적인 문제가 없는지 체크를 해야 하고, 주기적으로 기능 및 보안에 대한 업데이트를 해줘야 하는데, 구글의 Android IOT (https://developer.android.com/things/index.html) 플랫폼은 이를 다 자동으로 해준다.


더구나, 기존의 모바일 안드로이드 플랫폼을 기반으로 하기 때문에, 안드로이드 개발자 풀을 그대로 사용할 수 있다는 장점이 있다.

이미 Android IOT 플랫폼은 인텔,라즈베리파이등 여러 디바이스 업체와 협업을 하면서 Certi 작업을 하고 있기 때문에 잘 알려진 플랫폼이라면 보안 테스트나 별도의 기능 테스트 없이 바로 사용이 가능하다.


백앤드

IOT의 백앤드는 구글 클라우드 플랫폼을 이용한다.

  • 디바이스로 부터 수집된 데이타는 Pub/Sub 큐에 저장된후

  • DataFlow 프레임웍을 통해서 배치나 실시간 스트리밍 분석이 되고

  • 분석된 데이타는 빅테이블이나 빅쿼리에 저장된다. 분석이나 리포팅을 위해서는 빅쿼리, 타임 시리즈 데이타나 고속의 데이타 접근을 위해서는 빅테이블이 사용된다.

  • 이렇게 저장된 데이타들은 구글의 머신러닝 프레임웍 텐서플로우의 클라우드 런타임인 CloudML을 사용해서 분석 및 예측 모델을 만들게 된다.



머신러닝을 등에 탑재한  디바이스

구글이 재미있는 점은 텐서플로우라는 머신러닝 프레임웍을 가지고 있다는 것인데, 애초부터 텐서플로우의 디자인은 서버 뿐만 아니라, 클라이언트 그리고 IOT 디바이스에서 동작하게 디자인이 되었다. 그래서 학습된 모델을 디바이스로 전송하여, 디바이스에서 머신러닝을 이용한 예측이 가능하다.

예를 들어 방범용 카메라를 만들었을때, 방문자의 사진을 클라우드로 저장하는 시나리오가 있다고 하자.

그런데 매번 전송을 하면 배터리나 네트워크 패킷 요금이 문제가 될 수 있기 때문에, 텐서 플로우 기반의 얼굴 인식 모델을 탑재하여 등록되지 않은 사용자만 사진을 찍어서 클라우드로 전송하게 하는 등의 시나리오 구현이 가능하다.


파이어 베이스 연동

동영상을 보다가 놀란점 중의 하나는 파이어 베이스가 Android IOT에 연동이 된다.

아래 그림은 온도를 측정해서 팬의 속도를 조정하는 시나리오인데, 우측 하단에 보면 파이어베이스가 위치해 있다.



센서로 부터 온도를 측정한 다음, 디바이스 컨트롤러로 온도 조정 명령을 내리는 것을 파이어베이스 메시징 서비스를 이용하도록 되어 있다.


결론

Android IOT 서비스 하나만 IOT 서비스로 내놓은 것이 아니라 구글 클라우드 플랫폼, 텐서플로우에 파이어베이스까지 구글의 기존의 노하우들을 묶어서 포트폴리오를 만들어 내었고, 더구나 이러한 기술들이 개발자 에코 시스템이 이미 형성이 되어 있는 시스템인 점에서, IOT 개발에 있어서 누구나 쉽게 IOT 서비스를 개발할 수 있게 한다는데, 큰 의미가 있다고 본다.


'클라우드 컴퓨팅 & NoSQL > M2M & IOT' 카테고리의 다른 글

구글의 IOT 솔루션  (0) 2017.03.10
TI의 IOT 개발용 센서 키트  (0) 2016.03.17
MQTT 서버 간단 공부 노트  (2) 2014.02.13

안드로이드에서 ListView를 이용한 채팅 UI 만들기


조대협 (http://bcho.tistory.com)


안드로이드 프로그래밍 기본 개념이 어느정도 잡혀가기 시작하니, 몬가 만들어봐야겠다는 생각이 들어서 생각하던중에 결론 낸것이, 간단한 채팅 서비스, 기존에 node.js 하면서 웹용 채팅을 만들어보기도 했고, 찾아보니, 안드로이드용 SocketIO 라이브러리도 잘되어 있어서 서버 연계도 어려울것이 없을것 같고, 또한 메세지가 왔을때 푸쉬 알림을 써야 하는 등 이것저것 실습이 될것 같아서, 결국은 채팅으로 정했다.


서버나 연계 코드 구현보다, 가장 어려운게 역시나 UI 디자인과 프로그래밍인데, 가장 쉬운 방법으로는 ListView를 사용하는 방법이 무난하다. (결국 코딩을 하고 나니 여러가지 한계를 느껴서 다른  UI를 찾으려고 하고는 있지만)


궁극적으로 만들고자 하는 UI는 카카오톡 처럼 말풍선이 나오는 UI이다. 





말풍선은 Ninepatch (나인패치) 이미지 라는 것으로 만들 수 가 있는데, 나인 패치 이미지에 대해서는 나중에 알아보도록 하고, 여기서는 말풍선 없이, 화면에 스크롤되서 내려가는 텍스트 기반의 대화창을 만드는 것을 먼저 진행하도록 한다.  스크롤이 되는 채팅창은 ListView 컴포넌트를 사용해서 구현하는데, 아래 그림과 같이 지난 메세지는 화면에 나오지 않고 현재 대화되는 상황만 보이도록 한다. 





리스트뷰(ListView) 에 대해서


그렇다면 리스트뷰는 무엇인가? 리스트뷰는 안드로이드 뷰 그룹 (View Group)의 일종으로, 스크롤이 가능한 아이템들의 리스트를 출력해주는 그룹이다.



아답터(Adaptor) 의 개념


채팅용 리스트뷰를 만들기 위해서는 아답터(Adaptor)의 개념을 이해해야 하는데, 아답터는 크게 두가지 기능을 한다. 리스트뷰에서 보여질 아이템들을 저장하는 데이타 저장소의 역할과, 리스트뷰안에 아이템이 그려질때 이를 렌더링하는 역할을 한다.


add 메서드를 이용하여, 아이템을 추가하고

getItem(int index)를 이용하여, index  번째의 아이템을 리턴하며 (자바의 일반적인 List형과 유사하다)

View getView(int position, xxx )이 중요한데, position 번째의 아이템을 화면에 출력할때 렌더링하는 역할을 한다.


그러면 실제로 작동하는 코드를 만들어보자. 이 예제에서는 텍스트를 입력하면 리스트 뷰에 추가되고, 텍스트가 입력됨에 따라 쭈욱 아래로 리스트뷰가 자동으로 스크롤되는 예제이다. 





아답터 클래스 구현


제일 먼저 채팅 메세지 리스트를 저장할 아답터 클래스를 구현해보자


package com.example.terry.simplelistview;


import android.content.Context;

import android.graphics.Color;

import android.view.LayoutInflater;

import android.view.View;

import android.view.ViewGroup;

import android.widget.ArrayAdapter;

import android.widget.TextView;


import java.util.ArrayList;

import java.util.List;


/**

 * Created by terry on 2015. 10. 7..

 */

public class ChatMessageAdapter extends ArrayAdapter {


    List msgs = new ArrayList();


    public ChatMessageAdapter(Context context, int textViewResourceId) {

        super(context, textViewResourceId);

    }


    //@Override

    public void add(ChatMessage object){

        msgs.add(object);

        super.add(object);

    }


    @Override

    public int getCount() {

        return msgs.size();

    }


    @Override

    public ChatMessage getItem(int index) {

        return (ChatMessage) msgs.get(index);

    }


    @Override

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

        View row = convertView;

        if (row == null) {

            // inflator를 생성하여, chatting_message.xml을 읽어서 View객체로 생성한다.

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

            row = inflater.inflate(R.layout.chatting_message, parent, false);

        }


        // Array List에 들어 있는 채팅 문자열을 읽어

        ChatMessage msg = (ChatMessage) msgs.get(position);


        // Inflater를 이용해서 생성한 View에, ChatMessage를 삽입한다.

        TextView msgText = (TextView) row.findViewById(R.id.chatmessage);

        msgText.setText(msg.getMessage());

        msgText.setTextColor(Color.parseColor("#000000"));


        return row;


    }

}


add나 getItem,getCount등은 메세지를 Java List에 저장하고, n 번째 메세지를 리턴하거나 전체 크기를 저장하는 방식으로 구현한다.


가장 중요한 부분은 getView 메서드인데,리스트의 n 번째 아이템에 대한 내용을 화면에 출력하는 역할을 하며 여기서는 두 단계를 거쳐서 렌더링을 진행한다.


첫번째로는 인플레이터 (inflator)를 사용하여, 아이템을 렌더링할 View 컴포넌트를 ~/layout/XX.XML 에서 읽어오는 역할을 한다.

인플레이터에 대해서 간략하게 짚고 넘어가면, View등 화면등을 디자인할 때 안드로이드에서는 XML 을 사용하여 쉽게 View를 정의할 수 있다. 이렇게 정의된 XML을 실제 View 자바 Object로 생성을 해주는 것이 인플레이터(inflator)이다. 


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


를 통해서 인플레이터를 생성하고, 이 인플레이터를 통해서 각 아이템을 출력해줄 View를 생성하여 row라는 변수에 저장한다.


 row = inflater.inflate(R.layout.chatting_message, parent, false);


이때, 레이아웃을 정의한 XML 파일은 chatting_message.xml 로 안드로이드 프로젝트의 ~/layout 디렉토리 아래에 있으며, 인플레이트 할때는 R.layout.chatting_message라는 이름으로 지칭한다.


chatting_message.xml 의 내용은 다음과 같다.


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

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

    android:orientation="vertical" android:layout_width="match_parent"

    android:layout_height="match_parent">


    <TextView

        android:layout_width="match_parent"

        android:layout_height="match_parent"

        android:textAppearance="?android:attr/textAppearanceMedium"

        android:text="Chat message"

        android:id="@+id/chatmessage"

        android:gravity="left|center_vertical|center_horizontal"

        android:layout_marginLeft="20dp" />

</LinearLayout>



이렇게 아이템을 표시할 View가 생성되었으면, 그 안에 알맹이를 채워넣어야 하는데, 채팅 메세지를 저장하는 List 객체에서, position 번째의 채팅 메세지를 읽어온 후에, row 뷰 안에 있는 TextView에 그 채팅 메세지를 채워 넣는다. 


getView 코드에서 주의해서 봐야할 부분이


  View row = convertView;

        if (row == null) { …


인데, 가만히 보면 row가 null 일 경우에만 인플레이터를 이용해서 row를 생성하는 것을 볼 수 있다.


ListView의 특징중 하나는, 아이템을 랜더링 하는 View 객체가 매번 생성되는 것이 아니라, 해당 아이템에 대해서 이미 생성되어 있는 View가 있다면, getView( .. ,View convertView, ..) 를 통해서 인자로 전달된다.


그래서, convertView가 null 인지를 체크하고, 만약에 null이 아닌 경우에만 View를 생성한다. 


여기까지가 채팅 메세지를 저장하고, 각 메세지를 렌더링 해주는 ListView 용 아답터의 구현이었다. 

그러면 아답터를 이용한 ListView를 사용하여 채팅 메세지를 출력하는 부분을 구현해보자


아래는 ListView를 안고 있는 MainActivity이다.


package com.example.terry.simplelistview;


import android.database.DataSetObserver;

import android.support.v7.app.ActionBarActivity;

import android.os.Bundle;

import android.view.Menu;

import android.view.MenuItem;

import android.view.View;

import android.widget.AbsListView;

import android.widget.EditText;

import android.widget.ListView;



public class MainActivity extends ActionBarActivity {

    ChatMessageAdapter chatMessageAdapter;


    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

    }


    @Override

    public boolean onCreateOptionsMenu(Menu menu) {


        // Inflate the menu; this adds items to the action bar if it is present.

        getMenuInflater().inflate(R.menu.menu_main, menu);


        chatMessageAdapter = new ChatMessageAdapter(this.getApplicationContext(),R.layout.chatting_message);

        final ListView listView = (ListView)findViewById(R.id.listView);

        listView.setAdapter(chatMessageAdapter);

        listView.setTranscriptMode(ListView.TRANSCRIPT_MODE_ALWAYS_SCROLL); // 이게 필수


        // When message is added, it makes listview to scroll last message

        chatMessageAdapter.registerDataSetObserver(new DataSetObserver() {

            @Override

            public void onChanged() {

                super.onChanged();

                listView.setSelection(chatMessageAdapter.getCount()-1);

            }

        });

        return true;

    }


    @Override

    public boolean onOptionsItemSelected(MenuItem item) {

        // Handle action bar item clicks here. The action bar will

        // automatically handle clicks on the Home/Up button, so long

        // as you specify a parent activity in AndroidManifest.xml.

        int id = item.getItemId();


        //noinspection SimplifiableIfStatement

        if (id == R.id.action_settings) {

            return true;

        }


        return super.onOptionsItemSelected(item);

    }


    public void send(View view){

        EditText etMsg = (EditText)findViewById(R.id.etMessage);

        String strMsg = (String)etMsg.getText().toString();

        chatMessageAdapter.add(new ChatMessage(strMsg));

    }

}


먼저 send는 “SEND” 버튼을 눌렀을때, 화면상에서 채팅 메세지를 읽어드려서 Adapter에 저장하는 역할을 한다.

가장 중요한 메서드는  onCreateOptionsMenu인데, 이 메서드의 주요 내용은, Adapter를 생성하여, Adapter를 listView에 바인딩한다.


  chatMessageAdapter = new ChatMessageAdapter(this.getApplicationContext(),R.layout.chatting_message);

        final ListView listView = (ListView)findViewById(R.id.listView);

        listView.setAdapter(chatMessageAdapter);


다음 기능으로는, 새로운 아이템이 listView에 추가되었을때, 맨 아래로 화면을 스크롤 하는 기능인데, 이는 Adapter에 DataObserver를 바인딩해서, 데이타가 바뀌었을때 즉 채팅 메세지가 추가되었을때, listView에서 리스트업 되는 아이템 목록을 맨 아래로 이동 시킨다.


        // When message is added, it makes listview to scroll last message

        chatMessageAdapter.registerDataSetObserver(new DataSetObserver() {

            @Override

            public void onChanged() {

                super.onChanged();

                listView.setSelection(chatMessageAdapter.getCount()-1);

            }

        });


이렇게 DataObserver를 추가하더라도, 아래로 스크롤이 안되는데, 새로운 아이템이 리스트뷰에 추가되었을 때 스크롤을 하게 해줄려면 TranscriptMode에 새로운 아이템이 추가되었을때 스크롤이 되도록 설정을 해줘야 한다. (디폴트는 새로운 아이템이 추가되더라도 스크롤이 되지 않는 옵션으로 설정되어 있다.)


listView.setTranscriptMode(ListView.TRANSCRIPT_MODE_ALWAYS_SCROLL); // 이게 필수



간단하게, ListView를 이용한 채팅 UI를 만들어봤다. 다음에는 나인패치 이미지를 이용하여, 말풍선을 넣는 기능을 추가해보도록 한다.


참고 : http://javapapers.com/android/android-chat-bubble/



SharedPreference를 이용한 데이타 저장 

및 애플리케이션간 데이타 공유


조대협 (http://bcho.tistory.com)



안드로이드에서는 데이타를 저장할때 여러가지 방법을 사용할 수 있다.

대표적인 방법으로는 일반적으로 파일을 생성해서 저장하는 방법, 그리고 안드로이드에 내장된 데이타 베이스인 SQLite 데이타 베이스를 활용하는 방법이외에 SharedPreference라는 클래스를 사용하여 데이타를 저장하는 방법이 있다.


SharedPreference


SharedPreference는 간단하게 Key/Value 형태로 데이타를 저장할 수 있는 데이타 구조로 내부적으로는 XML 파일로 저장이 된다.


사용법이 매우 간단해서 일반적인 설정값이나 세션 정보, 사용자 정보와 같은 값들을 저장하거나 주로 복잡하지 않고 경량의 데이타를 저장하는데 쉽게 사용할 수 있다. 단 해당 정보는 안드로이드 파일 시스템 내에 XML 파일로 접근이 되기 때문에, 보안적으로 안전하지 않을 수 있기 때문에, 노출등을 대비해서 중요한 정보는 저장하지 않거나 또는 최소한의 암호화를 해서 저장하는 게 어떨까 하는 생각이다. 


실제로 사용하는 방법은 Activity나 Service  클래스에서 Context를 가지고 온후, 해당 Context를 통하여 SharedPreference를 생성하여, 데이타를 저장하면 된다.


다음은 SharedPreference를 write 하는 예제이다



    public void writeSharedPreference(View view){


        EditText txtValue = (EditText) findViewById(R.id.txtValue);

        String value = txtValue.getText().toString();



        // 1. get Shared Preference

        SharedPreferences sharedPreference

                = this.getSharedPreferences("MYPREFRENCE", Context.MODE_MULTI_PROCESS | Context.MODE_WORLD_READABLE); 


        // 2. get Editor

        SharedPreferences.Editor editor = sharedPreference.edit();


        // 3. set Key values

        editor.putString("MYKEY",value);

        editor.putString("KEY2", "VALUE2");


        // 4. commit the values

        editor.commit();

    }


먼저 위의 1단계에서 getSharedPreference를 이용하여, 애플리케이션 컨텍스트로부터 SharedPreference를 가지고 온다. 이때 Preference 의 이름을 지정할 수 있는데, 여기서는 “MYPREFRENCE”라는 이름으로 Preference를 생성하였다. 그리고 다음으로는 access mode를 설정하였는데, 이 예제에서 설정한 access mode는 여러 프로세스간 공유가 가능하고, 읽을 수 있는 모드로 설정하였다. (뒤에서 다른 애플리케이션에서 이 Preference를 열기 위해서 권한을 부여하였다)


잠깐 access_mode에 대해서 짚고 넘어가면

보통은 MODE_PRIVATE을 사용하는데, 이 설정은 다른 애플리케이션은 접근이 불가하고, 오직 이 SharedPreference를 만든 애플리케이션만 접근이 가능하게 한다. MODE_WORLD_READABLE은 다른 애플리케이션도 읽기를 허용하는 권한이고, MODE_WORLD_WRITABLE은 다른 애플리케이션도 쓰기를 허용하는 권한이다.

조금더 자세한 권한에 대한 설명은 http://www.tutorialspoint.com/android/android_shared_preferences.htm 를 참고하기 바란다.



애플리케이션을 실행하면 텍스트 문자열을 입력 받아서 SharedPreference를 저장하게된다.

이때 바로 저장하는 것이 아니라 SharedPreference로 부터 Editor라는 객체를 받아온 후, setXXX메서드를 이용하여 데이타를 셋한후, 저장을 할때는 반드시 editor.commit()을 하여 저장한다.


주) 일종의 트렌젝션과 같은 기능을 제공하는데, 추측하건데 동시에 여러 애플리케이션이 접근하거나 여러 쓰레드가 접근할때 데이타가 틀어지는 것을 막기 위해서 단일 트렌젝션의 기능을 제공하는 걸로 이해 된다.




<그림 1. > 문자열을 입력받아 SharedPreference에 저장하는 앱


실제로 저장이 되었는지 확인을 하려면 Android Device Monitor 라는 도구를 이용하여, 파일 시스템을 살펴보면 생성된 SharedPreference 파일을 찾을 수 있다.


파일은 /data/data/PACKAGE_NAME/shared_prefs 에 저장된다.



<그림 2> Android Device Monitor


실제로 파일을 열어보면 다음과 같이 입력한 값이 XML 형태로 저장됨을 확인할 수 있다.



<그림 3> MYPREFRENCE.xml


SharedPreference를 통하여 다른 애플리케이션과 데이타를 공유하기


SharedPreference는 생성할때 다른 애플리케이션의 접근을 허용하여 생성할 경우, 다른 애플리케이션이 그 데이타를 액세스 할 수 있다. 즉 SharedPreference 를 이용해서 애플리케이션간에 데이타를 공유하는 것이 가능하다.


다음은, 타 애플리케이션의 SharedPreference로 부터 데이타를 읽어와서 EditText에 출력하는 예제의 실행예와 코드이다.




    public void readPreference(View view) throws Exception {



        // get Context of other application

        Context otherAppContext = null;

        try{

            otherAppContext =

                    createPackageContext("com.example.terry.sharedpreferencewriter",0);

        }catch(PackageManager.NameNotFoundException e){

            // log

            e.printStackTrace();

            throw e;

        }


        // getting Shared preference from other application

        SharedPreferences pref

                = otherAppContext.getSharedPreferences("MYPREFRENCE", Context.MODE_WORLD_READABLE | Context.MODE_MULTI_PROCESS);

        String value = pref.getString("MYKEY", "NOTFOUND");


        EditText txtValue = (EditText)findViewById(R.id.txtValue);

        txtValue.setText(value);


    }


먼저 createPcakgeContext라는 메서드를 이용하여 다른 애플리케이션인 “com.example.terry.sharedpreferencewriter” 에 대한 애플리케이션 Context를 읽어온다.

다음으로는 얻어온 Context로 부터 getSharedPreferences 메서드를 읽어온후에, 해당 SharedPreference로 부터 getString 메서드를 이용하여 “MYKEY” 라는 키로 저장된 값을 읽어와서 출력한다.





드로이드에 애플리케이션  컴파일,설치해서 실행하기 


(에뮬레이터 실행 포함)


조대협 (http://bcho.tistory.com)


다음은 개발된 애플리케이션을 단말이나 또는 에뮬레이터에서 실행하는 방법이다.

이 방법은 안드로이드 스튜디오 1.2.2 을 맥북에 설치한 기준으로 설명한다.

안드로이드 스튜디오 IDE가 좋아서, 필요는 없겠지만. 몰 하던간에 IDE 없이도 할 수 있는 방법이 있어야 하기에 테스트 해놓은 내용을 기록해놓는다.


1. 애플리케이션 컴파일 하기


디렉토리에서 ./gradlew assembleDebug

명령을 실행하면, 디버깅 모드로 컴파일이 되고 apk 파일이 생성된다.



위와 같은 에러가 발생할 수 있는데, 이 경우는 ANDROID_HOME이 설정되지 않은 경우이다.

~/bash_profile에

export ANDROID_HOME=~/Library/Android/sdk 으로 설정하면 된다.



2. 애플리케이션을 디바이스에 설치하기 


안드로이드 SDK가 설치되는 곳은 /User/{username}/Library/Android/sdk

안드로이드의 필수적인 adb는 /Users/{username}/Library/Android/sdk/platform-tools

에 있는데 Path가 걸려있지 않아서, 다른 디렉토리에서 실행이 되지 않는다.


~/.bash_profile 파일에서 PATH에 위의 platform-tools 디렉토리 경로를 추가해주면 된다.


adb를 사용할 수 있는 상태가 되면, apk 파일이 있는 위치에서 다음과 같이 adb install 명령으로 설치한다.


ChoByungwookui-MacBook-Pro:apk terry$ adb install app-debug.apk


8/17 추가. adb install 시 adb install -r 을 하면, 기존에 파일이 있으면 replace하게 된다.


3. 에뮬레이터 띄우기


  실제로 디바이스 없는 경우에는 소프트웨어 기반의 버추얼 디바이스를 사용하면된다. (에뮬레이터)

에뮬레이터는 

%/Users/{username}/Library/Android/sdk/android avd

로 실행하면 된다.



4. 애플리케이션 실행하기


에뮬레이터를 띄워서 adb를 이용해 애플리케이션을 설치했으면, 실행을 해야 하는데, 직접 에뮬레이터에 들어가서 실행하는 방법도 있지만 adb shell을 이용하면 조금 더 쉽게 커맨드라인에서 실행을 해볼 수 있다.


adb shell am start 패키지명/패키지명.액티버티클래스명

예) adb shell am start -n com.example.terry.basicintentsample/com.example.terry.basicintentsample.MyActivity



안드로이드 리소스 파일 개요


조대협 (http://bcho.tistory.com)


안드로이드 프로젝트에서 소스 코드 이외에 이미지나 디자인 레이아웃등의 기타 리소스등을 코드와 분리해서 취급한다.


이렇게 리소스를 코드와 분리해서 사용하는 이유는, 조금 더 유연하게 애플리케이션을 개발하게 하기 위함이다. string과 같은 문자열을 별도의 리소스 파일에서 관리함으로써, 애플리케이션 수정 없이 string 리소스 파일에만 문자열을 추가함으로 간단하게 다국어를 지원할 수 있다.


리소스는 크게 7가지 타입의 리소스가 있고, “/res” 디렉토리에 저장된다. 



Layout Resource (뷰 리소스)

애플리케이션 UI의 배치 에 대한 내용을 XML 형태로 정의하여 저장한다. res/layout 아래와 XML 형태로 저장되며, R.layout으로 접근이 가능하다.


Animation Resource (애니메이션 리소스)

안드로이드에서 애니메이션 기능을 구현하는데 사용되는 설정들이 저장된다. res/anim에 XML로 정의되며 R.anim 이라는 전역 변수를 통해서 접근이 가능하다.


Drawable Resource (그래픽 리소스)

res/drawable 디렉토리에 저장되는 gif,png와 같은 이미지나,  기타 화면에 그려지는 그래픽 관련 리소스들을 저장한다. 이미지 뿐만 아니라, 레이아웃 리스트등의 XML 등 여러 관련 리소스를 저장 하며 R.drawable 을 통해서 접근이 가능하다.


Color State List Resource (칼라 리소스)

res/color 디렉토리 안에 XML 형태로 저장되며, R.color 를 통해서 접근이 가능하다. 버튼이나 글자등의 색을 정의한다.


Menu Resource (메뉴 리소스)

res/menu 디렉토리 내에 XML 형태로 저장되며, Popup 이나 Option과 같은 메뉴를 정의하는 리소스들이 저장된다. R.menu를 이용하여 접근이 가능하다.


String Resource (문자열 리소스)

res/values에 XML 형태로 저장된다. 



위에서 간략하게 정리했지만, 각 리소스안에는 더 세부적인 설정들이 여러 파일로 저장된다. 조금 더 자세한 내용은 http://developer.android.com/guide/topics/resources/available-resources.html 를 참고하기 바란다.


안드로이드 인테트를 통해서 액티버티 생성하기


조대협(http://bcho.tistory.com)


요즘 일이 바쁘고 하는 역할도 바뀌어서 기술보다는 관리나 스크럼에 대한 글들을 읽고 공부하다보니, 반대적으로 기술에 대한 포스팅이 줄었다. 이래서는 안되겠다 싶어서, 어떤 부분을 봐야 할까 고민하다 보니 빅데이타와 앱 개발 기술쪽을 봐야겠다가 시작은 했는데, 막상 진도는 잘 안 나가고 있어서. 앱팀을 운영하고 있으니, 말이라도 통할려면 기본이라도 알아야 겠다 해서 안드로이드 앱쪽을 공부하고 있는데, 일단 해놓은 내용이라고 까먹지 않으려면 정리해놔야겠다는 생각에 간단하게 포스팅한다.


본 문서의 내용은 https://developer.android.com/training/index.html 를 기반으로 작성하였다. (문서에 있는데로 하면 생각보다 안되느 부분이 많다. 문서를 업데이트 하지 않아서 그런것 같은데). 

해당 프로그램은 화면에서 문자열을 입력 받아서, SEND라는 버튼을 누르면 다른 화면에서 입력 받은 문자열을 다른 화면에서 보여주는 예제이다 



MyActivity라는 액티비트에서 텍스트 박스를 통해서 문자열을 입력받고, SEND 버튼을 누르면, 텍스트 박스내의 문자열을 읽어서 Intent로 만들어서 DisplayActivity라는 액티비티로 전달하면 해당 문자열을 출력하는 내용이다.



먼저 인텐트의 개념에 대해서 이해할 필요가 있는데, 인텐트는 서로 다른 컴포넌트 간에 이벤트를 전달할 수 있는 개념이다. 위의 그림에서와 같이 MyActivity에서, DisplayActicity로 액티비티간에 출력할 문자열을 보내는것과 같은 이벤트이다. 유닉스의 IPC(Inter process call)과 같은 개념으로 생각을 하면된다. 

인텐트에 대한 자세한 내용은 https://developer.android.com/guide/components/intents-filters.html 를 참고하기 바라며, 나중에 다시 다루도록한다.


먼저, 문자열을 입력받을 화면을 작성한다. 안드로이드 스튜디오에서, 다음과 같은 레이아웃으로 작성을 한다. 



다음으로 버튼을 눌렀을때, 인텐트를 만들어서 보내기 위해서 버튼을 눌렀을때 실행되는 메서드를 정의하여 버튼 이벤트와 연결한다.

    public void sendMessage(View view){


        EditText editText = (EditText)findViewById(R.id.editText);

        String message = editText.getText().toString();


        Intent intent = new Intent(this,DisplayMessageActivity.class);

        intent.putExtra("com.example.terry.basicintentsample.MESSAGE",message);

        startActivity(intent);

    }


이 메서드는 새로운 인텐트를 생성하여 DisplayActivity라는 액티비티 클래스로 보내지는 인텐트가 된다.

인텐트를 통해서 보내지는 데이타는 “com.example.terry.basicintentsample.MESSAGE” 문자열을 키로 해서, EditText 텍스트 문자열 박스에서 읽어도 문자열을 값으로 해서 putExtra메서드를 이용해서 인텐트에 바인딩 된다. 

이 인텐트는 startActivity를 통해서 DisplayActivity 액티비티를 새롭게 시작하면서 같이 전달이 된다. 

위와 같이 코드를 작성하여,액티버티 뷰를 정의한 activity_my.xml에서 다음과 같이 버튼이 눌렀을때 작성한 “sendMessage”메서드가 호출되도록 정의 한다.

        <Button

            android:layout_width="wrap_content"

            android:layout_height="wrap_content"

            android:text="SEND"

            android:id="@+id/button"

            android:layout_weight="1"

            android:onClick="sendMessage" /


인텐트를 만들어서 보내는 부분이 끝났으면, 이제 인텐트를 받아서 출력하는 부분을 구현해보도록 하자.

안드로이드 스튜디오에서,  새로운 액티비티를 생성한다.




생성된 액티비티에는 간단하게, “myMessage”라는 아이디로 TextView 하나만 배치 시킨다.

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

    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"

    android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"

    android:paddingRight="@dimen/activity_horizontal_margin"

    android:paddingTop="@dimen/activity_vertical_margin"

    android:paddingBottom="@dimen/activity_vertical_margin"

    tools:context="com.example.terry.basicintentsample.DisplayActivity">


    <TextView android:text="@string/hello_world" android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:id="@+id/myMessage" />


</RelativeLayout>


다음으로 OnCreate 메세지에 다음과 같은 코드를 추가한다.

    @Override 

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);


        // Render View first

        setContentView(R.layout.activity_display);


        // Get the message from the intent

        Intent intent = getIntent();

        String message = intent.getStringExtra("com.example.terry.basicintentsample.MESSAGE");

        TextView textView = (TextView)findViewById(R.id.myMessage);

        textView.setText(message);


    }

위의 코드는 인텐트에서 문자열을 받아서 “myMessage”라는 id의 텍스트 뷰에 그 문자열을 출력하는 코드이다.

출력전에, setContentView를 먼저 호출해야 하는데, View를 렌더링한 후에 myMessage TextView가 생성된다. 

다음으로 getIntent() 메서드를 이용하면, 인텐트를 받을 수 있고, getStringExtra메서드를 이용해서 “com.example.terry.basicintentsample.MESSAGE” 키로 문자열을 받아서 TextView에 setText로 세팅한다.





Android View와 ViewGroup의 개념



View와 ViewGroup 객체를 이용해서 만듬.

View는 버튼이나 텍스트필드와 같은  UI Widget 이다. 

ViewGroup은 여러개의 View 또는 ViewGroup을 포함할 수 있는 컨테이너의 개념이며, ViewGroup을 상속하여 화면 배치 속성을 갖는 Layout을 가지고 있는 ViewGroup이 있다. ex)LinearLayout 등






<그림. ViewGroup과  View의 상하 관계>

출처 : https://developer.android.com/training/basics/firstapp/building-ui.html


이 ViewGroup은 res/layout/*.xml 에 XML 형태로 정의할 수 있다.

다음은 LearLayout 을 선언한 예이다.

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

    xmlns:tools="http://schemas.android.com/tools"

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    android:orientation="horizontal" >

</LinearLayout>

View의 경우에는 보통 다음과 같은 형태로 표현되는데, 

<TextView

    android:layout_width="wrap_content"

    android:layout_height="wrap_content"

    android:textAppearance="?android:attr/textAppearanceMedium"

    android:text="Medium Text"

    android:id="@+id/textView2" />

해당 뷰 컴포넌트를 식별하기 위한 id, 그리고 사이드를 정의하기 위한 layout_width,layout_height으로 정의한다. 위에서는 wrap_content로 정의했는데, 절대 크기가 아니라 화면에 나타난 뷰 크기만큼 맞추겠다는 정의이다. (안드로이드 디바이스는 해상도가 다양하기 때문에 절대 크기 보다는 위와 같은 상태 크기를 사용할 수 있도록 지원한다.)





안드로이드 Activity Lifecycle (액티버티 생명주기)와

, Back Stack & Task


조대협 (http://bcho.tistory.com)


Back Stack

Activity의 라이프 사이클 개념을 이해하기 전에, 먼저 BackStack이라는 개념을 이해해야 한다.
안드로이드 애플리케이션은 모바일 애플리케이션의 특성상 하나의 화면, 즉 Activity만 화면에 활성화가 되게 된다. 활성화 된 화면에서 작업하다가, Back 버튼등을 눌러서 이전화면으로 돌아갈 수 있는데, 이런 돌아가는 구조를 지원하기 위해서, 내부적으로 화면이 전환이 되더라도 기존의 화면을 없애는것이 아니라 메모리에 저장해놨다가, 돌아갈때 저장된 화면을 띄워 주는 구조가 된다
이런 구조를 구현하기 위해서 내부적으로 BackStack이라는 구조를 사용하는데, 각각의 화면이 Activity이기 때문에, 화면이 전환될때 마다, 그 Activity를  BackStack안에 저장한다. 그리고, Back 버튼등으로 돌아기기를 할 경우에는 Stack에서 하나씩 꺼내서 이전 화면으로 전환을 한다

아래 그림을 보자, 
  • 처음에 Activity 1 화면이 활성화 되었다.
  • 다음에 Activity 2가 실행되면, Activity 1이 BackStack에 저장된다.
  • 그 다음에 Activity 3이 실행되면, Activity 2가 BackStack에 추가적으로 저장되는 것을 볼 수 있다.
  • 이때 Activity 3가 Back 버튼등에 의해서 종료되면, Activity 3는 Destroy 되고, Back Stack에 있던 Activity2 가 활성화 된다.





Task

A task is a collection of activities that users interact with when performing a certain job. The activities are arranged in a stack (the back stack), in the order in which each activity is opened.

BackStack의 개념을 대략적으로 이해한후에, Task라는 개념을 이해하는게 좀 어려웠는데, Task하는 하나의 BackStack이라고 생각하면 된다. 그런데 왜 굳이 이 Task라는 개념을 사용하느냐? 아직은 잘 모르겠지만, 글들을 읽어보면 일단 키 포인트는 안드로이드 애플리케이션의 Activity는 서로 다른 application간에 공유가 가능하다.
즉, 내가 A라는 애플리케이션을 수행하다가, SNS 공유와 같은 기능을 사용할때, SNS 애플리케이션 B의 Activity들을 불러올 수 있다.  또는 이메일 클라이언트에서, 사진앱의 activity들을 불러서, 첨부 사진을 선택할 수 있다.

그렇다보니, 하나의 Activity는 동시에 여러개의 애플리케이션에 의해서 사용될 수 있기 때문에, 다른 개념을 사용하는데, 이를 Task 라고 한다. 현재 실행중인 애플리케이션의 Activity Stack은 이 Task라는 곳에 저장이 되게 된다.
(일종의 instance 개념과 유사한듯)


Activity의 라이프 사이클

Activity는 사용자와 Interaction이 있는 UI를 처리하는 컴포넌트로 다음과 같은 생명 주기를 갖는다. 각 주기마다 이벤트에 의해서 호출되는 함수들을 정리하면 다음과 같다.





  • OnCreate : 맨 처음 랩이 처음 실행되었을때, 실행되는 메서드로 UI 컴포넌트등을 로딩하고, Listner를 바인딩하고, 초기 데이타를 로딩하는 등의 초기화 작업을 수행한다.
  • OnStart : UI가 화면에 보이기 전에 바로 호출된다. UI가 로딩 된다고 해도, 사용자로 부터 Input을 받을 수 는 없다
  • OnResume : UI 로딩이 끝난후, 사용자 Input (Interaction)이 시작되기 전에 호출된다. 이 함수들이 다 호출되고 나면, 애플리케이션은 실행 가능 상태인 “Activity Running” 상태가 되며, UI도 모두 로딩되고, 사용자로 부터 입력을 받을 준비도 끝난다.
  • OnPause : 이 상태에서 만약에 다른 Activity 화면이 올 경우, OnPause가 호출된다.
    정확한 상태 정의는 “보이기는 하지만 사용자가 Interaction을 할 수 없는 상태” 정도로 정의할 수 있다. 이런 상태가 어떤 상태인가 하면, 다이얼로그등과 같은 다른 액티버티가 앞에 떠서 사용자 Interaction을 수행하는 상태이다 그러나 기존의 Activity는 그대로 뒤에 떠 있지만, 뒤에 떠 있는 activity 는 사용자 Interaction을 받지 못하는 상태이다.
 




이 때 사용중인 쓰레드 정리, 데이타 저장등을 수행하거나 또는 포커스를 잃은 화면이기 때문에 애니메이션등을 정지해야 한다.
  - 이때 다시 해당 Activity로 돌아가게 되면 OnResume으로 다시 돌아가서, 화면을 다시 호출하게 된다.
  - 화면이 보이지 않는 상태에서 만약에 메모리가 부족하게 되면 안드로이드 시스템에 의해서 이 단계에서 자동으로 정지(Killed) 될 수 있다. Killed 된 상태에서 다시 그 화면으로 돌아가게 되면 다시 onCreate로 돌아가서 앱을 처음부터 다시 시작하게 된다.

  • OnStop : 액티버티가 더이상 사용자에게 보여지지 않을때 호출된다. 
  • OnDestroy : 액티버티가 소멸될때 호출된다. 

Activities in the system are managed as an activity stack. When a new activity is started, it is placed on the top of the stack and becomes the running activity -- the previous activity always remains below it in the stack, and will not come to the foreground again until the new activity exits.

An activity has essentially four states:

  • If an activity in the foreground of the screen (at the top of the stack), it is active or running.
  • If an activity has lost focus but is still visible (that is, a new non-full-sized or transparent activity has focus on top of your activity), it ispaused. A paused activity is completely alive (it maintains all state and member information and remains attached to the window manager), but can be killed by the system in extreme low memory situations.
  • If an activity is completely obscured by another activity, it is stopped. It still retains all state and member information, however, it is no longer visible to the user so its window is hidden and it will often be killed by the system when memory is needed elsewhere.
  • If an activity is paused or stopped, the system can drop the activity from memory by either asking it to finish, or simply killing its process. When it is displayed again to the user, it must be completely restarted and restored to its previous state.

각 단계별로 실제로 돌아가는 걸 볼래면, 한번 만들어보고 이벤트 걸고 추적해봐야 겠다.





안드로이드 플랫폼 기본 아키텍쳐


조대협 (http://bcho.tistory.com)


안드로이드 플랫폼의 기반 아키텍쳐를 살펴보면 다음 그림과 같다.




리눅스 커널
일단 가장 아랫단에, Linux 커널 이 올라가 있다. 일반적인 Linux 커널과 크게 다르지는 않지만, 모바일 디바이스에 최적화된 전력 관리 기능이나 안드로이드에 최적화된 Binder IPC (프로세스간 커뮤니케이션) 부분등이 포함되어 있다.

시스템 라이브러리
리눅스 커널위에는 C로 구현된 몇가지 네이티브 라이이브러리들이 올라가 있다. 3차원 그래픽을 위한, OPEN GL, 로컬 데이타 베이스를 제공하는 SQLLite 데이타 베이스, 웹 브라우징을 위한 WebKit, 멀티미디어 재생을 위한 Media Framework들이 올라가 있다. 
이러한 시스템 라이브러리들은 내부적으로 JNI 인터페이스를 통해서 자바 코드로부터 호출되게 된다. 

안드로이드 런타임
이러한 시스템 라이브러리 위에, 안드로이드 런타임이 올라가 있는데, 안드로이드 런타임은 JVM (Java Virtual Machine)이다. 단, 모바일 애플리케이션을 위해서 최적화된 JVM으로 안드로이드는 달빅(Dalvik)이라는 이름의 VM이 올라간다. 이 달빅 JVM이 실제로 안드로이드 애플리케이션을 시작하게 되낟.   그리고, 그위에 코어 자바라이브러리들이 올라가게 된다. (java.*, javax.* ,org.* ...등)


애플리케이션 프레임웍
안드로이드 런타임 까지 기본 JVM과 자바 라이브러리가 올라갔다면 애플리케이션 개발 프레임웍은 라이브러리이다. 마치 서버 개발에서 자바 위에, JEE 나 스프링,Hibernate와 같은 프레임웍이 있는 것 같이 애플리케이션 개발용 프레임웍이 올라가 있다. 
  • Package manager : 어떤 애플리케이션들이 설치되어 있는지를 관리한다. 
  • Windows manager : 윈도우 화면을 관리 (윈도우란, 영역으로 맨 윗부분의 네비게이션바, 다이얼로그 형식으로 나오는 윈도우등등 모든 윈도우 시스템을 관리하는 부분이다.)
  • View manager : 기본적인 그래픽 컴포넌트를 관리 한다. 라디오 버튼이나, 탭, 버튼등. 
  • Resource manager  : 컴파일이 되지 않는 리소스를 관리한다. 예를 들어 폰 애플리케이션에 같이 패키징된 string, 이미지 파일등을 관리한다. (안드로이드 프로젝트상 main/res 내에 있는 것을 관리하는 듯)
  • Activity manager : 안드로이드의 액티버티를 관리한다. 이 액티버티는 안드로이드 애플리케이션내의 하나의 화면에 해당(?)하는 것으로, 이 액티버터의 생성 및 소멸까지의 라이프 싸이클을 관리한다.
  • Contents provider: 데이타 저장소에 대한 추상화된 계층으로, 이 Contents Provider 계층을 통하여, 데이타를 저장할 수 있고, 이 저장소를 다른 애플리케이션에게 공유하여 애플리케이션 간에 데이타를 공유할 수 도 있다.  
  • Location manager : 위치 관련 서비스 기능을 제공한다.  
  • Notification manager : notification bar에 중요한 이벤트를 보여주는 기능을 제공한다. (푸쉬 시스템도 여기서 관리 하나?)


기본 애플리케이션

그위에, 기본적으로 폰에 프리로드 되어 설치되는 애플리케이션들이 존재한다. 연락처, 메신져, 브라우져, 카메라등의 기본적인 애플리케이션 등이 이에 해당한다. 


안드로이드 프로그래밍 시작하기 (둘러보기)


조대협 (http://bcho.tistory.com)


요즘 서버쪽 기술은 다소 시들해진것도 같고, 하는 일이 모바일 서비스인지라 (얼마전에도 그랬지만..) 알아야할것 같아서, 안드로이드 프로그래밍을 보기 시작했다. 
아무래도 서버 개발자 출신이다 보니 안드로이드 개발이라는 생소한 분야를 이 나이에 공부하면서 정리하는 내용이니 혹여나 내용이 허접하더라도.. 이해해주시기를.. (초보자 시각에서 정리한 내용입니다. )

구글에서 안드로이드 IDE인 안드로이드 스튜디오를 다운받아서 설치하면, 통합 개발 환경을 사용할 수 있다. 이 IDE는 보니, JetBrain의 IntelliJ를 기반으로 만들어진 환경이다. (구글이 이런 투자는 잘한듯..)
그러다 보니 이클립스를 쓰면서 불편했던 문제는 없이 제법 꽤 완성도가 높은 개발도구로 개발이 가능할 듯 하다.

스튜디오를 다운받아서 실행을 하고, 새로운 프로젝트를 만들면, 특이 한것이 다음과 같이, “Minimum SDK” 버전이라는 것을 고르는게 나온다. 즉, 어느 SDK버전으로 개발을 할것인지를 선택하는 것인데, 




이는 앱의 호환성과 기능 문제와 결부 된다. 너무 낮은 SDK버전으로 개발을 하게 되면 여러 앱을 커버할 수 있는 호환성은 높겠으나, 고급 기능을 사용할 수 없고, 높은 SDK 버전을 사용하게 되면 낮은 단말을 지원할 수 없어서 많은 사용자를 놓칠 수 있다.

위자드 상에서 가이드를 보면, 안드로이드 4.0.3 아이스크림 샌드위치 버전을 권장하는데, 이 버전을 사용하면 현존 단말이 90.4% 가 커버가 가능하다고 나온다.




약 10%가 커버가 안된다는 것인데, 말이 10%이지 100만 배포의 경우에는 10만대, 1000만 배포의 경우에는 100만대의 시장을 잃어버린다는 것이지 어마어마한 규모이다. (근대 2.X 대에서 요즘 앱이 잘돌아가나? 가벼운 앱이면 몰라도, 어느정도 기능이 있으면 어렵지 않을까??)

안드로이드 스튜디오에서 프로젝트를 생성하면 다음과 같은 파일 구조가 나온다. 몇몇 중요한 점만 짚고 넘어가보자




자바 소스 코드들은 main/java 디렉토리에 들어가 있고,
기타 리소스 파일들은 main/res 디렉토리에 들어가 있다. 일반적인 자바 프로젝트와 크게 다르지 않다
빌드 스크립트가 특이하게 gradle로 생성이된다. 예전에는 무엇으로 생성이 되었는지 모르겠지만, maven에 익숙한 나로써는 다소 챌린지이다.
그리고, proguard 관련 설정 파일이 생성되는 것으로 봐서, 기본으로 사용이 가능한거 같은데, proguard는 난독화 (소스 코드를 읽기 어렵게 만들어서 디컴파일시에 소스코드를 봐도 이해 하기가 어렵게 만드는 도구) 기능이 통합 되어 있는 듯하다.

몇몇 눈에 띄는 중요한 디렉토리와 파일들이 있어서 정리를 해보면 다음과 같다.

layout 파일 
모바일 프로그래밍은 GUI가 있는 만큼 역시나 안드로이드도 레이아웃 시스템을 사용한다. 예전 Java Swing이나, 현대 웹 프로그래밍의 BootStrap UI 프로임웍과 크게 다르지 않다. IDE에서 에디팅을 통해서 작성할 수 있기 때문에, 일반 컴포넌트를 사용하는 GUI 배치 구성은 layout이라는 파일에 작성하여 지정을 한다.

string.xml 파일
라벨등에 사용되는 기타 문자열들을 string.xml이라는 파일에 몰아서 저장해놓고 불러 쓸 수 있는데, 이는 Localization(지역화)를 위해서 디자인 되었다. 하나의 파일에 모든 텍스트를 지정해놓고 사용하고 다른 언어를 바꾸게 되면 이 string.xml 파일만 교체하면 되기 때문에, 지역화에 대응하기 좋은 구조이다.




그런데, 하나의 앱으로 여러 언어를 표현할 수 있는 Globalization(세계화)지원은 어떻게 하지?