(원문: http://developer.android.com/guide/components/activities.html)

(위치: Develop > API Guides > App Components > Activities)

(전체 번역 글 목록으로 이동하기)


Activity에 대하여


액티비티(Activity)는 사용자에게 UI가 있는 화면을 제공하는 앱 컴포넌트입니다. 다시 말해서, 폰 다이얼러 화면, 카메라 촬영 화면, 이메일 쓰기 화면, 지도 보기 화면 등과 같이 사용자들이 뭔가 하기 위해 상호작용을 할 수 있는 화면을 제공한다는 것입니다. 각 액티비티는 하나의 윈도우에 UI를 그리며, 그 윈도우가 보통은 화면을 꽉 채우지만, 화면보다 작을수도 있고, 다른 윈도우의 위에 떠 있을 수도 있습니다.

앱은 보통 여러개의 액티비티로 이루어져 있고, 각 액티비티는 서로 느슨한 관계를 갖습니다. 일반적으로, 앱은 하나의 메인(main) 액티비티를 갖고, 그것은 사용자가 앱을 처음 실행했을때 보여지는 액티비티입니다. 각 액티비티는 다른 액티비티를 실행할 수 있습니다. 다른 액티비티가 실행되면 이전의 액티비티는 정지되지만(stopped), 시스템이 "백스택"이라 불리는 스택에 저장해뒀기 때문에 없어지지는 않습니다. 다시 말해서, 시스템은 새로운 액티비티를 시작하면 백스택에 담고 나서 사용자에게 보여줍니다. 백스택은 "후입선출"의 스택 메커니즘을 따르며, 사용자가 뒤로버튼을 누를 경우, 스택의 최상위(top)에 있는 현재 액티비티를 제거(pop and destroy)하고 이전의 액티비티를 시작합니다. 백스택에 대한 더 자세한 내용은 태스크와 백스택에서 학습하실 수 있습니다.

새로운 액티비티가 실행되면서 현재 액티비티가 정지하게 되면, 시스템은 생명주기의 콜백 메소드를 호출함으로써 액티비티의 상태가 변경되었음을 알려줍니다. 이러한 콜백 메소드들은 시스템이 액티비티를 생성하고, 보여주고, 멈추고, 제거하는 등의 상황에 호출되며, 적절히 오버라이드(override)함으로써 각각의 상황에 맞는 동작을 구현할 수 있습니다. 예를 들어, 액티비티는 정지되었을 때 네트웍이나 데이터베이스 관련 객체와 같이 덩치가 큰 객채들을 해제하는 것이 좋고, 액티비티가 다시 화면에 보여질 때 필요한 리소스들을 다시 가져와서 중지되었던 작업들을 다시 실행할 수 있습니다. 이렇게 액티비티가 생성되고 보여지고 멈추고 제거되고 하는 등의 상태변화는 모두 액티비티 생명주기의 일부입니다. 

여기서는 액티비티를 만들고 사용하는 기본적인 방법과 액티비티의 생명주기에 대한 내용, 그리고 다양한 콜백 메소드 구현 방법에 대해 학습할 것입니다.


액티비티 생성하기


액티비티를 생성하기 위해서는 Activity 클래스를 상속받거나, Activity 클래스의 서브클래스를 상속받아서, 생명주기에 따른 콜백 메소드들을 오버라이드하여 원하는대로 구현하면 됩니다. 콜백 메소드들 중 아래 2개의 메소드가 가장 중요합니다.

onCreate()
반드시 구현되어야 하는 콜백 메소드로서, 액티비티가 생성될 때 호출됩니다. 여기서 액티비티에 필요한 요소들을 초기화 및 멤버변수로 저장하고, setContentView()를 호출하여 액티비티의 레이아웃을 지정해야 합니다.

onPause()
사용자와 액티비티의 거리가 한단계 멀어지는 경우에 호출되는 콜백 메소드입니다. 달리 말하면, 액티비티가 화면에 보여지고 있는 상태와 완전히 정지되어 화면에서 사라진 상태의 중간 단계라고 볼 수 있습니다. 이 상태 이후에 액티비티가 종료될 수도 있기 때문에, 여기서는 액티비티가 다시 시작되었을 때 유지되어야 하는 값들이 있을 경우 저장하는 일을 합니다.

위의 두 콜백 메소드 외에도, 유연한 사용자 경험을 제공하고 예상치 못하게 액티비티가 멈추거나 종료되는 상황들에 대응하기 위하여 사용할 수 있는 콜백 메소드들이 더 있습니다. 모든 생명주기 콜백 메소드들에 대한 더 자세한 내용은 아래의 액티비티 생명주기 관리하기 부분에서 학습하실 수 있습니다.


사용자 인터페이스(UI) 구현하기

액티비티의 UI는 View클래스를 상속받은 뷰(view)들의 계층구조에 의해 제공됩니다. 각 뷰는 액티비티의 윈도우 안에서 일부 사각형 영역을 할당 받고, 사용자와 상호작용을 할 수 있습니다. 예를 들어, 액티비티 안에 버튼뷰가 있다면, 그 버튼뷰가 차지한 사각형 영역을 사용자가 터치할 경우 앱이 그에 따른 동작을 수행할 수 있습니다. 

안드로이드는 앱의 레이아웃을 쉽게 구성할 수 있도록 미리 만들어진 뷰들을 제공합니다. "위젯(Widgets)"은 Button, EditText, CheckBox, ImageView 등과 같이 사용자와 상호작용할 수 있는 뷰이고, "레이아웃(Layouts)"은 LinearLayout, GridView, RelativeLayout 등과 같이 자식뷰들을 갖는 뷰로서 ViewGroup 클래스를 상속받아 만들어졌습니다. 그리고 View클래스나 ViewGroup클래스, 또는 위젯이나 레이아웃 등을 상속받아 그것의 서브클래스를 만들어서 사용할 수도 있습니다. 

액티비티의 레이아웃을 정의하는 가장 일반적인 방법은 XML 레이아웃 파일을 만들고 앱의 리소스로 저장하는 것입니다. 이 방법은 앱의 동작을 책임지는 소스코드와 앱의 UI 디자인을 분리시켜 줍니다. 액티비티에서 레이아웃은 보통 setContentView()에 레이아웃의 리소스 ID를 넘겨서 지정하지만, 소스코드에서 생성된 뷰 객체를 setContentView()에 넘기기도 합니다. 

UI를 만드는 방법에 대하여 더 자세한 내용은 사용자 인터페이스 문서에서 학습하실 수 있습니다.


매니페스트에 액티비티 정의하기

시스템이 액티비티를 인식하기 위해서는 매니페스트 파일(AndroidManifest.xml)에 그 액티비티가 정의되어 있어야 합니다. 따라서 아래와 같이 매니페스트 파일의 <application>요소 안에 <activity>요소를 추가합니다.

<manifest ... >
 
<application ... >
     
<activity android:name=".ExampleActivity" />
      ...
 
</application ... >
  ...
</manifest >

<activity>요소에는 액티비티의 이름이나 아이콘, 테마, 스타일 등의 속성을 추가할 수 있습니다. android:name 속성은 필수이며 액티비티의 클래스명을 지정합니다. 이 값은 앱 출시 이후에는 되도록이면 바꾸지 않는 것이 좋습니다. 왜냐하면 바뀌기 전의 액티비티명을 갖는 명시적 인텐트에 의해 실행되는 경우가 만약에라도 있다면 에러가 발생할 것이기 때문입니다. 

<activity>요소 정의하기에서 더 자세한 내용을 학습하실 수 있습니다.


인텐트 필터 사용하기

<activity>요소는 다른 앱의 컴포넌트가 자신을 실행할 수 있도록 하기 위해 <intent-filter>요소를 포함할 수 있습니다.

안드로이드 SDK를 이용하여 새 앱 프로젝트를 생성할 때, 아래와 같이 "main" 액션과 "launcher" 카테고리를 갖는 인텐트 필터가 추가된 액티비티를 자동으로 생성할 수 있습니다.

<activity android:name=".ExampleActivity" android:icon="@drawable/app_icon">
   
<intent-filter>
       
<action android:name="android.intent.action.MAIN" />
       
<category android:name="android.intent.category.LAUNCHER" />
   
</intent-filter>
</activity>

위의 예제에서, <action>요소는 해당 액티비티가 앱의 진입점임을 나타내고, <category>요소는 시스템 런처의 앱 목록에 보여준다는 것을 의미합니다.

내 앱이 다른 앱에 의해 실행되는 것을 원치 않는다면, 인텐트 필터를 추가할 필요가 없으며, 명시적 인텐트를 통해서만 액티비티를 실행하도록 하면 됩니다. 그리고 위와 같이 "main" 액션과 "launcher" 카테고리를 갖는 인텐트 필터는 하나의 앱에 대하여 오직 하나의 액티비티에서만 가질 수 있습니다. 

반대로, 다른 앱으로부터나 내 앱 내에서 암묵적 인텐트로 액티비티를 실행하고자 한다면, 액티비티에 인텐트 필터를 추가해야 합니다. <intent-filter><action>은 필수사항이며, <category><data>는 선택사항입니다. 암묵적 인텐트로 어떤 액티비티가 실행될 것인가를 이러한 요소들로 정해줄 수 있는 것입니다.

액티비티가 인텐트에 어떻게 반응하는지에 대한 더 자세한 내용은 인텐트와 인텐트 필터에서 학습하실 수 있습니다.


액티비티 실행하기


어떤 액티비티를 실행할지에 대한 정보를 가지고 있는 인텐트를 startActivity()에 넘김으로써 액티비티를 실행할 수 있습니다. 인텐트는 명시적으로 실행할 액티비티를 지정할 수도 있고, 암묵적으로 액션타입을 지정하여 시스템이 조건에 부합하는 액티비티들을 찾도록 할 수도 있습니다. 또한 인텐트는 실행될 액티비티에서 필요로 하는 데이터들을 전달하는 역할도 합니다.

다른 앱과 상호작용하는 앱이 아니라면, 아래 예제와 같이 명시적으로 액티비티를 지정하는 명시적 인텐트만 사용해도 될 것입니다.

Intent intent = new Intent(this, SignInActivity.class);
startActivity
(intent);

하지만, 이메일을 쓰거나 메시지를 보내거나 SNS앱의 내 상태를 업데이트 하는 등의 일을 하려는데 내 앱에서는 해당 액티비티들을 구현하지 않았다면, 아래 예제와 같은 암묵적 인텐트를 이용해서 디바이스에 설치된 다른 앱의 해당 액티비티를 내 앱의 일부인 것처럼 실행할 수 있습니다. 이것이 인텐트의 매력적인 부분인데, 인텐트에 액션값을 지정하여 액티비티 실행요청을 하면 시스템이 조건에 부합하는 액티비티들을 찾아서, 1개이면 바로 실행하고 2개 이상이면 사용자가 선택할 수 있도록 보여줍니다. 

Intent intent = new Intent(Intent.ACTION_SEND);
intent
.putExtra(Intent.EXTRA_EMAIL, recipientArray);
startActivity
(intent);

위 예제의 인텐트는 이메일앱이나 메신저앱, SNS앱 등의 ACTION_SEND를 허용하는 앱들로 메시지를 전송하기 위해 만든 인텐트이며, EXTRA_EMAIL은 사용자가 이메일앱을 선택한 경우에 수신자 입력란(To.)에 미리 채워줄 수신자 이메일 주소들입니다. 위의 인텐트로 액티비티가 실행되었다가 할일을 모두 마치고 액티비티가 종료되면 바로 이전의 액티비티(새 액티비티를 실행했던 액티비티)가 다시 시작됩니다.


액티비티를 실행하여 결과값 얻기

때로는 액티비티를 실행하여 그로부터 어떤 결과값을 얻어야 하는 경우가 있습니다. 예를 들면, 내 사진 중 하나를 업로드하기 위해 갤러리앱에서 사진을 선택하는 경우가 있을 수 있겠죠. 이때는 액티비티 실행을 위해 startActivityForResult()를 실행하고, 실행된 액티비티에서 결과값 셋팅 후 종료되면, 이전 액티비티의 onActivityResult()에서 해당 결과값을 전달받아 관련 작업을 할 수가 있습니다.

아래 예제는, 내 액티비티가 주소록의 사람 목록 액티비티로부터 누군가의 정보를 가져와서 원하는 작업을 하는 코드 일부입니다.

private void pickContact() {
   
// Create an intent to "pick" a contact, as defined by the content provider URI
   
Intent intent = new Intent(Intent.ACTION_PICK, Contacts.CONTENT_URI);
    startActivityForResult
(intent, PICK_CONTACT_REQUEST);
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
   
// If the request went well (OK) and the request was PICK_CONTACT_REQUEST
   
if (resultCode == Activity.RESULT_OK && requestCode == PICK_CONTACT_REQUEST) {
       
// Perform a query to the contact's content provider for the contact's name
       
Cursor cursor = getContentResolver().query(data.getData(),
       
new String[] {Contacts.DISPLAY_NAME}, null, null, null);
       
if (cursor.moveToFirst()) { // True if the cursor is not empty
           
int columnIndex = cursor.getColumnIndex(Contacts.DISPLAY_NAME);
           
String name = cursor.getString(columnIndex);
           
// Do something with the selected contact's name...
       
}
   
}
}

위의 예제에서는 onActivityResult()를 어떻게 구현할지에 대한 기본적인 로직을 보여줍니다. 첫째로 요청이 성공했는가, 즉 실행된 액티비티에서 정상적으로 결과값을 셋팅했는지를 확인하기 위해 resultCodeRESULT_OK인지를 비교합니다. 그리고 요청이 여러개일 수 있기 때문에 요청을 구분하기 위해 requestCodestartActivityForResult() 호출할 때 보냈던 값과 같은지를 비교합니다. 그리고나서, 매개변수로 전달받은 Intent객체(data)를 이용하여 선택된 주소록 사용자 정보를 구합니다.

ContentResolver는 사용자 정보를 쿼리하여 Cursor객체를 래턴해줍니다. 더 자세한 내용은 컨텐트 프로바이더에 대하여에서 학습하실 수 있습니다.

그리고 인텐트 사용 방법에 대한 더 자세한 내용은 인텐트와 인텐트 필터에서 학습하실 수 있습니다.


액티비티 종료하기


finish()를 호출함으로써 현재 액티비티를 종료할 수 있고, finishActivity()를 호출함으로써 startActivityForResult()로 호출했던 액티비티를 종료할 수 있습니다. 

메모: 보통은 위의 메소드들을 이용하여 액티비티를 종료할 필요가 없습니다. 시스템이 액티비티의 생명주기를 관리해주기 때문이며, 액티비티를 강제로 종료하는게 오히려 사용자에게 좋지 않은 영향을 미칠 수도 있기 때문입니다. 따라서, 액티비티가 다음 액티비티를 실행하면서 자신은 스택에 아예 남지 않기를 원하는 경우에만 액티비티를 명시적으로 종료하도록 합니다.


액티비티 생명주기 관리하기


액티비티의 콜백 메소드들을 구현함으로써 생명주기를 관리하는 것은 안드로이드 앱을 만드는데에 필수적인 부분입니다. 액티비티의 생명주기는 다른 액티비티들과의 관계, 그리고 그들의 태스크와 백스택에 직접적으로 영향을 받습니다. 

액티비티의 상태에는 아래의 세가지가 있습니다.

Resumed
액티비티가 전면(foreground)에 있고, 사용자가 인터페이스를 사용할 수 있도록 포커스를 가지고 있는 상태입니다. ("running" 상태라고도 합니다.)

Paused
다른 액티비티가 resumed 상태가 되었으나 여전히 이전 액티비티가 일부 보이고 있는 상태입니다. 다시 말해서, 이전 액티비티 위에 다음 액티비티가 보여지고 있지만, 반투명하거나 화면의 일부만 덮고 있는 상태입니다. paused 상태의 액티비티는 resumed 상태와 마찬가지로 메모리에 살아 있으며 여전히 윈도우 매니저와 연결되어 있습니다. 하지만 메모리가 부족한데 다른데에서 끌어쓸 자원이 없다면 시스템에 의해 종료될 수도 있는 상태입니다. 

Stopped
다른 액티비티가 resumed 상태가 되면서 이전 액티비티가 화면에서 완전히 사라져서 후면(background)으로 넘어간 상태입니다. stopped 상태의 액티비티도 paused 상태와 마찬가지로 메모리에 살아는 있으나 윈도우 매니저와의 연결은 끊어진 상태입니다. 따라서 메모리가 부족할 때 시스템에 의해서 종료될 수 있습니다.

만약 액티비티가 pausedstopped 상태가 되면, 시스템은 finish()메소드를 호출하거나, 바로 프로세스를 종료함으로써 액티비티 객체를 메모리에서 날려버릴 수 있습니다. 그럴 경우 액티비티를 다시 실행하면, 당연히도 다시 생성하게 됩니다.


생명주기 콜백 메소드 구현하기

액티비티의 상태들이 변경될 때, 콜백 메소드들이 호출됩니다. Activity클래스를 상속받아 내 액티비티를 만들 때, 모든 콜백 메소드들을 오버라이드(override)하여 구현할 수 있습니다. 아래 예제에는 액티비티의 주요 콜백 메소드들이 포함되어 있습니다.

public class ExampleActivity extends Activity {
   
@Override
   
public void onCreate(Bundle savedInstanceState) {
       
super.onCreate(savedInstanceState);
       
// The activity is being created.
   
}
   
@Override
   
protected void onStart() {
       
super.onStart();
       
// The activity is about to become visible.
   
}
   
@Override
   
protected void onResume() {
       
super.onResume();
       
// The activity has become visible (it is now "resumed").
   
}
   
@Override
   
protected void onPause() {
       
super.onPause();
       
// Another activity is taking focus (this activity is about to be "paused").
   
}
   
@Override
   
protected void onStop() {
       
super.onStop();
       
// The activity is no longer visible (it is now "stopped")
   
}
   
@Override
   
protected void onDestroy() {
       
super.onDestroy();
       
// The activity is about to be destroyed.
   
}
}

메모: 위와 같이 콜백 메소드를 구현할 때는 항상 부모 클래스의 메소드인 super.onXXX()를 호출해줘야 합니다. 

종합적으로 생각해보면, 이러한 콜백 메소드들은 액티비티의 전체 생명주기를 정의합니다. 콜백 메소드들을 구현함으로써 생명주기 안에 있는 세가지 중첩된 반복구간(nested loops)을 관리할 수 있습니다. 그 세가지는 아래와 같습니다.

  • 전체 구간(entire lifetime)onCreate()에서 시작하여 onDestroy()로 끝납니다. onCreate()에서 레이아웃 정의와 같은 액티비티 전체에 대한 준비 작업을 하고, onDestroy()에서 리소스들을 해제합니다. 예를 들어 액티비티에서 네트웍을 통해 필요한 데이터를 받아와야 한다면, 쓰레드를 onCreate()에서 생성하여 멤버 변수로 가지고 있다가 onDestroy()에서 해제하면 될 것입니다.

  • 보이는 구간(visible lifetime)onStart()에서 시작하여 onStop()으로 끝납니다. 이때 사용자는 액티비티를 화면에서 볼 수 있으며 상호작용(interact)할 수 있습니다. 만약 다른 액티비티가 실행되면서 현재 액티비티의 onStop()이 호출되었다면 그 액티비티는 화면에 보이지 않을 것입니다. onStart()와 onStop() 사이에서는 액티비티가 사용자에게 보여지는 동안 필요한 리소스들을 잘 가지고 있어야 합니다. 예를 들면, 브로드캐스트 리시버를 등록하는 작업은 보통 onStart()에서 하며, onStop()에서 등록해제를 합니다. 이것은 브로드캐스트를 받아서 하는 일이 보통 화면 UI를 변경하는 일이기 때문에 보이는 구간에서만 등록되어 있도록 하는 것입니다. 전체 구간 안에서 보이는 구간은 여러번 있을 수 있습니다. 다시 말해서, onCreate()와 onDestroy() 사이에서 onStart()와 onStop()이 여러번 호출될 수 있다는 것입니다.

  • 전면 구간(foreground lifetime)onResume()에서 시작하여 onPause()로 끝납니다. 이때 액티비티는 가장 전면에, 즉 사용자와 가장 가까운 곳에 있고 사용자와 상호작용할 수 있도록 포커스를 갖습니다. onPause()는 디바이스가 슬립모드로 들어가는 경우(화면이 꺼지는 경우)와 다이얼로그가 뜨는 경우 등에 호출됩니다. 이렇듯 액티비티는 전면에 나섰다가 빠지는 경우가 자주 발생하기 때문에, 콜백 메소드에서 시간이 오래 걸리는 작업은 되도록 피해야 합니다.

아래의 그림1은 위의 세가지 구간과 상태들간의 이동흐름을 나타냅니다. 사각형은 상태가 변할때 호출되는 콜백 메소드를 표현합니다.


그림 1. 액티비티 생명주기


아래의 표1에서도 생명주기를 표현하는데, 각 콜백 메소드에 대하여 더 자세히 설명하며, 콜백 메소드가 호출된 후 (다른 콜백 메소드가 호출되기 전) 시스템에 의해 종료될 수 있는지 여부와 다음으로 호출될 콜백 메소드가 무엇인지를 알려줍니다.


표 1. 액티비티 콜백 메소드에 대한 설명

 콜백메소드

 설명

 콜백메소드
 호출후 종료
 가능여부

 다음으로
 호출될
 콜백메소드

 onCreate()

 액티비티가 생성될때 호출되며, 뷰를 생성하거나 리스트에 데이터를 바인드하는 등의 구현을 여기에 하도록 합니다. 매개변수로 Bundle객체를 받는데, 종료될때 액티비티의 상태를 저장했다면(액티비티의 상태 저장하기 참고) 해당 Bundle객체를 통해 그 상태를 복구할 수 있습니다.
onCreate() 다음으로는 항상 onStart()가 호출됩니다.

 불가능

 onStart()

 - onRestart()

 액티비티가 정지되었다가 다시 시작되기 직전에 호출됩니다. 
onRestart() 다음으로는 항상 onStart()가 호출됩니다.

 불가능

 onStart()

 - onStart()

 액티비티가 사용자에게 보여지기 전에 호출됩니다. 
 이어서 액티비티가 전면에 나서며 포커스를 받을때 onResume()이 호출되며, 그렇지 않고 바로 가려지면 onStop()이 호출됩니다.

 불가능

 onResume()
 또는
 onStop()

 - - onResume()

 액티비티가 사용자와 상호작용을 할 수 있는 상태가 되기 직전에 호출됩니다. 이때 액티비티는 백스택의 최상단에 있고, 사용자는 액티비티에 뭔가를 할 수 있는 상태가 됩니다.
 onResume() 다음으로는 항상 onPause()가 호출됩니다.

 불가능

 onPause()

 - - onPause()

 시스템이 다른 액티비티를 resume상태로 만들때 호출됩니다. 여기서는 보통 데이터를 저장하거나 애니메이션을 정지하거나 CPU 사용량을 줄이기 위해 정지시켜도 될만한 객체들의 동작을 정지시키는 등의 구현을 합니다. 다만 onPause()가 완료된 후에야 다른 액티비티가 resume상태가 될 수 있기 때문에 시간이 오래 걸리는 작업은 피해야 합니다.
 이어서 액티비티가 다시 resume상태가 된다면 onResume()이 호출되고, 아예 화면에서 보이지 않게 되면 onStop()이 호출됩니다.

 가능

 onResume()
 또는
 onStop()

 - onStop()

 액티비티가 더이상 사용자에게 보여지지 않을때 호출됩니다. 이러한 경우는 보통 액티비티가 종료되는 상황이거나, 다른 액티비티가 resume상태가 되면서 이전 액티비티를 완전히 가려버리는 상황입니다. 
 이어서 액티비티가 다시 사용자에게 보여지는 상태가 되는 경우 onRestart()가 호출되고, 액티비티가 종료되고 백스택에서도 빠지는 경우 onDestroy()가 호출됩니다.

 가능 

 onRestart()
 또는
 onDestroy()

 onDestroy()

 액티비티가 종료될 때 호출되며, 생명주기에서 마지막으로 호출되는 콜백 메소드입니다. finish()가 호출되었거나, 시스템이 다른 작업을 위한 메모리를 확보하기 위해 임의로 종료시키는 경우에 호출되는데, 이 두가지 경우는 isFinishing()을 통해 구별할 수 있습니다. finish()가 호출된 경우 isFinishing()은 true를 리턴합니다.

 가능

 없음


"콜백 메소드 호출후 종료 가능여부"는 해당 메소드가 리턴된 후 다른 코드의 실행이 없는 상태에서 시스템이 액티비티를 종료할 수 있는지 여부를 나타내며, onPause(), onStop(), onDestroy() 이렇게 세개만이 "가능"합니다. 이 중 onPause()가 가장 먼저 호출되는 메소드이고 여기서 시스템에 의해 종료되면 onStop()onDestroy()가 호출되지 않을 수도 있기 때문에, onPause()에서 중요한 데이터들은 저장해야 합니다. 하지만 onPause()에서 어떤 일을 할지에 대해서는 신중히 결정해야 하는데, 그것은 onPause()에서 시간이 오래 걸리는 작업으로 프로시져를 붙들고 있으면 다음 액티비티의 실행이 늦어질 수 있기 때문입니다.

"콜백 메소드 호출후 종료 가능여부"가 "불가능"인 콜백 메소드들이 호출된 후 다른 메소드가 호출되기 전까지는, 액티비티를 실행중인 프로세스가 시스템의 종료 명령으로부터 보호됩니다. 따라서, 액티비티는 onPause()가 호출된 후부터 다시 onResume()이 호출되기 전까지는 시스템에 의해 종료될 수 있으며, 반대로 onResume()이 호출된 후부터 onPause()가 호출되기 전까지는 종료될 수 없습니다. 

메모: 표1에서 "콜백 메소드 호출후 종료 가능여부"가 "불가능"인 메소드가 호출된 상태라 하더라도 아주 긴급한 상황에서는 시스템에 의해 액티비티가 종료될 수도 있습니다. 액티비티가 종료되는 상황에 대한 더 자세한 내용은 프로세스와 쓰레드에서 학습하실 수 있습니다.


액티비티의 상태 저장하기

액티비티 생명주기 관리하기에서 설명한 바와 같이 액티비티는 paused나 stopped상태에서도 메모리에 액티비티의 상태를 나타내는 데이터들을 가지고 있습니다. 따라서, 다시 resume상태가 되면 paused나 stopped상태가 되기 전과 같은 상태가 되는 것입니다.

하지만 시스템이 메모리 확보를 위해 액티비티를 종료시키는 경우에는 그 액티비티를 다시 실행할 때 이전 상태가 메모리에 남아있지 않기 때문에 복구할 수 없습니다. 이러한 경우, 액티비티가 종료되기 전 호출되는 콜백 메소드인 onSaveInstanceState()에서 유지해야하는 데이터들을 저장해야 액티비티가 다시 생성될 때 저장된 데이터들을 전달받아 이전 상태를 복구할 수 있습니다.

onSaveInstanceState()에서 매개변수로 Bundle객체를 전달받는데, 이 객체의 putString(), putInt() 등의 메소드를 이용하여 이름-값 쌍의 데이터를 저장할 수 있습니다. 그러면 액티비티가 종료되었다가 다시 생성될 때 onCreate()onRestoreInstanceState()에서 매개변수로 받는 Bundle객체에 저장된 데이터들을 꺼내서 액티비티의 이전 상태를 복구할 수 있는 것입니다. 액티비티를 처음 생성하는 경우와 같이 복구할 데이터가 없는 경우에는 Bundle객체가 null로 옵니다.


그림 2. 액티비티가 이전 상태를 복구하는 두가지 경우를 보여줍니다. 하나는 액티비티가 종료되었다가 다시 생성될 때 저장된 데이터를 이용하여 복구하는 경우이고, 다른 하나는 액티비티가 stopped상태가 되었다가 다시 resume상태가 될 때 메모리에 데이터들이 그대로 살아있기 때문에 자연스럽게 이전 상태가 복구된 경우입니다.

메모: 액티비티가 종료되기 전에 onSaveInstanceState()의 호출이 반드시 보장되는 것은 아닙니다. 사용자가 명시적으로 뒤로버튼을 누르는 경우와 같이 액티비티의 이전 상태를 복구할 필요가 없는 경우에는 onSaveInstanceState()가 호출되지 않습니다. 만약 시스템이 onSaveInstanceState()를 호출한다면, 그것은 아마도 onStop()이나 onPause()보다 먼저 호출될 것입니다.


onSaveInstanceState()를 오버라이드하여 추가적인 구현을 하지 않더라도, 기본적으로 몇가지 복구되는 것들이 있습니다. Activity클래스의 onSaveInstanceState()에서는 액티비티의 모든 뷰의 onSaveInstanceState()를 호출하도록 되어 있습니다. 즉, 뷰가 이전 상태를 복구하도록 구현되어 있다면 그 뷰는 액티비티가 종료 및 재생성 될 때 이전 상태로 복구된다는 것입니다. 안드로이드 프레임웍의 거의 모든 위젯들은 이 onSaveInstanceState()가 적절히 구현되어 있습니다. 예를 들면, EditText 위젯은 사용자로부터 입력받은 문자열을 저장하고, CheckBox 위젯은 체크여부를 저장합니다. 다만, 이러한 뷰 및 위젯들을 레이아웃 xml파일에 정의할 때 android:id를 지정해야만 상태를 저장 및 복구하며, android:id를 지정하지 않으면 이전 상태가 복구되지 않습니다.

뷰나 위젯이 이전 상태를 저장 및 복구하지 않도록 하기 위해서는 android:saveEnabled"false"로 선언하거나, setSaveEnabled()메소드를 호출하면 됩니다. 보통은 굳이 이렇게 설정할 필요가 없으나, 액티비티의 UI를 뭔가 다르게 복구되도록 구현하고 싶다면 이용할 수도 있겠죠.

비록 뷰나 위젯이 기본적으로 이전 상태를 복구할 수 있다 하더라도, 원하는 모든게 복구될 수는 없으므로 결국 onSaveInstanceState()를 오버라이드해야 하는 경우가 생깁니다. 예를 들어, 내 액티비티를 만들면서 추가한 멤버변수에 대하여, 액티비티가 실행되는 동안 그 값이 변한다면, 액티비티가 종료 및 재생성될때 그 값을 저장 및 복구해야할 것입니다.

위에서 언급한 바와 같이 onSaveInstanceState()onRestoreInstanceState()는 뷰나 위젯의 상태 복구와 같이 기본적인 기능이 구현되어 있기 때문에, 그것들을 오버라이드할 경우 추가적인 구현부분 전에 반드시 super.onSaveInstanceState()super.onRestoreInstanceState()를 호출해줘야 합니다.

메모: onSaveInstanceState()는 호출이 보장되지 않기 때문에, 즉 호출되지 않는 경우도 있을 수 있기 때문에 액티비티 UI의 상태를 저장하는 용도로만 사용하도록 합니다. 그리고 반드시 저장해야하는 중요 데이터는 onPause()에서 저장하도록 합니다.

액티비티의 이전 상태 저장 및 복구에 대하여 가장 손쉬운 테스트 방법은, 디바이스의 오리엔테이션을 가로(landscape), 세로(portrait)로 번갈아서 변경해보는 것입니다. 이렇게 디바이스의 오리엔테이션이 변경될 경우, 기본적으로 시스템은 액티비티를 종료 및 재생성합니다. 가로 및 세로에 대하여 서로 다른 레이아웃이 적용될 수도 있기 때문이죠. 사용자들은 디바이스의 오리엔테이션을 수시로 변경할 수 있기 때문에 액티비티의 상태를 유지하도록 조치하는 것은 상당히 중요합니다.


디바이스의 상태가 변경될 때

스크린 오리엔테이션이나 키보드의 사용가능 여부, 언어 등의 디바이스 상태는 앱이 실행중일때 변경될 수도 있습니다. 이러한 경우 안드로이드 시스템은 액티비티를 종료 및 재실행하며, onDestroy()onCreate()가 연이어 호출됩니다. 이것은 디바이스의 상태가 변경될 경우 그에 맞는 리소스를 적용하기 위해서입니다. 가로일때와 세로일때 서로 다른 레이아웃 xml파일을 적용해야 하는 경우가 그 예입니다.

내 앱이 스크린 오리엔테이션의 변화에 적절히 대응하도록 개발되었다면, 액티비티의 생명주기상에서 일어날 수 있는 다른 불청객 이벤트에 대해서도 대응할 수 있을 것입니다.

위에서 계속 언급했던바와 같이 액티비티의 종료 및 재생성시 상태를 유지하는 가장 좋은 방법은, onSaveInstanceState()에서 상태를 저장하고, onRestoreInstanceState() 또는 onCreate()에서 상태를 복구하는 것입니다.

실행중에 디바이스의 상태가 변경될 때 그것을 다루는 방법들은 실행 중 상태 저장 및 복구하기에서 학습하실 수 있습니다.


액티비티들 조직화하기

어떤 액티비티가 다른 액티비티를 실행하는 경우, 그 둘 모두 상태의 변화가 발생합니다. 이전 액티비티는 paused 및 stopped상태가 되고(완전히 가려지지 않고 일부가 보인다면 stopped상태가 되지 않습니다), 다음 액티비티는 생성됩니다. 이때, 다음 액티비티가 생성되기 전까지 이전 액티비티가 완전히 정지(stopped)되지 않는다는 것을 기억해야 합니다. 

액티비티의 생명주기 콜백 메소드가 호출되는 순서는 잘 정의되어 있습니다. 아래 예제는 액티비티A가 액티비티B를 실행하는 경우 콜백 메소드가 호출되는 순서를 보여줍니다.

  1. 액티비티A의 onPause()가 호출됩니다.
  2. 액티비티B의 onCreate(), onStart(), onResume()이 차례로 호출됩니다. (이제 액티비티B가 포커스를 가지고 있습니다.)
  3. 액티비티A가 더이상 화면에 보이지 않는다면 onStop()이 호출됩니다.

위와 같은 콜백 메소드의 호출 순서를 잘 숙지하고, 액티비티들 사이의 상태 변화시 서로 공유되는 데이터들을 처리할 때 주의해야 합니다. 예를 들어 액티비티A가 정지될때 데이터를 DB에 저장하고, 액티비티B가 생성될때 그 데이터를 DB에서 불러와야 하는 경우, 액티비티A의 onStop()에서가 아니라 onPause()에서 데이터를 저장해야 한다는 것을 알 수 있습니다.


Posted by 개발자 김태우
,

(원문: http://developer.android.com/guide/components/intents-filters.html)

(위치: Develop > API Guides > Intents and Intent Filters)

(전체 번역 글 목록으로 이동하기)


인텐트와 인텐트 필터


인텐트는 앱의 컴포넌트들 간에 요청 및 메시지를 전달해 주는 객체입니다. 인텐트를 사용하는 방법에는 여러가지가 있지만 대표적인 세가지는 아래와 같습니다.

  • 액티비티 시작하기:
    하나의 액티비티는 내 앱에서 보통 하나의 화면을 구성합니다. 현재 컴포넌트에서 새로운 액티비티를 실행하기 위해서는, 인텐트에 실행할 액티비티 정보와 (필요하다면) 액티비티에 넘겨줄 데이터를 담아서 startActivity() 호출시 인자로 넘겨주면 됩니다.
    현재 액티비티에서 새로운 액티비티를 실행하고 그로부터 결과값을 받아오고 싶다면, startActivityForResult()를 호출합니다. 새로운 액티비티가 결과값을 설정하고 종료되면, 현재 액티비티의 onActivityResult() 콜백 메소드에서 결과값을 인텐트로 받을 수 있습니다. 자세한 내용은 Activity에 대하여에서 학습하실 수 있습니다.

  • 서비스 시작하기:
    서비스는 사용자에게 UI를 제공하지 않고 백그라운드에서 실행되는 컴포넌트입니다. 파일을 다운로드 하는 경우와 같이 백그라운드에서 한가지 일만 하고 종료되게 하려면, 인텐트에 실행할 서비스 정보와 (필요하다면) 넘겨줄 데이터를 담아서 startService() 호출시 인자로 넘겨주면 됩니다.
    만약, 액티비티와 서비스가 클라이언트-서버 구조로 동작하길 원한다면, bindService() 호출시 서비스 및 필요 데이터를 담은 인텐트를 넘겨서, 액티비티에 서비스를 바인드 합니다. 자세한 내용은 Service에 대하여에서 학습하실 수 있습니다.

  • 알림(broadcast) 발송하기:
    알림은 어떤 앱이건 (퍼미션만 가지고 있다면) 받을 수 있는 메시지입니다. 시스템이 부팅되었을 때나 디바이스가 충전되기 시작할 때 등의 상황에, 시스템은 알림 발송을 위해 이벤트를 발생시킵니다. sendBroadcast(), sendOrderedBroadcast(), sendStickyBroadcast() 호출시 필요한 데이터를 담은 인텐트를 인자로 넘기면 됩니다.


인텐트의 타입


인텐트는 아래와 같이 두가지 타입이 있습니다:

  • 명시적 인텐트는 실행할 컴포넌트의 클래스 정보를 명시적으로 가지고 있는 인텐트입니다. 내 앱 안에서 컴포넌트들을 실행할 때는, 컴포넌트들을 다 알고 있기 때문에, 보통 명시적 인텐트를 사용합니다. 

  • 암묵적 인텐트는 실행할 컴포넌트의 클래스 정보 대신 액션값을 선언한 인텐트이며, 다른 앱의 컴포넌트를 실행할 때에도 사용할 수 있습니다. 예를 들어, 내 앱에서 가지고 있는 위치값을 지도 위에서 보고 싶다면, 암묵적 인텐트를 이용하여 지도 기능을 가진 다른 앱의 컴포넌트를 실행할 수 있는 것입니다.

명시적 인텐트를 사용하여 액티비티나 서비스 실행 메소드를 호출하면, 시스템은 즉시 그 인텐트에 지정된 컴포넌트를 실행합니다.

암묵적 인텐트를 사용하면, 시스템은 우선 인텐트에 설정된 액션값 등의 정보와, 다른 앱들의 매니페스트 파일에 선언된 인텐트 필터들을 비교하여 조건에 맞는 컴포넌트를 찾습니다. 그래서 만약 한개의 컴포넌트를 찾았다면 인텐트를 넘겨주며 실행시키고, 두개 이상이라면 사용자가 선택할 수 있도록 화면에 선택 다이얼로그를 보여줍니다.

인텐트 필터는 매니페스트 파일의 컴포넌트 요소 안에 추가되는 요소로서, 컴포넌트가 받고자 하는 인텐트의 액션값 및 데이터 타입 등을 지정합니다. 예를 들면, 액티비티에 인텐트 필터를 선언함으로써, 다른 앱에서 그에 맞는 암묵적 인텐트로 그 액티비티를 실행할 수 있습니다. 반대로 액티비티에 인텐트 필터를 선언하지 않으면, 그 액티비티를 실행할 수 있는 액션값이 없는 것이므로 명시적 인텐트로만 실행할 수 있습니다.

주의사항: 앱의 보안을 위해서, 서비스에는 인텐트 필터를 선언하지 말고 명시적 인텐트로만 실행하도록 합니다. 암묵적 인텐트로 서비스를 실행하는 것은, 그 인텐트에 어떤 서비스가 응답할 것인지를 확신할 수 없고, 어떤 서비스가 실행되는지 사용자가 확인할 수도 없기 때문에 보안적으로 위험합니다.


그림1. 암묵적 인텐트가 어떻게 액티비티를 시작하는지를 표현합니다. 

[1] Activity A에서 인텐트에 액션값을 담아 startActivity() 호출시 인자로 넘깁니다.

[2] Android System이 모든 앱의 인텐트 필터에 대하여, 인텐트의 액션값에 해당하는 것이 있는지 검색합니다.

[3] 검색결과 Activity B를 찾았다면, 시스템은 액티비티 실행을 위해 onCreate() 메소드 호출시 해당 인텐트를 넘겨줍니다.


인텐트 만들기 (Building an Intent)


인텐트 객체는 실행하고자 하는 컴포넌트의 이름이나 카테고리 정보를 전달하는 역할을 하며, 응답하는 컴포넌트에게 액션값과 필요한 데이터들을 전달하기도 합니다.

인텐트가 담고 있는 주요한 정보들은 아래와 같습니다.


컴포넌트 이름 (Component name)

실행할 컴포넌트의 이름입니다.

이것은 명시적 인텐트에 필요한 것으로서, 그 이름에 해당하는 컴포넌트를 실행합니다. 암묵적 인텐트에서는 컴포넌트 이름 대신에 액션(action), 데이터(data), 카테고리(category) 정보를 담고 있습니다(액션은 필수, 데이터와 카테고리는 선택). 보통 앱 내의 컴포넌트를 실행할 때는 컴포넌트 이름을 이용한 명시적 인텐트를 사용합니다. 

주의사항: 서비스는 항상 명시적 인텐트를 사용해야 합니다. 만약 암묵적 인텐트를 사용한다면, 인텐트에 응답하는 서비스가 무엇인지 확신할 수 없고, 사용자들도 어떤 서비스가 실행되는지 볼 수 없기 때문입니다.

컴포넌트 이름은, 패키지명과 클래스명(예를 들면, com.example.ExampleActivity)을 담고 있는 ComponentName 객체로 인텐트에 추가할 수 있으며, setComponent() 메소드를 사용합니다. 그 외에도 인텐트의 setClass()setClassName() 메소드나 생성자에 클래스 정보를 인자로 넘겨서 실행할 컴포넌트를 명시할 수 있습니다.


액션 (Action)

실행하고자 하는 액션을 나타내는 문자열입니다. (예를 들면, view나 pick)

알림(broadcast) 인텐트의 경우, 액션은 리시버가 어떤 일을 할 것인지를 나타냅니다. 액션은 암묵적 인텐트의 주요한 값이 되며, 인텐트는 그 액션에 따라 필요한 데이터 등을 담을 수 있습니다.

내 앱에서 임의로 액션을 추가하여 사용할 수도 있지만, 보통은 Intent 클래스나 다른 프레임웍에 이미 정의되어 있는 값들을 사용하는 것이 좋습니다.

Intent.ACTION_VIEW

실제값은 "android.intent.action.VIEW" 입니다. 사진을 갤러리 앱에서 보거나 어떤 주소에 대한 위치를 지도 앱에서 볼 때와 같이, 어떤 정보를 가지고 있고 그것을 사용자에게 보여주고자 할 때 사용하기 위해 정의된 값입니다.

Intent.ACTION_SEND

실제값은 "android.intent.action.SEND" 입니다. "공유하기" 기능을 구현할 때 사용하는 이 액션값은, 이메일 앱이나 소셜 공유 앱과 같은 다른 앱을 통해 데이터를 공유하고자 할 때 사용하기 위해 정의된 값입니다.

미리 정의된 일반적인 액션값들은 Intent 클래스의 레퍼런스 정보에서 확인할 수 있으며, 그 외에 안드로이드 프레임웍의 다른 부분에서 정의된 액션값들은 그와 관련된 클래스 정보에서 확인할 수 있습니다. 예를 들어, 설정 앱과 관련된 액션값들은 Settings 클래스에서 확인할 수 있습니다.

액션값은 인텐트의 setAction() 메소드 또는 생성자에 인자로 넘겨 설정할 수 있습니다.

만약 내 앱의 자체적인 액션값을 만든다면, 아래와 같이 앞부분에 패키지명을 넣어 주시기 바랍니다.

static final String ACTION_TIMETRAVEL = "com.example.action.TIMETRAVEL";


데이터 (Data)

액션에 관련된 데이터의 URI(Uri 클래스의 객체)와 그 데이터에 대한 MIME 타입을 말합니다. 데이터는 보통 액션값과 관련되어 있습니다. 예를 들어, 액션값이 ACTION_EDIT이라면, 데이터는 수정할 문서의 URI를 포함하고 있어야 합니다. 

인텐트를 만들 때, 데이터의 URI와 함께 MIME 타입을 지정해야 하는 경우도 있습니다. 예를 들어 이미지 뷰어 액티비티는 동영상 파일을 재생할 필요가 없는데, 이미지나 동영상의 URI가 비슷하여 구분하기 어려운 경우가 있을 수 있습니다. 그래서 안드로이드는 MIME 타입을 이용하여 더 적합한 컴포넌트를 찾습니다. 반면에 MIME 타입을 URI로부터 추론할 수 있는 경우도 있습니다. URI가 content: 로 시작하면, 디바이스의 로컬 저장소에 저장된 데이터이고 컨텐트 프로바이더를 통해 제공된 데이터라는 의미이며, 이때는 프로바이더가 시스템에게 MIME 타입을 알려줍니다.

인텐트 객체에 URI를 담기 위해서는 setData() 메소드를 호출하고, MIME 타입을 담기 위해서는 setType()을 호출하며, 둘다 담기 위해서는 setDataAndType()을 호출합니다.

주의사항: URI와 MIME 타입 모두 셋팅하는 경우에는, setData()와 setType()을 사용하면 안됩니다. setData()는 MIME 타입을 null로 만들고, setType()은 URI를 null로 만들기 때문입니다. 따라서 둘 모두 셋팅하는 경우에는 setDataAndType()을 사용해야만 합니다.


카테고리 (Category)

암묵적 인텐트로 찾고 있는 컴포넌트에 대한 추가적인 정보입니다. 인텐트는 카테고리를 1개 이상 담고 있을 수 있지만, 대부분의 경우 카테고리를 필요로 하지는 않습니다.

일반적으로 접하게 되는 카테고리 두 가지는 아래와 같습니다.

Intent.CATEGORY_BROWSABLE

실행될 액티비티가, 링크로 연결된 컨텐츠(웹문서, 이미지, 이메일 메시지)를 보여줄 수 있는 웹 브라우저 기능을 포함하고 있다는 것을 의미합니다.

Intent.CATEGORY_LAUNCHER

실행될 액티비티가 태스크의 첫 액티비티이며, 런처 화면의 앱 목록에 보여진다는 것을 의미합니다.

카테고리 전체 목록은 Intent 클래스의 레퍼런스 문서에서 확인하실 수 있습니다.

인텐트에 카테고리 정보를 담기 위해서는 addCategory() 메소드를 사용합니다.


위에서 설명한 속성들(컴포넌트 이름, 액션, 데이터, 카테고리)은 인텐트의 성격을 대표하는 값으로서, 시스템이 컴포넌트를 찾을 때 사용됩니다.

하지만 인텐트는, 컴포넌트 찾는 일과 상관 없이 부가적인 정보를 전달할 수 있으며, 아래 두 가지 속성을 지원합니다.


엑스트라 정보 (Extras)

요청된 액션을 수행하는데 필요한 부가적인 정보이며, 키와 값의 조합으로 구성되어 있습니다(key-value pairs). 일부 액션이 특정 종류의 URI를 필요로 하는 것과 마찬가지로, 일부 액션은 특정 엑스트라 정보를 필요로 합니다. 

엑스트라 정보는 인텐트의 다양하게 오버로딩된 putExtra() 메소드에 키와 값을 인자로 넘겨서 셋팅할 수 있습니다. 또한 Bundle 객체를 만들어서 putExtras() 메소드에 넘길 수도 있습니다.

예를 들어 "새 이메일 쓰기" 액티비티를 실행하기 위해서, 인텐트의 액션값은 ACTION_SEND이고, 수신자가 미리 채워져 있도록 하기 위해 putExtra(Intent.EXTRA_EMAIL, "수신자 이메일")을 호출하며, 제목이 미리 채워져 있도록 하기 위해 putExtra(Intent.EXTRA_SUBJECT, "이메일 제목")을 호출합니다.

엑스트라 정보의 키 값을 표준화하기 위해, Intent 클래스에는 상당히 많은 EXTRA_* 상수가 정의되어 있습니다. 만약 내 앱 자체적으로 엑스트라 키를 정의해서 사용해야 한다면, 아래와 같이 앞부분에 패키지명을 넣어야 미리 정의된 값들과 충돌되는 일이 없을 것입니다.

static final String EXTRA_GIGAWATTS = "com.example.EXTRA_GIGAWATTS";


플래그 (Flags)

플래그는 인텐트의 메타데이터 역할을 합니다. 플래그는 액티비티를 어떻게 실행할 것인지(예를 들어, 어떤 태스크(task)에 포함될 것인지)와 실행된 후에 어떻게 처리할 것인지(예를 들어, 최근 액티비티 목록에 포함을 시킬지 말지 여부)를 지시합니다. 

자세한 내용은 Intent 클래스 문서의 setFlags() 메소드 부분에서 학습할 수 있습니다.


명시적 인텐트의 예제


명시적 인텐트는 내 앱의 액티비티나 서비스와 같은 컴포넌트를 실행하기 위한 수단 중 하나로, 인텐트 객체에 컴포넌트 이름을 명시적으로 지정하여 만듭니다. 컴포넌트 이름 이외의 다른 속성들은 없어도 됩니다.

예를 들어, 내 앱에서 웹으로부터 파일을 다운로드 받는 DownloadService 를 만들었다면, 아래 코드와 같이 서비스를 실행할 수 있습니다.

// Executed in an Activity, so 'this' is the Context
// The fileUrl is a string URL, such as "http://www.example.com/image.png"
Intent downloadIntent = new Intent(this, DownloadService.class);
downloadIntent
.setData(Uri.parse(fileUrl));
startService
(downloadIntent);

Intent(Context, Class) 생성자는 앱의 Context와 실행할 컴포넌트의 Class 객체를 받습니다. 따라서 해당 생성자로 만들어진 인텐트는 명시적으로 DownloadService를 실행할 수 있습니다.

서비스를 만들고 실행하는 것에 대한 자세한 내용은 서비스에 대하여에서 학습하실 수 있습니다.


암묵적 인텐트의 예제


암묵적 인텐트에는, 디바이스의 모든 앱 중에서 원하는 앱을 실행하기 위해 액션값을 지정합니다. 어떤 기능에 대하여 내 앱 안에는 그 기능을 수행할 수 있는 컴포넌트가 없고 다른 앱에는 있을 때 암묵적 인텐트를 사용하여 다른 앱의 컴포넌트를 실행할 수 있습니다. 해당 기능을 수행할 수 있는 컴포넌트가 두 개 이상이라면 사용자가 선택 다이얼로그를 통해 선택할 수 있습니다.

예를 들어, 다른 사람에게 컨텐츠를 공유하고 싶다면, 인텐트에 ACTION_SEND를 설정하고 엑스트라 정보로 공유할 컨텐츠를 추가한 후 startActivity() 호출시 인자로 넘겨주면, 컨텐츠 공유가 가능한 앱들이 선택 다이얼로그로 보여지며 사용자가 그 중 하나를 선택할 수 있습니다.

주의사항: 암묵적 인텐트로 startActivity()를 호출했을 때, 그 인텐트로 찾고자 하는 컴포넌트가 하나도 없을 수도 있습니다. 이 경우, 호출은 실패하고 앱은 종료될 것입니다. 그래서 이러한 경우를 예방하기 위해서는 인텐트 객체의 resolveActivity() 메소드를 이용할 수 있습니다. resolveActivity()의 호출결과가 null이 아니라면 실행될 수 있는 컴포넌트가 있다는 의미이므로 startActivity()가 정상적으로 수행될 것이고, 반대로 null이라면 실행될 컴포넌트가 없으므로 startActivity()를 호출하지 않도록 해당 UI를 비활성 처리해야 할 것입니다.

// Create the text message with a string
Intent sendIntent = new Intent();
sendIntent
.setAction(Intent.ACTION_SEND);
sendIntent
.putExtra(Intent.EXTRA_TEXT, textMessage);
sendIntent
.setType(HTTP.PLAIN_TEXT_TYPE); // "text/plain" MIME type

// Verify that the intent will resolve to an activity
if (sendIntent.resolveActivity(getPackageManager()) != null) {
    startActivity
(sendIntent);
}

메모: 위 예제의 경우, URI는 사용되지 않지만 엑스트라 정보의 타입을 지정하기 위해 setType()을 호출합니다.

startActivity()가 호출되면, 시스템은 설치된 모든 앱 중에서 인텐트에 부합하는 앱들을 찾습니다 (위의 예제에서 인텐트의 액션값은 ACTION_SEND이고, 데이터 타입은 text/plain입니다). 만약 인텐트에 부합하는 앱이 한 개라면 그 앱을 바로 실행하고, 두 개 이상이라면 사용자에게 선택 다이얼로그를 보여줍니다. 여기서는 앱을 찾는다고 표현했으나 실제로는 컴포넌트를 찾는 것이며, 선택 다이얼로그에서는 해당 컴포넌트를 포함하고 있는 앱을 보여줍니다. 


앱 선택 다이얼로그 띄우기


암묵적 인텐트에 부합하는 앱이 두 개 이상인 경우, 사용자는 어떤 앱을 실행할 지 선택할 수 있고, 선택한 앱을 기본값으로 사용할 수도 있습니다. 이것은 사용자가 어떤 액션에 대해서는 항상 같은 앱이 실행되기를 바랄 때 유용합니다. 예를 들면, 만약 브라우저 앱이 여러개 있다면 웹페이지 링크를 클릭했을 때 선택 다이얼로그가 뜰 것입니다. 이때 하단에 체크박스(이 작업에 대해 기본값으로 사용)를 체크하고 크롬을 선택하면, 다음부터는 바로 크롬이 실행됩니다.

하지만 사용자가 상황에 따라 다른 앱을 선택할 수 있어야 하는 경우라면, 항상 사용자가 선택 다이얼로그를 볼 수 있도록 명시적으로 다이얼로그를 출력해 줘야 합니다. 예를 들어, 아래 코드 예제에서와 같이 createChooser()로 인텐트를 만들어서 startActivity() 하면 [그림2]와 같이 선택 다이얼로그가 뜨는데, 여기에는 하단의 기본값 설정 관련 체크박스가 없습니다.

Intent intent = new Intent(Intent.ACTION_SEND);
...

// Always use string resources for UI text.
// This says something like "Share this photo with"
String title = getResources().getString(R.string.chooser_title);
// Create intent to show chooser
Intent sendIntent = Intent.createChooser(intent, title);

// Verify the intent will resolve to at least one activity
if (sendIntent.resolveActivity(getPackageManager()) != null) {
    startActivity
(sendIntent);
}

createChooser() 호출시 인자로 넘겨주는 title은 선택 다이얼로그의 상단에 출력되는 제목입니다.


그림2. 선택 다이얼로그


암묵적 인텐트 받기 


암묵적 인텐트를 받을 수 있는 컴포넌트가 되려면, 매니페스트 파일의 컴포넌트 요소 안에 하나 이상의 인텐트 필터 요소(<intent-filter>)를 선언해야 합니다. 각 인텐트 필터는 인텐트의 액션, 데이터, 카테고리에 대응되는 요소들을 포함합니다. 시스템은 컴포넌트의 인텐트 필터들을 확인하여 인텐트에 부합하는 인텐트 필터가 있을 경우에만 그 인텐트를 컴포넌트에게 전달합니다. 

메모: 명시적 인텐트는 인텐트 필터와 상관 없이 대상 컴포넌트에게 전달됩니다.

앱의 컴포넌트는 어떤 일을 할 것인가에 따라 구분된 인텐트 필터를 선언해야 합니다. 예를 들어, 갤러리 앱의 액티비티는 아마도 두 개의 인텐트 필터를 가질 것인데, 하나는 이미지를 보는 것에 대한 필터일 것이고 다른 하나는 이미지를 편집하는 것에 대한 필터일 것입니다. 액티비티가 시작되면, 그 액티비티는 넘겨받은 인텐트의 액션값 등을 확인하여, 이미지를 그냥 보여줄 것인지 아니면 편집화면을 보여줄 것인지를 결정합니다.

인텐트 필터는 매니페스트 파일에서 <activity>와 같은 컴포넌트 요소의 자식 요소인 <intent-filter>로 선언되고, <intent-filter> 요소에는 다시 자식 요소로 아래 세가지 요소들이 선언됩니다. 

<action>
받아들일 인텐트의 액션값을 name 속성에 선언합니다. 이때 주의할 점은, 자바코드의 Intent.ACTION_SEND와 같은 변수명이 아니라 "android.intent.action.SEND"와 같은 실제 문자열을 넣어야 한다는 것입니다.

<data>
받아들일 인텐트의 데이터 타입을 한 개 이상 선언할 수 있습니다. 여기에는 URI를 구성하는 부분들(scheme, host, port, path 등)과 MIME 타입이 선언됩니다.

<category>
받아들일 인텐트의 카테고리를 name 속성에 선업합니다. 이때 주의할 점은 변수명이 아니라 실제 문자열을 넣어야 한다는 것입니다.

메모: 암묵적 인텐트를 받기 위해서는 인텐트 필터에 "android.intent.category.DEFAULT" 가 반드시 포함되어 있어야 합니다. startActivity()startActivityForResult() 메소드는 모든 인텐트에 Intent.CATEGORY_DEFAULT가 선언되어 있다고 전제하고 동작하기 때문에, 인텐트 필터에 CATEGORY_DEFAULT가 선언되어 있지 않으면, 인텐트에 부합하는 컴포넌트가 되지 못합니다.

아래 예제는, 액션이 ACTION_SEND이고 데이터 타입이 텍스트인 인텐트를 받을 수 있는 액티비티의 선언입니다.

<activity android:name="ShareActivity">
   
<intent-filter>
       
<action android:name="android.intent.action.SEND"/>
       
<category android:name="android.intent.category.DEFAULT"/>
       
<data android:mimeType="text/plain"/>
   
</intent-filter>
</activity>

하나의 인텐트 필터가 <action>, <data>, <category>를 각각 두 개 이상씩 포함할 수도 있습니다. 이러한 경우에는 모든 조합에 대하여 한 가지만 조건에 맞아도 인텐트가 필터를 통과할 수 있습니다.

만약 여러 종류의 인텐트를 받고 싶으나 특정 조합들에 대해서만 제한하고 싶다면, 각 조합들에 해당하는 인텐트 필터들을 추가하면 됩니다.

암묵적 인텐트는 액션, 데이터, 카테고리에 대하여 인텐트 필터의 그것들과 각각 테스트를 하며, 모든 테스트가 통과되어야만 컴포넌트에게 인텐트가 전달되며, 하나라도 통과하지 못하면 인텐트가 전달되지 않습니다. 하지만 인텐트 필터가 여러개일 경우, 어떤 인텐트 필터에 대하여 테스트가 실패하면 다음 인텐트 필터에 대하여 테스트하게 되고, 테스트가 통과되는 인텐트 필터를 찾을 때까지 모두 확인합니다. 시스템이 인텐트를 처리하는 방법에 대한 자세한 내용은 아래의 인텐트 처리하기에서 학습할 수 있습니다.

주의사항: 우연히 다른 앱의 서비스가 실행되는 일이 없도록 하기 위해, 내 앱의 서비스를 실행할 때는 명시적 인텐트를 사용하고, 서비스에는 인텐트 필터를 선언하지 않도록 합니다.

메모: 액티비티를 선언하거나 액티비티에 인텐트 필터를 선언하는 것은 매니페스트 파일에서만 가능합니다. 하지만 브로드캐스트 리시버는 소스코드에서 registerReceiver()로 등록할 수 있고(이때 인텐트 필터를 설정합니다), unregisterReceiver()로 해제할 수 있습니다. 매니페스트에 선언된 리시버는 시스템이 정해놓은 시점에 등록 및 해제가 되지만, 소스코드에서는 개발자가 원하는 시점에 리시버를 등록 및 해제할 수 있습니다.

컴포넌트로의 접근 제한 방법: 컴포넌트에 인텐트 필터를 선언하는 것은 암묵적 인텐트의 종류를 제한하기는 하지만 다른 앱에 의해 실행될 수 있기 때문에 보안적으로 안전한 방법은 아닙니다. 또한 다른 앱에서 내 앱의 컴포넌트 이름을 알고 있다면 명시적 인텐트를 통해 해당 컴포넌트를 실행할 수도 있습니다. 그래서 다른 앱으로부터 내 앱의 컴포넌트를 안전하게 지키기 위해서는 exported 속성을 "false"로 설정해야 합니다. 


인텐트 필터의 예제

아래 예제는 인텐트 필터의 동작에 대한 이해를 돕기 위해 준비한 어떤 소셜 공유 앱의 매니페스트 파일 일부입니다.

<activity android:name="MainActivity">
   
<!-- This activity is the main entry, should appear in app launcher -->
   
<intent-filter>
       
<action android:name="android.intent.action.MAIN" />
       
<category android:name="android.intent.category.LAUNCHER" />
   
</intent-filter>
</activity>

<activity android:name="ShareActivity">
   
<!-- This activity handles "SEND" actions with text data -->
   
<intent-filter>
       
<action android:name="android.intent.action.SEND"/>
       
<category android:name="android.intent.category.DEFAULT"/>
       
<data android:mimeType="text/plain"/>
   
</intent-filter>
   
<!-- This activity also handles "SEND" and "SEND_MULTIPLE" with media data -->
   
<intent-filter>
       
<action android:name="android.intent.action.SEND"/>
       
<action android:name="android.intent.action.SEND_MULTIPLE"/>
       
<category android:name="android.intent.category.DEFAULT"/>
       
<data android:mimeType="application/vnd.google.panorama360+jpg"/>
       
<data android:mimeType="image/*"/>
       
<data android:mimeType="video/*"/>
   
</intent-filter>
</activity>

첫번째 액티비티인 MainActivity는 앱의 진입점으로서 시작화면이고, 사용자가 런처화면에서 아이콘을 눌러 들어오게 되는 액티비티입니다.

  • "android.intent.action.MAIN"(Intent.ACTION_MAIN)은 이 액티비티가 시작화면이고, 부가적인 데이터는 필요로 하지 않는다는 것을 의미합니다.
  • "android.intent.category.LAUNCHER" (Intent.CATEGORY_LAUNCHER)는 이 액티비티의 아이콘이 안드로이드 런처화면에 보여진다는 것을 의미합니다. 만약 <activity> 요소에 icon 속성이 정의되어 있지 않다면, <application> 요소의 icon 속성이 사용됩니다.

런처화면에 아이콘이 보여지기 위해서는 인텐트 필터에 위의 두가지가 모두 있어야 합니다.

두번째 액티비티인 ShareActivity는 텍스트나 미디어 컨텐츠를 공유하기 위한 액티비티입니다. 이것은 MainActivity로부터 실행될 수도 있고, 다른 앱으로부터 (두 필터 중 하나와 부합하는) 암묵적 인텐트를 통해 실행될 수도 있습니다.

메모: MIME 타입 중에 application/vnd.google.panorama360+jpg는 파노라마 사진(360도 촬영 사진)을 나타내는 특별한 데이터 타입이며, 파노라마 사진은 구글 파노라마 API (Google panorama APIs)를 사용하여 만들 수 있습니다.


펜딩 인텐트 사용하기


펜딩 인텐트(PendingIntent) 객체는 인텐트 객체를 포함하는 객체이며, 주요 목적은 다른 앱이 펜딩 인텐트에 포함된 인텐트를 마치 자신의 인텐트인양 권한을 부여하는 것입니다. 

펜딩 인텐트는 주로 아래와 같은 상황에서 사용됩니다.

  • 노티(Notification)를 노티피케이션바에 올린 경우, 노티를 눌렀을 때 시스템의 NotificationManager가 펜딩 인텐트에 포함된 인텐트를 사용하여 컴포넌트를 실행합니다.
  • 앱 위젯(App Widget)을 이용하는 경우, 위젯의 버튼 등을 눌렀을 때, 런처의 홈스크린앱이 펜딩 인텐트에 포함된 인텐트를 사용하여 컴포넌트를 실행합니다.
  • 시스템 알람 기능을 이용하는 경우, 미래의 특정 시점이 되었을 때 AlarmManager가 펜딩 인텐트에 포함된 인텐트를 사용하여 컴포넌트를 실행합니다.

각 인텐트는 액티비티, 서비스, 브로드캐스트 리시버 중 하나를 실행하도록 설계되어 있기 때문에, 펜딩 인텐트도 마찬가지로 한가지 목적을 가지고 만들어져야 합니다. 펜딩 인텐트를 사용하는 경우에는 startActivity()와 같은 메소드를 사용할 수 없습니다. 대신에, 아래 소개되는 펜딩 인텐트의 생성 메소드들 중 하나를 사용함으로써 실행할 컴포넌트를 지정해야 합니다.

  • PendingIntent.getActivity()는, 인텐트가 실행할 컴포넌트가 액티비티인 경우에 사용하며, 펜딩 인텐트 객체를 반환해 줍니다.
  • PendingIntent.getService()는, 인텐트가 실행할 컴포넌트가 서비스인 경우에 사용합니다.
  • PendingIntent.getBroadcast()는, 인텐트가 실행할 컴포넌트가 브로드캐스트 리시버인 경우에 사용합니다.

다른 앱으로부터 펜딩 인텐트를 받기만 하는 것이 아니라 만들기도 해야한다면 위의 세가지 메소드가 필요할 것입니다.

각 메소드의 인자로는, 앱의 Context와 펜딩 인텐트 안에 포함할 인텐트, 그리고 인텐트가 어떤 방식으로 사용될 지를 지정하는 플래그를 받습니다. 

펜딩 인텐트를 사용하는 방법에 대한 더 자세한 내용은 노티피케이션(Notifications)과 앱 위젯(App Widget)에서 학습하실 수 있습니다.


인텐트 처리하기


시스템은, 암묵적 인텐트를 받아 액티비티를 실행하려고 할때, 그 인텐트와 설치된 앱들의 인텐트 필터들을 아래 나열된 세가지 측면에서 비교하여 조건에 맞는 액티비티를 찾습니다.

  • 인텐트의 액션값
  • 인텐트의 데이터 (URI와 데이터 타입)
  • 인텐트의 카테고리

아래 내용에서는 원하는 인텐트를 받기 위해 매니페스트 파일에 인텐트 필터를 어떻게 선언할지에 대하여 학습합니다.


액션 테스트 (Action test)

어떤 액션값을 받을지 지정하기 위해, 인텐트 필터는 아래와 같이 0개 이상의 <action> 요소를 선언할 수 있습니다.

<intent-filter>
   
<action android:name="android.intent.action.EDIT" />
   
<action android:name="android.intent.action.VIEW" />
    ...
</intent-filter>

위의 필터를 통과하기 위해서는 인텐트가 필터에 선언된 두 액션 중 하나를 가지고 있어야 합니다.

만약 필터에 액션이 하나도 선언되지 않았다면, 그 필터를 통과할 수 있는 인텐트는 없을 것입니다.


카테고리 테스트 (Category test)

어떤 카테고리를 받을지 지정하기 위해, 인텐트 필터는 아래와 같이 0개 이상의 <category> 요소를 선언할 수 있습니다.

<intent-filter>
   
<category android:name="android.intent.category.DEFAULT" />
   
<category android:name="android.intent.category.BROWSABLE" />
    ...
</intent-filter>

인텐트가 카테고리 테스트를 통과하기 위해서는, 인텐트가 가지고 있는 카테고리가 모두 필터에 선언된 카테고리들 안에 포함되어야 합니다. 그래서 필터 안에 카테고리들을 더 선언하더라도 기존에 통과하던 인텐트는 여전히 통과할 수 있습니다. 따라서, 카테고리를 가지고 있지 않은 인텐트는 필터에 어떤 카테고리가 선언되었느냐에 상관없이 무조건 통과할 수 있습니다.

메모: 안드로이드는 startActivity()startActivityForResult()를 할때 자동적으로 인텐트에 기본 카테고리(Intent.CATEGORY_DEFAULT, "android.intent.category.DEFAULT")를 적용합니다. 따라서 암묵적 인텐트로 액티비티를 실행하려고 한다면, 그 액티비티의 인텐트 필터에 기본 카테고리가 선언되어 있어야 합니다. 


데이터 테스트 (Data test)

어떤 종류의 데이터를 받을지 지정하기 위해, 인텐트 필터는 아래와 같이 0개 이상의 <data> 요소를 선언할 수 있습니다.

<intent-filter>
   
<data android:mimeType="video/mpeg" android:scheme="http" ... />
   
<data android:mimeType="audio/mpeg" android:scheme="http" ... />
    ...
</intent-filter>

<data>에는 URI를 구성하는 부분들(scheme, host, port, path)과 MIME 타입이 선언됩니다.

<scheme>://<host>:<port>/<path>

와 같은 형태가 되며 예를 들면 아래와 같습니다.

content://com.example.project:200/folder/subfolder/etc

이 URI에서 scheme은 content, host는 com.example.project, port는 200, path는 folder/subfolder/etc 입니다.

<data>의 각 속성들은 옵셔널(optional)하지만, 서로간에 의존성이 있습니다.

  • scheme이 없으면, host는 무시됩니다.
  • host가 없으면, port는 무시됩니다.
  • scheme과 host가 둘다 없으면, path는 무시됩니다.

인텐트가 가지고 있는 URI를 인텐트 필터와 비교할 때는, <data>에 선언된 속성들만을 비교합니다. 예를 들면,

  • <data>에 scheme만 선언되어 있다면, 선언된 scheme으로 시작하는 URI들은 모두 테스트를 통과합니다.
  • <data>에 scheme과 authority(host+port부분, 실제로 port는 선언되는 경우가 별로 없습니다)가 선언되어 있다면, 선언된 값들로 구성된 URI들은 path와 상관없이 테스트를 통과합니다.
  • <data>에 scheme, authority, path가 선언되어 있다면, 선언된 값들로 구성된 URI만이 테스트를 통과합니다.

메모: path 선언시에는 와일드카드 문자 *를 포함하여 융통성 있게 선언할 수 있습니다.

데이터 테스트는 인텐트의 URI와 MIME 타입을, 인텐트 필터에 선언된 URI와 MIME 타입과 비교합니다. 그 규칙은 아래와 같습니다.

  1. URI와 MIME 타입이 모두 없는 인텐트는, URI와 MIME 타입이 모두 선언되지 않은 인텐트 필터에서 테스트를 통과합니다.
  2. URI는 있으나 MIME 타입이 없는 인텐트는, 마찬가지로 URI는 선언되어 있고 MIME 타입이 선언되지 않은 필터에서 테스트를 통과할 수 있습니다.
  3. MIME 타입은 있으나 URI가 없는 인텐트는, MIME 타입이 선언되어 있고 URI가 선언되지 않은 필터에서 테스트를 통과할 수 있습니다.
  4. URI와 MIME 타입이 모두 있는 인텐트는, 일단 필터에 일치하는 MIME 타입이 선언되어 있어야 하고 다음 조건을 만족해야 테스트를 통과할 수 있습니다. 인텐트 필터에 URI가 선언되어 있다면 인텐트의 그것과 일치해야 하며, 필터에 URI가 선언되어 있지 않다면 URI가 content:file: 로 시작하는 값이어야 합니다. 바꿔 말하면, 인텐트 필터의 <data>에 MIME 타입만 선언하는 것은, content:file: 로 시작하는 URI만 받겠다는 것을 의미합니다.

위의 마지막 규칙 D는, MIME 타입만 선언된 필터를 가지고 있는 컴포넌트가 파일 시스템이나 컨텐트 프로바이더를 통해 로컬 파일을 처리할 수 있다는 것을 알려줍니다. 따라서 이러한 경우는 데이터 타입들만 선언하고, 명시적으로 scheme에 contentfile을 선언할 필요는 없습니다. 이것은 전형적인 사용 방식으로서, 아래 예제는 시스템에게, 이 필터를 포함한 컴포넌트는 컨텐트 프로바이더로부터 이미지를 얻어와 보여주는 역할을 한다라고 말해줍니다.

<intent-filter>
   
<data android:mimeType="image/*" />
    ...
</intent-filter>

많은 경우의 데이터들이 컨텐트 프로바이더로부터 오기 때문에, 인텐트 필터의 <data> 요소에 URI 관련 선언이 없는 경우가 일반적입니다.

위와 다르게, <data>에 scheme과 mimeType이 선언된 경우가 있습니다. 아래 예제는 시스템에게, 이 필터를 포함한 컴포넌트가 네트웍을 통해 동영상 데이터를 받아오는 역할을 한다라고 말해줍니다.

<intent-filter>
   
<data android:scheme="http" android:mimeType="video/*" />
    ...
</intent-filter>


인텐트 매칭 (Intent matching)

인텐트는 두 가지 목적에서 인텐트 필터와 비교합니다. 실행할 컴포넌트를 찾기 위해서이기도 하고, 어떤 조건에 맞는 컴포넌트 목록을 얻어오기 위해서이기도 합니다. 예를 들어 홈스크린앱은, 설치된 모든 앱에서 인텐트 필터에 ACTION_MAINCATEGORY_LAUNCHER가 선언된 액티비티들을 찾아와 화면에 그 목록을 보여주는 것이 주된 역할입니다.

내 앱에서도 비슷한 방식으로 인텐트 매칭을 사용할 수 있습니다. PackageManagerquery...() 형태의 메소드들과 resolve...() 형태의 메소드들을 지원합니다. query...()은 인텐트를 받을 수 있는 모든 컴포넌트의 목록을 리턴해주고, resolve...()은 가장 적절한 컴포넌트를 리턴해 줍니다. 예를 들어, queryIntentActivities()는 인텐트를 받을 수 있는 모든 액티비티들을 리턴해주고, queryIntentServices()는 서비스들을 리턴해주며, queryBroadcastReceivers()는 브로드캐스트 리시버들을 리턴해줍니다. 위의 세 메소드는 실제로 컴포넌트를 실행하거나 하지는 않고, 단지 목록을 리턴해주기만 합니다.


Posted by 개발자 김태우
,

(원문: http://developer.android.com/guide/components/index.html)

(위치: Develop > API Guides > App Components)

(전체 번역 글 목록으로 이동하기)


앱 컴포넌트




안드로이드의 앱 개발 프레임웍은 재사용 가능한 컴포넌트들을 사용함으로써 혁신적인 앱을 만들 수 있도록 해줍니다. 이어지는 섹션들에서는 컴포넌트들을 만드는 방법과 인텐트로 서로 연결하는 방법을 학습합니다.

Intent와 Intnt Filter >


트레이닝 카테고리의 관련 글:

액티비티(Activity)의 생명주기 관리하기

액티비티 생명주기에 따른 콜백 메소드들의 구현 방법에 대하여 학습합니다. 액티비티는 사용자들에게 익숙한 UI를 제공해야 하고, 쓸데없이 시스템 리소스를 낭비하지 않아야 합니다.


프레그먼트(Fragment)를 이용한 동적 UI 개발하기

프레그먼트를 이용하여 스마트폰 및 태블릿에 모두 대응할 수 있는 동적 UI를 개발하는 방법을 학습합니다. 다만, 프레그먼트는 안드로이드 버전 1.6부터 사용 가능합니다.


컨텐츠 공유하기

인텐트와 액션 프로바이더(ActionProvider)를 사용하는 앱들간에 컨텐츠를 주고 받는 일반적인 방법들에 대하여 학습합니다.


Posted by 개발자 김태우
,

(원문: 

http://developer.android.com/guide/topics/security/permissions.html)

(위치: Develop > API Guides > Introduction > System Permissions)

(전체 번역 글 목록으로 이동하기)


시스템 퍼미션


안드로이드는 유저 및 그룹별로 권한이 분리되어 있는 OS로서, 앱들은 각각의 시스템 ID(리눅스 유저 ID와 그룹 ID)를 가지고 실행됩니다. 시스템의 각 부분들도 각각의 ID를 갖고 분리되어 있습니다. 리눅스에서, 앱들은 서로 분리된 환경에서 구동되고, 또 시스템과도 분리되어 있는데, 안드로이드가 이러한 리눅스를 기반으로 하고 있는 것입니다.

추가로 도입된 보안적 특징은 퍼미션(permission) 메커니즘을 통해 실행을 제한한다는 점입니다. 특정 프로세스의 실행을 제한할 수 있고, 특정 데이터에 접근을 허용하는 URI들에 대해서도 실행을 제한할 수 있습니다.

여기서는 개발자들이 안드로이드의 보안 특징들을 어떻게 이용할 수 있는지 설명하겠습니다. 더 자세한 내용은 안드로이드 보안 훑어보기에서 학습하실 수 있습니다.


보안 아키텍쳐 (Security Architecture)


안드로이드 보안 아키텍쳐의 핵심은, 기본적으로 앱들이 다른 앱이나 OS, 또는 사용자에게 불리한 영향을 미칠 만한 퍼미션들을 갖고 있지 않습니다. 여기에는 주소록이나 이메일과 같은 사용자 개인 정보의 읽기/쓰기 권한과, 다른 앱의 파일에 대한 읽기/쓰기 권한, 그리고 네트워크 접속과 관련된 권한, 디바이스를 켜진 상태로 유지하는 권한 등이 포함됩니다.

안드로이드 앱은 각각의 프로세스 샌드박스에서 구동되기 때문에, 앱끼리 리소스나 데이터를 공유하려면 명시적으로 해야 합니다. 이것은, 기본적으로는 사용할 수 없지만 퍼미션을 선언함으로써 사용할 수 있다는 의미입니다. 앱들은 필요한 퍼미션들을 매니페스트 파일에 선언해야 하고, 안드로이드 시스템은 앱이 설치되기 직전에 사용자에게 퍼미션 목록을 보여줌으로써 동의를 구합니다. 안드로이드는 보안과 관련된 사용자 경험을 복잡하게 만들지 않기 위해서, 런타임에 동적으로 퍼미션을 관리하지는 않습니다. 다시 말해서, 앱을 설치할 때 한번 사용자에게 모든 동의를 받고, 앱을 실행하는 중에는 퍼미션과 관련된 절차를 거치지 않는다는 것입니다.

앱의 샌드박스는 앱을 빌드할 때 사용하는 개발언어 및 기술들에 의존적이지 않습니다. 좀더 구체적으로 말하면, 달빅 가상머신(Dalvik VM)은 보안 환경 안에 있지 않으며, 어떤 앱이던 C/C++과 같은 네이티브 코드(안드로이드 NDK 참고)를 실행할 수 있습니다. 즉, 자바로 개발된 앱이나, C/C++로 개발된 앱이나, 또는 둘을 혼용한 앱이나 모두 같은 방식의 샌드박스 안에서 실행되며, 서로간의 보안 환경도 같습니다.


앱에 사인하기 (Application Signing)


모든 APK 파일들(.apk)은 개발자의 개인키가 되는 증명서로 사인이 되어 있어야만 합니다. 이 증명서는 앱의 소유자를 확인하는 용도로 사용되는데, 인증 기관을 통해 발행되는 것이 아니라 개발자가 자체적으로 만드는 것이며(self-signed), 안드로이드에서 이러한 증명서들의 목적은 앱의 소유자를 구분하는 것입니다. 시스템이 시그니쳐 레벨의 퍼미션(signature-level permissions)을 승인하거나 거부할 때, 그리고 동일한 리눅스 ID로부터 요청받은 사항을 승인하거나 거부할 때 증명서를 사용합니다.


유저 ID와 파일 접근성(User IDs and File Access)


안드로이드는 앱을 설치할 때 해당 앱의 대표 패키지명에 대응되는 리눅스 유저 ID를 부여합니다. 이 ID는 해당 패키지가 디바이스에서 제거될 때까지 상수로 남습니다. 안드로이드 OS는 각 디바이스에 따로 있기 때문에, 같은 패키지에 대한 유저 ID가 디바이스마다 서로 다를 수는 있겠지만, 한 디바이스 안에서 서로 구분만 되면 되기 때문에 문제가 되지는 않습니다.

보안 실행은 프로세스 레벨에서 발생하기 때문에, 서로 다른 리눅스 유저 ID를 갖는 두 개의 패키지가 하나의 프로세스에서 실행될 수는 없습니다. 그러나 안드로이드 매니페스트 파일(AndroidManifest.xml)의 <manifest> 태그에 있는 sharedUserId 속성값을 똑같이 설정하면, 안드로이드는 이러한 두개의 패키지를 같은 앱으로서 관리하고 같은 유저 ID 및 같은 파일 접근 권한을 사용합니다. 그러나, 보안을 유지하기 위해 두 개의 앱이 같은 증명서로 사인되어야만 합니다.

앱에서 저장한 데이터는 해당 앱의 유저 ID에 할당되기 때문에, 기본적으로 다른 앱에서는 접근하지 못합니다. 그러나, 데이터를 저장하기 위해  getSharedPreferences(String, int), openFileOutput(String, int), openOrCreateDatabase(String, int, SQLiteDatabase.CursorFactory) 메소드들을 호출할 때, MODE_WORLD_RELEASE, MODE_WORLD_WRITEABLE 값을 인자로 넘기면 저장되는 파일의 접근 권한을 변경하여, 다른 앱에서도 읽기 또는 쓰기를 할 수가 있습니다.


퍼미션 사용하기 (Using Permissions)


아주 기본적인 안드로이드 앱은 아무런 퍼미션도 갖고 있지 않습니다. 사용자 경험이나 디바이스에 저장된 데이터와 관련하여, 사용자 동의를 구하지 않고 얻은 퍼미션으로 인해 좋지 않은 영향을 받을 수도 있기 때문입니다. 따라서 보호된 기능들을 사용하기 위해서는 매니페스트 파일(AndroidManifest.xml)에 해당 기능들에 대한 <uses-permission> 태그들을 추가해야 합니다. 

예를 들어, SMS 메시지가 수신될 때, 내 앱에서 이벤트를 받아 관련된 기능을 구현하고자 한다면 아래와 같이 선언해 줘야 합니다.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   
package="com.android.app.myapp" >
   
<uses-permission android:name="android.permission.RECEIVE_SMS" />
    ...
</manifest>

앱이 설치되기 직전에, 패키지 인스톨러(package installer)가 정상적으로 사인된 앱에 선언된 퍼미션들을 확인하여 사용자에게 보여주고 동의를 구합니다. 그리고 나서 앱이 실행될 때에는 퍼미션과 관련하여 사용자에게 동의를 구하지 않습니다. 만약 매니페스트 파일에 선언되지 않은 기능을 런타임에 사용하려고 하면, 사용자에게 동의를 구하고자 하지 않고 바로 실패 처리해 버립니다.

퍼미션이 선언되지 않아 관련 기능에 대한 호출이 실패한 경우, 대부분은 보안 예외(SecurityException)가 발생되지만 이것이 항상 보장되는 것은 아닙니다. 예를 들면, sendBroadcast(Intent) 메소드는 비동기적으로 동작하기 때문에, 데이터가 브로드캐스트 리시버들에게 제대로 전달되었는지를, 결과를 리턴해주고 나서야 확인합니다. 따라서 sendBroadcast(Intent)를 호출하는 시점에 보안 예외를 잡을 수는 없습니다(try-catch). 하지만 거의 모든 경우에, 퍼미션 실패가 발생되면 시스템 로그가 출력됩니다.

그러나, 구글플레이 스토어에서 앱을 설치할 때와 같이 일반적인 경우에, 사용자가 퍼미션들을 허용하는데에 동의하지 않는다면 그 앱은 설치되지 않기 때문에, 런타임때 보안 예외가 발생할 것에 대해 걱정할 필요는 없습니다. 바꿔 말하면, 앱이 설치가 되었다는 것은 그 앱이 요구하는 모든 퍼미션이 승인되었다는 것을 의미합니다. 이것은 필요한 퍼미션을 모두 매니페스트 파일에 선언했다는 것을 전제합니다.

안드로이드 시스템에 의해 제공되는 퍼미션들은 Manifest.permission 클래스에서 확인할 수 있습니다. 하지만, 내 앱에서 따로 퍼미션을 정의하고 요구할 수 있기 때문에, Manifest.permission 클래스의 퍼미션들이 전부라고 할 수는 없습니다.

내 앱이 실행되는 동안에 퍼미션이 필요한 몇가지 경우들을 요약하면 아래와 같습니다.

  • 앱이 시스템에 속한 기능을 사용하려고 할 때
  • 다른 앱의 액티비티(activity)를 실행하려고 할 때
  • 알림(broadcasts)을 발송하거나 수신하는 경우, 발송자나 수신자를 제한해야 할 때
  • 컨텐트 프로바이더(content provider)를 사용할 때
  • 서비스(service)를 바인드하거나 시작할 때


주의사항: 

시간이 흘러 안드로이드가 계속 업데이트 되다 보면, 어떤 API를 호출하기 위해 이전에는 신경쓰지 않았던 퍼미션을 요청해야 하는 경우가 생길 수도 있습니다. 이러한 변화 때문에 기존에 출시되어 있던 앱이 영향을 받지 않도록 하기 위해, 안드로이드는 targetSdkVersion 속성값을 확인하여 그 값이 현재 플랫폼 버전보다 낮을 경우, 하위호환을 위해 앱이 퍼미션을 요청하지 않았더라도 승인해 줄 수 있습니다.

예를 들면, API 레벨 4에서 외부 저장소로의 접근을 제한하기 위해 WRITE_EXTERNAL_STORAGE 퍼미션이 추가되었는데, 만약 내 앱의 targetSdkVersion이 3이하로 되어 있다면, API 레벨 4 이상의 안드로이드에서는 해당 퍼미션을 내 앱에 추가해 줍니다.

만약 내 앱에 이러한 경우가 생긴다면, 실제로 내 앱에서 외부 저장소 관련 기능을 사용하지 않더라도 구글플레이에서는 사용자에게 보여주는 퍼미션 목록에 WRITE_EXTERNAL_STORAGE 퍼미션을 추가한다는 것을 기억해야 합니다.

이렇게 필요하지도 않은 퍼미션이 사용자에게 보여지는 것을 막기 위해서는 targetSdkVersion을 가능한 한 높게 설정해야 합니다. 안드로이드 버전에 따라 어떤 퍼미션들이 추가되었는지에 대해서는 Build.VERSION_CODE 클래스 문서에서 확인하실 수 있습니다.


새로운 퍼미션의 정의와 사용

(Declaring and Enforcing Permissions)

 

새로운 퍼미션을 정의하기 위해서는 AndroidManifest.xml<permission> 태그를 선언해야 합니다.

예를 들어, 다른 앱들이 아무런 제한 없이 내 앱의 액티비티를 실행하는 것을 방지하기 위해 아래와 같이 퍼미션을 선언할 수 있습니다.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   
package="com.me.app.myapp" >
   
<permission android:name="com.me.app.myapp.permission.DEADLY_ACTIVITY"
       
android:label="@string/permlab_deadlyActivity"
       
android:description="@string/permdesc_deadlyActivity"
       
android:permissionGroup="android.permission-group.COST_MONEY"
       
android:protectionLevel="dangerous" />
    ...
</manifest>

<protectionLevel> 속성은 필수 입력 사항이며, 앱이 해당 퍼미션을 필요로 한다는 것을 사용자에게 알려줄 때 어느 정도의 영향을 줄 수 있는 퍼미션인지를 나타냅니다. 링크된 문서에서 자세히 학습하실 수 있습니다.

<permissionGroup> 속성은 선택 입력 사항이며, 시스템이 퍼미션을 사용자에게 보여줄 때 추가적으로 보여지는 정보에 불과합니다. 보통은 표준 시스템 그룹(android.Manifest.permission_group 클래스 참조)에 정의된 값을 사용하는데, 때에 따라서는 내 앱에서 정의한 값을 사용해야할 경우도 있을 것입니다. 하지만 사용자에게 보여지는 UI를 단순화하기 위해서는 기존에 있는 값을 사용하는게 권장됩니다. 이런저런 앱들을 설치할 때 항상 보던 문구가 아닌 다른 것이 있다면 사용자 입장에서는 다소 혼란스러울 수도 있기 때문입니다.

android:labelandroid:description은 사용자에게 보여지는 퍼미션 정보입니다. label은 퍼미션 목록에 보여지는 짧은 제목이고, description은 해당 퍼미션에 대한 자세한 정보로서 보통 2개의 문장으로 이루어져 있는데, 첫 문장에서 퍼미션에 대해 설명하고 두번째 문장에서는 해당 퍼미션을 승인했을 때 발생할 수도 있을 좋지 않은 영향에 대해 경고합니다.

아래의 예는 CALL_PHONE 퍼미션의 label과 description 입니다.

    <string name="permlab_callPhone">directly call phone numbers</string>
   
<string name="permdesc_callPhone">Allows the application to call
        phone numbers without your intervention. Malicious applications may
        cause unexpected calls on your phone bill. Note that this does not
        allow the application to call emergency numbers.
</string>

현재 시스템에 정의되어 있는 퍼미션들은 시스템 설정 앱이나 쉘 명령어를 통해 확인할 수 있습니다. 설정 앱을 사용할 경우, 설정 > 애플리케이션 에서 앱을 선택한 후 하단의 권한 정보를 확인하면 됩니다. 그리고 쉘 명령어를 이용하는 경우, 콘솔창에서 adb shell pm list permissions 를 입력하면 되는데 끝부분에 -s 옵션을 추가하면 사용자들이 보게 될 화면과 유사하게 콘솔창에 출력해 줍니다.

$ adb shell pm list permissions -s
All Permissions:

Network communication: view Wi-Fi state, create Bluetooth connections, full
Internet access, view network state

Your location: access extra location provider commands, fine (GPS) location,
mock location sources
for testing, coarse (network-based) location

Services that cost you money: send SMS messages, directly call phone numbers

...


AndroidManifest.xml에서 퍼미션 적용하기


시스템이나 앱의 모든 컴포넌트에 대해 접근을 제한하는 고수준의 퍼미션들은 AndroidManifest.xml을 통해 요청됩니다. 요청시 필요한 것은 매니페스트의 컴포넌트 요소에 android:permission 속성을 추가하는게 전부입니다.

액티비티의 퍼미션은 누가 그 액티비티를 시작할 수 있는지를 제한합니다. Context.startActivity()Activity.startActivityForResult()가 호출되었을 때, 호출한 쪽에서 호출받는 액티비티의 퍼미션을 가지고 있지 않다면 SecurityException이 발생됩니다.

마찬가지로 서비스의 퍼미션은 누가 그 서비스를 시작하거나 바인드할 수 있는지를 제한합니다. Context.startService(), Context.stopService(), Context.bindService()가 호출되었을 때, 퍼미션을 가지고 있지 않다면 SecurityException이 발생됩니다.

브로드캐스트 리시버의 퍼미션은 누가 그 리시버에게 알림(broadcasts)을 발송할 수 있는지를 제한하지만, 액티비티나 서비스의 퍼미션과는 다르게 Context.sendBroadcast()가 반환값(void)을 리턴한 후에, 시스템이 알림을 리시버에게 전달하려고 할 때 퍼미션을 체크합니다. 따라서 퍼미션을 가지고 있지 않아 접근 제한을 받는 상황이라 하더라도 호출한 쪽에서는 SecurityException이 발생되지 않으며, 그저 리시버에게 알림이 가지 않을 뿐입니다. 리시버의 퍼미션은 매니페스트 파일 뿐만 아니라 소스코드 내에서 Context.registerReceiver()를 통해 동적으로 등록될 때에도 설정될 수 있습니다. 그리고 반대로, Context.sendBroadcast()를 호출할 때 퍼미션을 설정하여, 해당 퍼미션을 가지고 있지 않은 리시버는 받지 못하도록 제한할 수도 있습니다.

컨텐트 프로바이더의 퍼미션은 누가 데이터에 접근할 수 있는지를 제한합니다 (컨텐트 프로바이더는 URI 퍼미션이라 불리는 중요한 보안적 특징을 가지고 있습니다). 다른 컴포넌트들과는 다르게 퍼미션 속성이 둘로 나뉘어 있는데, android:readPermission은 읽기 관련 퍼미션이고 android:writePermission은 쓰기 관련 퍼미션입니다. 이 둘은 독립적이기 때문에 쓰기 관련 퍼미션을 가지고 있는 것이 읽기 관련 퍼미션도 가지고 있는 것을 의미하지는 않습니다. 컨텐트 프로바이더의 퍼미션은 프로바이더가 처음 불려져서 실행될 때 체크되며, 만약 호출하는 쪽에서 요구되는 퍼미션을 가지고 있지 않다면 SecurityException이 발생됩니다. ContentResolver.query()는 (만약 프로바이더가 요구한다면) 읽기 관련 퍼미션을 가지고 있어야 하고, ContentResolver.insert(), ContentResolver.update()ContentResolver.delete()는 쓰기 관련 퍼미션을 가지고 있어야 하며, 이 모든 경우에 대하여 요구되는 퍼미션을 가지고 있지 않은 경우에는 SecurityException이 발생됩니다.


sendBroadcast()할 때 퍼미션 적용하기

 

위에서 설명한 바와 같이 브로드캐스트 리시버의 퍼미션은 누가 그 리시버에게 알림을 발송할 수 있는지를 제한할 수도 있지만, 반대로 어떤 리시버가 내 알림을 받을 수 있는지를 제한할 수도 있습니다. Context.sendBroadcast()를 호출할 때 퍼미션을 인자로 넘기면, 그 퍼미션을 가지고 있는 리시버만이 알림을 받을 수 있습니다. 

알림을 보내는 쪽(sendBroadcast() 호출)과 알림을 받는 쪽(브로드캐스트 리시버) 모두가 퍼미션을 요구하는 경우도 있을 수 있습니다. 이러한 경우에는 양쪽 모두 퍼미션들을 제대로 가지고 있어야 알림을 제대로 전달받을 수 있습니다.


다른 프로세스의 퍼미션 사용하기


원격 서비스 호출시에도 Context.checkCallingPermission()을 사용하여 퍼미션을 확인할 수 있습니다. 메소드 호출시 퍼미션을 인자로 넘기면, 현재 프로세스가 그 퍼미션을 가지고 있는지를 리턴해 줍니다. 단, 이 메소드는 서로 다른 프로세스간 통신(IPC)일 때에만 사용가능하며, 보통 IDL인터페이스를 통해 사용됩니다. 다시 말해서 Context.checkCallingPermission()은, 다른 앱의 서비스를 바인드하여 필요한 메소드를 호출할 때 먼저 내 앱이 퍼미션을 가지고 있는지를 확인하기 위하여 사용되는 것입니다.

퍼미션을 체크하는 방법 몇 가지를 더 알려드리겠습니다. 만약 다른 프로세스의 pid를 알고 있다면, 그 프로세스가 퍼미션을 가지고 있는지 확인하기 위해 Context.checkPermission(String, int, int)를 사용할 수 있습니다. 그리고 만약 다른 앱의 대표 패키지명을 알고 있다면, 그 앱이 퍼미션을 가지고 있는지 확인하기 위해 PackageManager.checkPermission(String, String)을 사용할 수 있습니다.


URI 퍼미션


지금까지 설명한 표준 퍼미션 시스템은 컨텐트 프로바이더를 사용할 때 다소 부족한 점이 있습니다. 컨텐트 프로바이더는 프로바이더 자체에 대해 읽기 및 쓰기 관련 퍼미션을 적용하고 있는데 반해, 프로바이더를 사용하고자 하는 클라이언트는 특정 URI들만 필요로 할 수 있기 때문입니다. 한가지 예로 이메일 앱에 이미지가 첨부되어 있고, 이것을 이미지 뷰어 앱으로 보는 경우를 들 수 있습니다. 이메일 앱은 민감한 사용자 정보를 다루기 때문에 퍼미션들에 의해 보호되어야 하며, 이미지 뷰어 앱은 이메일 내용에 대해서는 알 필요가 없기 때문에 이메일 관련 퍼미션은 가지고 있지 않습니다. 

이러한 경우의 해결책이 바로 URI당 퍼미션(per-URI permissions)입니다. 액티비티를 시작할 때나(startActivity()), 시작된 액티비티에서(startActivityForResult()) 돌려줄 결과값을 추가할 때, 인텐트에 Intent.FLAG_GRANT_READ_URI_PERMISSIONIntent.FLAG_GRANT_WRITE_URI_PERMISSION을 설정할 수 있습니다. 이것은 컨텐트 프로바이더에 대한 퍼미션과 상관 없이 특정 URI에 대한 읽기 또는 쓰기 권한을 주는 것입니다.

이 메카니즘은 사용자와의 상호작용(첨부파일을 열거나, 주소록의 항목을 선택하는 등)을 통해 세분화된 퍼미션을 임시로 허용해 줍니다. 만약 이런 방법이 없고 이미지 뷰어 앱으로 첨부된 이미지를 봐야 한다면, 첨부된 이미지를 볼 수 있게 하는 퍼미션을 만들어야 할 것입니다. 이렇게 특정 앱들간의 직접적인 문제들로 인해 퍼미션들이 생성되는 것을 줄이는 것이 URI당 퍼미션의 중요한 목적 중 하나입니다.

하지만 이것은 컨텐트 프로바이더의 협조를 필요로 합니다. 컨텐트 프로바이더는 URI당 퍼미션을 허용하기 위해, 매니페스트 파일의 프로바이더 요소 안에 android:grantUriPermissions 속성을 추가하거나, <grant-uri-permissions> 태그를 추가해야 합니다.

더 자세한 내용은 Context.grantUriPermission(), Context.revokeUriPermission(), Context.checkUriPermission() 메소드에 대한 레퍼런스 정보에서 확인하시기 바랍니다. 


이어서 학습할 내용들:

암묵적으로 기능을 요구하는 퍼미션들

내 앱에서 요구되는 퍼미션이 디바이스에서 제공하는 하드웨어나 소프트웨어 기능을 암묵적으로 요구하는 경우에 대하여 학습합니다.

<uses-permission>

내 앱이 필요로 하는 퍼미션을 매니페스트 파일에 선언하는 방법을 학습합니다.

Manifest.permission

모든 시스템 퍼미션에 대한 API 레퍼런스를 학습합니다.


추가로 학습할 내용들:

기기 호환성 (Device Compativility)

다양한 종류의 디바이스들에서 안드로이드가 어떻게 동작하는지에 대해서와, 그러한 디바이스들의 제공 가능한 기능들과 관련하여 어떻게 최적화시킬 수 있는지에 대해 자세히 학습합니다.

안드로이드 보안 훑어보기 (Android Security Overview)

안드로이드의 보안 모델에 대하여 학습합니다.


Posted by 개발자 김태우
,

(원문: http://developer.android.com/guide/practices/compatibility.html)

(위치: Develop > API Guides > Device Compatibility)

(전체 번역 글 목록으로 이동하기)


기기 호환성   


안드로이드는 다양한 종류의 디바이스에서 구동될 수 있도록 디자인 되었습니다 (폰, 태블릿, 텔레비전 등). 내 앱이 다양한 종류의 디바이스에서 실행될 수 있다면 그만큼 더 많은 잠재적 고객을 실제 고객으로 유도할 수 있을 것입니다. 이렇게 다양한 디바이스에서 성공적으로 실행되려면, 일부 기능들은 제한될 수 있고 다양한 환경들에 맞게 사용자 인터페이스도 융통성이 있어야 합니다.

그러기 위하여 안드로이드는, 다양한 환경에서 각 환경에 적합한 앱 리소스를 제공할 수 있는 프레임웍을 제공합니다 (예를 들어, 각기 다른 스크린 사이즈에 대하여 각각에 맞는 레이아웃 xml파일을 제공하도록 할 수 있습니다). 앱 프로젝트에서 각 환경에 맞게 리소스들을 잘 배치했다면, 런타임에 안드로이드가 디바이스의 현재 환경을 확인하여 그 환경에 맞는 리소스를 로드합니다. 따라서, 다양한 환경에 대한 리소스들을 미리 잘 준비한다면 하나의 APK파일로 그러한 환경들에 최적화시켜 출시할 수 있습니다.

그러나 필요에 따라서는 앱이 필요로 하는 기능을 명시하여, 구글플레이를 통해 설치할 수 있는 디바이스 종류를 제한할 수 있습니다. 여기서는 내 앱을 설치할 수 있는 디바이스의 종류를 어떻게 조정할 수 있는지와, 타겟 사용자들이 내 앱을 잘 이용할 수 있도록 무엇을 준비해야 하는지에 대해서 설명하도록 하겠습니다. 다양한 디바이스에서 잘 동작하는 앱을 만들기 위한 자세한 내용은 다양한 디바이스 지원하기에서 학습하실 수 있습니다.


"호환성(Compatibility)"이란?


안드로이드 개발과 관련하여 학습을 하고 계시다면, "호환성"이라는 단어를 많이 접하셨을 것입니다. 호환성은 크게 기기 호환성과 앱 호환성으로 나뉩니다.

안드로이드는 오픈 소스 프로젝트이기 때문에 하드웨어 제조업체들이 안드로이드 OS를 탑재한 디바이스를 만들 수 있습니다. 안드로이드 실행환경 (Android execution environment)에서 실행되도록 만들어진 앱을 디바이스에서 실행시킬 수 있다면, 그 디바이스는 "안드로이드와 호환된다"라고 표현합니다. 안드로이드 실행환경은 안드로이드 호환성 프로그램(Android compatibility program)에서 구체적으로 정의되어 있으며, 안드로이드와 호환되고자 하는 모든 디바이스는 호환성 테스트 수트(Compatibility Test Suite, CTS)를 통과해야 합니다.

앱 개발자들은 어떤 디바이스가 안드로이드와 호환되고 호환되지 않는지에 대하여 고민할 필요가 없습니다. 왜냐하면 오직 안드로이드와 호환되는 디바이스에만 구글플레이 스토어가 설치되어 있기 때문입니다. 따라서 안드로이드와 호환되지 않는 디바이스에서는 앱을 설치할 수 없기 때문에 개발자는 안심해도 되는 것입니다.

그러나 앱 개발자들은 다양한 디바이스 환경에 대하여 앱이 호환되도록 노력해야 합니다. 안드로이드와 호환되는 디바이스의 종류가 상당히 많고, 그 중에서는 앱이 필요로 하는 일부 기능이 없는 디바이스도 있을 수 있기 때문입니다. 예를 들어, 내 앱의 핵심 기능이 나침반 센서를 이용하고 있다면, 내 앱은 나침반 센서를 내장하고 있는 디바이스에서만 호환되는 것입니다.


내 앱의 설치 가능성 다루기

(Controlling Your App's Availability to Devices)


안드로이드는 내 앱이 다양한 기능을 구현할 수 있도록 플랫폼 API들을 제공합니다. 어떤 기능은 나침반 센서와 같은 하드웨어 기반의 API를 사용할 것이고, 또 어떤 기능은 앱 위젯과 같은 소프트웨어 기반의 API를 사용할 것입니다. 그리고 일부 기능들은 플랫폼의 버전에 의존적일 수 있습니다. 여기서 중요한 것은, 모든 디바이스가 이러한 모든 기능들을 지원하지는 않는다는 것입니다. 따라서 내 앱이 필요로 하는 기능에 따라 어떤 종류의 디바이스에서 설치될 수 있고, 또 어떤 종류의 디바이스에서 설치될 수 없는지를 정의하는 것이 필요합니다.

개발자는 최대한의 사용자를 확보하기 위하여, APK파일이 최대한 많은 종류의 디바이스에서 실행될 수 있도록 노력해야 합니다. 대부분의 경우, 런타임때 옵셔널한 기능은 비활성화하고, 다양한 환경에 최적화된 앱 리소스를 제공하도록 합니다 (예를 들면, 각각의 스크린 사이즈에 맞는 각각의 레이아웃 제공). 하지만 구글플레이 스토어를 통한 앱의 설치 가능성을 제한해야할 필요가 있는 경우도 있습니다. 그러한 경우 아래 세가지 속성을 잘 이용해야 합니다.


디바이스 기능 (Device Features)

디바이스가 지원하는 기능들에 따라 내 앱의 설치 가능성을 관리할 수 있도록 하기 위하여, 안드로이드는 하드웨어 및 소프트웨어 기능에 기능ID (feature IDs)를 정의해 놨습니다. 예를 들면, 나침반 센서의 기능ID는 FEATURE_SENSOR_COMPASS 이고, 앱 위젯의 기능ID는 FEATURE_APP_WIDGETS 입니다 (FEATURE_SENSOR_COMPASS과 FEATURE_APP_WIDGETS는 PackageManager 클래스의 정적 멤버 변수입니다).

필요하다면 매니페스트 파일<uses-feature> 요소를 추가함으로써 해당 기능을 지원하지 않는 디바이스에 내 앱이 (구글플레이 스토어를 통해서) 설치되는 것을 방지할 수 있습니다.

예를 들어, 내 앱이 나침반 센서가 없는 디바이스에 설치되지 않기를 원한다면 매니페스트 파일에 아래와 같이 추가하면 됩니다.

<manifest ... >
   
<uses-feature android:name="android.hardware.sensor.compass"
                 
android:required="true" />
    ...
</manifest>

구글플레이 스토어는 내 앱의 매니페스트에 선언된 기능들과 사용자의 디바이스에서 지원하는 기능들을 비교하여 내 앱이 그 디바이스와 호환되는지를 판단합니다. 그래서 내 앱이 필요로 하는 기능 중에 하나라도 지원하지 않는 것이 있다면, 구글플레이 스토어는 해당 디바이스에게 내 앱을 보여주지 않습니다.

하지만 디바이스의 어떤 기능을 내 앱에서 사용하기는 하지만 그 기능이 없어도 대세에 지장을 주지는 않는다고 한다면, android:required"false"로 하여 설치 가능하게 하고, 런타임에서 그 디바이스 기능을 체크하여 그 기능만 사용하지 못하게 막으면 됩니다. 예를 들면, 아래와 같이 hasSystemFeature() 메소드를 호출하여 디바이스 기능의 가능여부를 체크할 수 있습니다.

PackageManager pm = getPackageManager();
if (!pm.hasSystemFeature(PackageManager.FEATURE_SENSOR_COMPASS)) {
   
// This device does not have a compass, turn off the compass feature
    disableCompassFeature
();
}

내 앱이 구글플레이 스토어에서 타겟 사용자들을 필터링하는 방법에 대한 더 자세한 내용은 구글플레이에서의 앱 필터링에서 학습하실 수 있습니다.

  • 노트: 일부 시스템 퍼미션은 암묵적으로 디바이스의 특정 기능을 필요로 하기도 합니다. 예를 들면, 내 앱이 BLUETOOTH 퍼미션을 요청한다면 이것은 암묵적으로 디바이스 기능 중 FEATURE_BLUETOOTH를 필요로 하는 것입니다. 이러한 경우, 블루투스 기능이 없는 디바이스에서도 내 앱이 설치되도록 하려면, <uses-features>요소의 android:required 속성을 "false"로 하여 필터링을 없애면 됩니다. 이렇게 암묵적으로 디바이스 기능을 필요로 하는 경우들에 대하여 더 자세한 내용은 암묵적으로 기능을 요구하는 퍼미션들에서 학습하실 수 있습니다. 


플랫폼 버전 (Platform version)

디바이스들은 서로 다른 버전의 안드로이드 플랫폼 상에서 구동될 수 있습니다. 안드로이드 플랫폼은 기능 추가 등의 이유로 계속해서 버전 업데이트가 이뤄지고 있는데, 이러다보면 당연히 후속 버전에 추가된 API들을 이전 버전에서는 사용할 수 없는 경우가 많습니다. 그래서 API들의 셋을 구분하기 위하여 각 플랫폼은 API 레벨을 지정해 둡니다. 예를 들어, 안드로이드 버전1.0은 API 레벨 1이고 안드로이드 버전4.4는 API 레벨 19입니다.

내 앱이 실행될 수 있는 최소 버전을 지정할 때 <uses-sdk> 요소의 android:minSdkVersion 속성에 API 레벨값을 넣어 줍니다.

예를 들어, 캘린더 프로바이더(Calendar Provider)의 API들은 안드로이드 버전 4.0 (API레벨 14)에서 추가되었는데, 만약 내 앱이 그 API들 없이는 동작하지 않는다면 minSdkVersion을 14로 설정해야 합니다.

<manifest ... >
   
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="19" />
    ...
</manifest>

minSdkVersion 속성에는 내 앱이 호환되는 최소 버전을 선언하고, targetSdkVersion 속성에는 내 앱에 최적화된 버전 중 가장 높은 것을 선언합니다.

후속 버전에서는 이전 버전에서 만들어진 앱이 호환됩니다 (하위호환). 따라서 내 앱이 API들을 매뉴얼대로 사용했다면, 안드로이드 버전이 계속 업데이트 되더라도 항상 최신 버전과 호환됩니다.

  • 노트: targetSdkVersion 속성에 버전을 설정한다고 해서 그보다 높은 버전의 플랫폼에 설치되지 않는 것은 아닙니다. 그러나, 보다 높은 버전에서 변경된 내용들에 대하여 시스템에게 인지시켜 준다는 점에서 중요합니다. 만약, 실제로는 최신 버전의 API들을 사용하여 개발을 해놓고 targetSdkVersion은 그보다 낮은 버전으로 선언해 뒀다면, 내 앱이 최신 버전에서 구동될 때 시스템은 내 앱에 대하여 하위호환이 필요하다고 판단해 버립니다. 안드로이드 버전 4.4에서 변경된 내용 중 하나를 예로 들겠습니다. 알람 매니저(AlarmManager) API에 의해 생성된 알람은 기본적으로 약간 부정확합니다. 그래서 버전 4.4인 시스템은 앱의 알람들을 일괄처리하고 시스템 전원을 절약하는데, 만약 targetSdkVersion이 19보다 더 낮게 선언되어 있으면 앱이 낮은 버전에 맞춰져 있다고 가정하여 4.4에서 개선된대로 실행하지 않고 이전처럼 실행하게 됩니다. 바꿔 말하면, 플랫폼이 업데이트 되면서 로직을 개선했더라도 이전 버전에 맞춰진 앱은 새로운 로직에 대해 문제를 일으킬 수도 있기 때문에 예전 방식으로 실행하게 되는 것입니다.

하지만, 내 앱이 안드로이드 최신 버전의 API를 사용하는데 그게 꼭 필요한 기능은 아니라고 한다면, 낮은 버전의 플랫폼에서 실행되도록 하기 위해 런타임에서 API 레벨을 체크하여 비활성화할 수 있습니다. 그리고 이러한 경우, minSdkVersion에 지원 가능한 최소 버전을 선언해 줘야 하고, 소스코드에서 Build.VERSION.SDK_INT(현재 시스템의 버전)과 Build.VERSION_CODES.XXX(비교하고자 하는 코드명)을 비교하면 됩니다. 예를 들면 아래와 같습니다.

if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
   
// Running on something older than API level 11, so disable
   
// the drag/drop features that use ClipboardManager APIs
    disableDragAndDrop
();
}


스크린 형태 (Screen configuration)

안드로이드는 스마트폰, 태블릿, TV에 이르기까지 다양한 크기의 디바이스에 탑재되어 있습니다. 이렇게 다양한 스크린의 종류를 구분하기 위해, 안드로이드는 크게 두가지 특성에 따라 그룹화를 합니다. 한가지는 스크린의 (물리적인) 크기이고, 다른 한가지는 픽셀의 밀도(DPI, 1인치에 들어가 있는 픽셀수)입니다. 이러한 구분들을 단순화 및 그룹핑하여 붙여놓은 명칭들은 아래와 같습니다.

  • 스크린 크기: small, normal, large, xlarge
  • 픽셀의 밀도: mdpi (medium), hdpi (high), xhdpi (extra high), xxhdpi (extra-extra high) 등등

기본적으로, 내 앱은 특별히 신경쓰지 않아도 모든 스크린 크기와 픽셀 밀도에 호환됩니다. 그것은 시스템이 스크린 종류에 따라 UI 레이아웃과 이미지들을 적절히 조정해 주기 때문입니다. 그러나 그것이 완벽하지는 않기 때문에 최적화된 사용자 경험을 제공하기 위해서, 스크린 크기에 따라 그에 맞는 레이아웃 파일을 추가하고, 픽셀 밀도에 따라 그에 맞는 이미지들을 추가할 수 있습니다. (일반적으로, 스크린 종류 한가지를 주요 타겟으로 삼아 리소스 세트를 만들고, 부분적으로 최적화가 필요한 것들에 대하여 추가로 리소스를 추가하는 방식으로 개발을 진행합니다.)

다양한 스크린 종류에 대응하기 위해 리소스를 만드는 방법과 내 앱이 특정 스크린 크기는 지원하지 않도록 하는 방법 등은 다양한 스크린 지원하기에서 학습하실 수 있습니다.


사업적 이유에 따른 설치 가능성 다루기

(Controlling Your App's Availability for Business Reasons)


위에서는 디바이스의 종류에 따라 앱이 설치 되고 안되고 하는 내용을 설명했습니다. 하지만 사업적인 이유나 법적인 이유로 인하여 앱의 설치 가능성을 조정해야 할 필요도 있습니다. 예를 들어 런던 지하철 시간표를 보여주는 앱을 만들었다면, 이것은 영국 사람들 이외에는 별로 필요하지 않을 것입니다. 이러한 경우, 구글플레이 스토어의 개발자 콘솔에서 출시국가를 조정할 수 있습니다. 다시 말해서, 출시국가나 이동통신사와 같이 비기술적인 이유로도 앱의 설치 대상을 필터링할 수 있다는 것입니다.

기술적인 이유(디바이스가 요구하는 기능들)로 인한 필터링은 항상 APK 파일의 매니페스트 파일을 참조하고, 비기술적인 이유(출시국가 등)로 인한 필터링은 항상 구글플레이 개발자 콘솔에서 조정합니다. 


이어서 학습할 내용들:

리소스 제공하기 (Providing Resources)

앱에서 리소스가 소스코드와 어떻게 분리되어 있는지와 다양한 디바이스 환경에 최적화된 리소스를 어떻게 제공할 수 있는지에 대하여 자세히 학습합니다.

구글플레이에서의 앱 필터링 (Filters on Google Play)

구글플레이 스토어를 통해 내 앱의 설치 가능성을 조정하는 방법에 대하여 자세히 학습합니다.


추가로 학습할 내용들:

시스템 퍼미션 (System Permissions)

사용자의 동의를 필요로 하는 API들에 대하여 안드로이드가 어떻게 접근 제한을 하는지에 대해 자세히 학습합니다.


Posted by 개발자 김태우
,

(원문: 

http://developer.android.com/guide/components/fundamentals.html)

(위치: Develop > API Guides > App Fundamentals)

(전체 번역 글 목록으로 이동하기)


안드로이드 앱의 기본 개념


안드로이드 앱은 자바 언어로 개발됩니다. 안드로이드 SDK가 자바 소스코드와 레이아웃 xml파일 및 이미지 등의 리소스 파일들을 APK 파일로 만들어 줍니다. APK파일은 Android package를 가리키며 확장자가 .apk 입니다. 하나의 APK파일이 하나의 앱이며, 안드로이드 디바이스에 설치되는 파일입니다.

디바이스에 앱이 설치되고 나면, 각각의 앱은 각각의 환경에서 보안이 유지된 상태로 실행됩니다.

  • 안드로이드 OS는 멀티유저 리눅스 시스템이라서, 각각의 앱은 각각 다른 유저가 됩니다.
  • 기본적으로 각각의 앱은 시스템으로부터 유니크한 리눅스 유저ID를 할당 받습니다 (리눅스 유저ID는 시스템에서 내부적으로 관리하는 값이며, 앱은 그 값을 알지 못하고 알 필요도 없습니다). 앱의 모든 파일들은 퍼미션이 설정되어 해당 유저ID를 갖는 앱 만이 접근할 수 있습니다.
  • 각 프로세스는 자신만의 가상머신(VM)을 갖기 때문에, 앱의 코드는 다른 앱들과 분리된 공간에서 실행됩니다.
  • 기본적으로 모든 앱은 각각의 리눅스 프로세스에서 실행됩니다. 안드로이드는 프로세스를 실행하여 앱의 컴포넌트를 실행하고, 프로세스가 더이상 필요 없다고 판단되거나, 프로세스가 실행 중이 아닐 때 다른 앱에서 메모리를 필요로 하면 해당 프로세스를 종료하여 메모리를 관리합니다.

위와 같이, 안드로이드 시스템은 최소 특권의 원리(principle of least privilege)에 따릅니다. 다시 말해서, 앱은 뭔가 할일이 있는 컴포넌트에게만 접근 권한을 부여하며, 이것은 권한이 없는 부분에는 접근하지 못하게 함으로써 매우 강한 보안 환경을 제공합니다.

그러나 다른 앱들과 데이터를 공유하거나, 시스템 서비스에 접근할 수 있는 방법들이 있습니다.

  • 두 개의 앱이 하나의 리눅스 유저ID를 공유하는 것이 가능합니다. 이러한 경우 두 개의 앱이 서로의 파일에 접근이 가능해집니다. 시스템 리소스를 아끼기 위해 이러한 두 개의 앱은 하나의 리눅스 프로세스에서 실행되고 하나의 가상머신을 공유합니다. (다만, 똑같이 signing되어야 합니다. 바꿔 말하면, 빌드할때 같은 sign key를 사용해야 합니다.)
  • 앱은 사용자의 주소록이나 SMS메시지, SD카드, 카메라, 블루투스 등을 사용하기 위해 접근 권한을 요청할 수 있습니다. 이런한 모든 권한은 앱을 설치하는 과정에서 사용자가 확인할 수 있으며, 해당 권한들을 사용하는 데에 동의함으로써 사용자가 직접 권한을 부여하게 됩니다.

위의 내용들은 안드로이드 앱이 시스템 내에서 어떻게 동작하는지에 대한 기본적인 내용을 설명했습니다. 아래 내용들에서 더 많은 개념들을 소개하겠습니다.

  • 앱을 개발하는데에 필요한 핵심적인 프레임웍 컴포넌트
  • 컴포넌트 및 필요한 기능들을 정의하는 manifest 파일
  • 소스코드와는 별도로 존재하는 리소스들과, 앱을 다양한 디바이스 환경에 최적화 시키는 방법들


앱의 컴포넌트 (App Components)


컴포넌트들은 안드로이드 앱의 필수적인 구성 요소입니다. 각각의 컴포넌트는 앱의 진입점이 될 수 있습니다. 모든 컴포넌트가 사용자에게 직접적인 진입점이 되는 것은 아니고 일부는 다른 컴포넌트에 의존적이기도 하지만, 각각은 독립적으로 그들만의 역할이 있는 앱의 구성원입니다.

앱의 컴포넌트에는 4가지가 있습니다. 각각은 그들만의 목적이 있고, 생성되어 소멸되기까지의 생명주기를 갖습니다.

4가지 앱 컴포넌트들은 아래와 같습니다.


액티비티 (Activities)

액티비티는 사용자 인터페이스를 제공하는 하나의 화면을 표현합니다. 예를 들어, 이메일 앱은 아직 읽지 않은 메일 목록을 보여주는 액티비티가 있을 수 있고, 이메일 내용을 보는 액티비티가 있을 수 있으며, 읽은 이메일 목록을 보여주는 액티비티가 있을 수 있습니다. 비록 이러한 액티비티들이 이메일이라는 하나의 목적을 갖고 사용자에게 하나와 같은 경험을 제공하기는 하지만, 그것들 각각은 서로 독립적으로 존재합니다. 따라서, (이메일 앱이 허용하고 있다면) 다른 앱이 이메일 앱의 액티비티들을 각각 따로 실행할 수도 있습니다. 예를 들어, 카메라 앱에서 사진을 공유하기 위해 이메일 앱의 새편지 쓰기 화면을 실행할 수 있습니다.

앱의 액티비티는 프레임웍의 Activity 클래스를 상속받아 구현할 수 있으며 자세한 내용은 Activity에 대하여에서 학습하실 수 있습니다.


서비스 (Services)

서비스는 시간이 오래 걸리는 작업이나 원격 프로세스 호출에 의한 작업을 백그라운드에서 실행할 수 있는 컴포넌트이며, 사용자 인터페이스를 제공하지 않습니다. 예를 들어, 다른 앱이 실행되는 동안에도 음악을 재생할 수 있고, 사용자가 앱에서 다른 행동을 하는 동안에 네트웍을 통해 데이터를 받아오는 일을 할 수도 있습니다. 서비스는 액티비티 및 다른 컴포넌트로부터 두 가지 방법으로 실행될 수 있습니다. 반환값이 없는 메소드 호출하듯이 그냥 호출하는 방법이 있고, 서비스를 바인드하여 객체의 메소드 호출하듯이 상호작용하는 방법이 있습니다. 

앱의 서비스는 프레임웍의 Service 클래스를 상속받아 구현할 수 있으며 자세한 내용은 Service에 대하여에서 학습하실 수 있습니다. 


컨텐트 프로바이더 (Content providers)

컨텐트 프로바이더는 앱의 데이터를 다른 앱에 공유할 수 있도록 해줍니다. 데이터는 파일 시스템 및 SQLite 데이터베이스로 저장 가능하고, 웹이나 그 외에도 앱이 접근할 수 있는 저장 공간이 있다면 저장 가능합니다. 다른 앱들은 (컨텐츠 프로바이더가 허용하고 있다면) 컨텐츠 프로바이더를 통하여 데이터를 질의하거나 수정할 수 있습니다. 예를 들어, 안드로이드 시스템은 주소록 정보를 관리하는 컨텐트 프로바이더를 제공하며, 관련 퍼미션을 갖고 있는 앱들이라면 주소록 정보를 읽거나 수정할 수 있습니다. 

컨텐트 프로바이더는 외부에 공유하지 않는 자체 데이터를 읽거나 수정하는데에도 사용할 수 있습니다. 예를 들어, Note Pad 샘플앱은 노트를 저장할 때 컨텐트 프로바이더를 사용합니다.

앱의 컨텐트 프로바이더는 프레임웍의 ContentProvider 클래스를 상속받아서 다른 앱들이 요청하는 것들에 필요한 표준화된 API들을 구현하면 됩니다. 자세한 내용은 Content Provider에 대하여에서 학습하실 수 있습니다. 


브로드캐스트 리시버 (Broadcast Receivers)

브로드캐스트 리시버는 시스템 전체에 대해 발송(broadcast)된 알림을 수신하는 컴포넌트입니다. 알림의 상당수는 시스템에서 발송되는 것들인데, 예를 들면, 화면이 꺼졌을 때나 배터리가 얼마 남지 않았을 때, 사진을 찍었을 때 관련된 알림이 발송됩니다. 앱은 어떤 데이터를 모두 다운로드 했고 사용 가능해졌다는 것을 다른 앱들에게 알리기 위해 알림을 발송할 수 있습니다. 브로드캐스트 리시버는 사용자 인터페이스가 없지만, 알림을 수신하여 노티피케이션바에 내용을 표시하는 방식으로 사용자에게 보여줄 수 있습니다. 그러나 알림을 받아서 다른 컴포넌트를 실행시켜 주는 게이트웨이의 역할을 수행하는 것이 더 일반적입니다. 

앱의 브로드캐스트 리시버는 프레임웍의 BroadcastReceiver 클래스를 상속받아서 구현할 수 있으며 Intent를 통해 데이터를 전달 받습니다. 자세한 내용은 BroadcastReceiver에 대하여에서 학습하실 수 있습니다.


안드로이드 시스템 디자인의 독특한 점 중 한가지는 어떤 앱이던 다른 앱의 컴포넌트를 실행할 수 있다는 것입니다. 예를 들어, 앱에 사진 찍는 기능이 필요하다면 사진 찍는 액티비티를 따로 구현할 필요 없이 카메라 앱의 액티비티를 실행할 수 있습니다. 이때 카메라 앱을 내 앱에 포함하거나 카메라 관련 객체를 불러다 사용할 필요가 없습니다. 대신에 단순히 카메라 앱의 사진 찍는 액티비티를 실행하면 됩니다. 그리고 사진을 찍고 나면 데이터를 리턴 받아 사용할 수 있습니다. 이것은 사용자에게 마치 카메라 앱이 내 앱의 일부인 것처럼 보이게 합니다. 

시스템은 컴포넌트를 실행할 때 (이미 프로세스가 실행되고 있지 않다면) 해당 앱의 프로세스를 구동시키며 필요한 클래스들을 준비합니다. 예를 들어, 내 앱에서 사진을 찍기 위해 카메라 앱의 액티비티를 실행한다면 이것은 내 앱의 프로세스가 아니라 카메라 앱의 프로세스에서 실행됩니다. 달리 말하자면 안드로이드 시스템은 다른 시스템과 달리 진입점이 유일하지 않다는 것입니다( main()함수가 없다는 의미입니다 ). 

시스템은 앱의 파일들이 다른 앱에서 사용되는 것을 막기 위해 각 앱은 각각의 프로세스에서 실행되도록 하기 때문에, 내 앱이 다른 앱의 컴포넌트를 직접적으로 실행하지는 못합니다. 대신에 컴포넌트를 실행시키기 위한 인텐트(intent)를 만들어서 시스템에 전달해 주면 시스템이 해당 컴포넌트를 실행시켜 줍니다. 


컴포넌트 실행시키기

4가지의 컴포넌트 중 3가지(액티비티, 서비스, 브로드캐스트 리시버)는 인텐트라 불리는 비동기 메시지를 통해 실행됩니다. 인텐트는 런타임에 컴포넌트를 실행하여 그것들을 서로 연결시켜 주며, 실행된 컴포넌트를 내 앱 또는 해당 앱에 포함시킵니다. 

인텐트는 Intent 클래스의 객체로 생성되며 컴포넌트를 실행하기 위한 메시지를 정의합니다. 인텐트는 명시적(explicit)이거나 암묵적(implicit)일 수 있는데, 인텐트에 실행할 컴포넌트가 정의되어 있으면 명시적이라 하고, 컴포넌트 대신에 다른 속성이 정의되어 있으면 암묵적이라고 합니다.

액티비티와 서비스를 실행하기 위해 인텐트는 "view"나 "send"와 같은 액션값을 정의하고, 필요한 데이터의 URI를 추가하기도 합니다. 예를 들어, 이미지를 보거나 웹페이지를 열기 위한 요청을 액티비티에 전달하기도 합니다. 어떤 경우에는 원하는 결과값을 얻기 위해 액티비티를 실행하기도 하는데 이때 결과값을 인텐트를 통해 전달 받습니다. 예를 들어, 주소록 액티비티를 실행하여 친구를 선택하면 선택된 친구의 정보를 가리키는 URI 값을 인텐트를 통해 받을 수 있습니다. 

브로드캐스트 리시버에서 전달받은 인텐트에는 발송된 알림이 정의되어 있습니다. 예를 들어, 배터리가 얼마 남지 않았을 때 발송되는 인텐트에는 "배터리가 얼마 남지 않았다"를 의미하는 액션값이 정의되어 있습니다.

4가지의 컴포넌트 중 나머지 1가지인 컨텐트 프로바이더는 인텐트로 실행되지 않습니다. 대신에 컨텐트 리졸버(ContentResolver)로부터 요청 받을 때 실행됩니다. 컨텐트 리졸버는 컨텐트 프로바이더가 제공하는 데이터들을 요청할 때 사용합니다. 다시 말해서 데이터를 요청하는 컴포넌트가 프로바이더에게 직접 요청하지 않고 컨텐트 리졸버를 통해서 요청하는 것입니다. 이것은 보안상의 이유로 데이터를 요청하는 컴포넌트와 데이터를 제공하는 컨텐트 프로바이더 사이에 추상적인 레이어를 하나 뒀다고 생각하면 됩니다.

아래는 각 컴포넌트를 실행하는 메소드들입니다.

  • 액티비티 실행: startActivity() 또는 startActivityForResult()에 인텐트를 전달하여 액티비티를 실행할 수 있습니다. (결과값을 리턴 받고자 할 때 startActivityForResult()를 호출합니다.)
  • 서비스 실행: startService() 또는 bindService()에 인텐트를 전달하여 서비스를 실행할 수 있습니다. 
  • 브로드캐스트 리시버 실행: sendBroadcast() 또는 sendOrderedBroadcast() 또는 sendStickyBroadcast()에 인텐트를 전달하여 알림을 발송할 수 있고, 알림이 발송되면 그 알림을 받도록 정의된 브로드캐스트 리시버가 실행됩니다.
  • 컨텐트 프로바이더 실행: 컨텐트 리졸버의 query() 메소드를 통해 컨텐트 프로바이더를 실행하고 데이터를 질의할 수 있습니다.

관련하여 더 자세한 내용은 아래 링크된 문서들을 통해 학습하실 수 있습니다.

Intent와 Intent Filter

Activity에 대하여

Service에 대하여

BroadcastReceiver에 대하여

Content Provider에 대하여 


매니페스트 파일 (The Manifest File)


안드로이드 시스템은 앱 컴포넌트를 실행하기 전에 매니페스트 파일 (AndroidManifest.xml)을 읽어보면서 해당 컴포넌트가 존재하는지 확인합니다. 따라서 앱의 모든 컴포넌트는 매니페스트 파일에 선언되어 있어야 하며, 매니페스트 파일은 앱 프로젝트의 루트 디렉토리에 있어야 합니다.

매니페스트 파일은 앱 컴포넌트를 선언하는 것 외에도 몇 가지 할 일이 더 있습니다.

  • 앱이 필요로 하는 퍼미션들을 선언합니다. 예를 들면, 인터넷 접속이나 주소록 정보 접근 권한 등이 있습니다. (<uses-permission>)
  • 최소 API 레벨을 선언합니다. 이것은 앱이 사용하는 API들이 API 레벨 몇부터 지원되는 것인지를 의미합니다. (<uses-sdk>)
  • 앱이 필요로 하는 하드웨어 및 소프트웨어 기능들을 선언합니다. 예를 들면, 카메라나 블루투스, 멀티터치 기능 등이 있습니다. (<uses-feature>)
  • 앱이 필요로 하는 API 라이브러리를 선언합니다. 예를 들면, 구글 맵스 라이브러리가 있습니다. (<uses-library>)


컴포넌트 선언하기

매니페스트의 가장 주된 일은 앱의 컴포넌트들을 시스템에게 알려주는 것입니다. 예를 들면, 매니페스트 파일에서 액티비티를 선언하는 방법은 아래와 같습니다.

<?xml version="1.0" encoding="utf-8"?>
<manifest ... >
   
<application android:icon="@drawable/app_icon.png" ... >
       
<activity android:name="com.example.project.ExampleActivity"
                 
android:label="@string/example_label" ... >
       
</activity>
        ...
   
</application>
</manifest>

<application> 요소 안에 있는 android:icon 속성값은 앱의 아이콘 이미지를 가리킵니다.

<activity> 요소 안에 있는 android:name 속성값은 패키지명을 포함한 액티비티의 클래스 이름이고, android:label 속성값은 액티비티의 제목이며 보통 액티비티의 타이틀바에 표시됩니다.

앱의 각 컴포넌트에 대응되는 요소는 다음과 같습니다.

액티비티, 서비스, 컨텐트 프로바이더는 클래스를 만들었다 하더라도 매니페스트 파일에 선언하지 않으면 안드로이드 시스템이 인식하지 못하기 때문에 실행되지 않습니다. 하지만 브로드캐스트 리시버는 조금 다릅니다. 다른 컴포넌트들과 마찬가지로 매니페스트 파일에 선언할 수도 있지만, 소스코드 내에서 동적으로 생성하고 registerReceiver() 메소드를 호출하여 시스템에 등록할 수 있습니다.

매니페스트 파일의 구성에 대한 자세한 내용은 안드로이드 매니페스트 파일에서 학습하실 수 있습니다.


컴포넌트의 역할을 선언하기

위의 컴포넌트 실행시키기에서 학습한 바에 따르면, 액티비티, 서비스, 브로드캐스트 리시버는 인텐트에 컴포넌트명을 넣어서 명시적(explicit)으로 실행할 수 있습니다. 하지만 인텐트의 진정한 힘(장점)은 암묵적(implicit)인 실행에 있습니다. 암묵적 인텐트는 컴포넌트명 없이 액션값을 갖고, 필요에 따라서는 데이터값을 가질 수도 있습니다. 그래서 액션값을 가지고 시스템으로 하여금 해당되는 컴포넌트를 찾아서 실행하게 할 수 있는 것입니다 (시스템은 컴포넌트가 선언한 역할들 중에 해당 액션값이 있는지를 찾습니다). 만약 액션값에 해당되는 컴포넌트가 2개 이상일 경우에는 사용자가 선택할 수 있도록 선택창이 뜹니다.

시스템은 설치된 앱들의 매니페스트 파일에 컴포넌트의 역할로서 선언된 인텐트 필터(intent filters) 중에서 해당 액션값이 있는지를 찾습니다.

매니페스트 파일에 액티비티를 선언할 때, 액티비티의 역할도 함께 선언하기 위해 인텐트 필터를 추가할 수 있습니다. 그러면 다른 앱들이 내 앱을 암묵적 인텐트를 이용하여 실행할 수 있습니다. 인텐트 필터는 그것을 추가하고자 하는 컴포넌트의 요소 안에 <intent-filter> 요소를 추가함으로써 선언할 수 있습니다. 

예를 들어, 이메일 쓰기 액티비티가 있는 이메일 앱을 만들었다면, 그 액티비티가 보내기 액션값(android.intent.action.SEND)이 있는 인텐트에 반응할 수 있도록 아래와 같이 인텐트 필터를 선언할 수 있습니다.

<manifest ... >
    ...
   
<application ... >
       
<activity android:name="com.example.project.ComposeEmailActivity">
           
<intent-filter>
               
<action android:name="android.intent.action.SEND" />
               
<data android:type="*/*" />
               
<category android:name="android.intent.category.DEFAULT" />
           
</intent-filter>
       
</activity>
   
</application>
</manifest>

위와 같이 선언된 액티비티는, 다른 앱이 보내기 액션값을 가진 인텐트를 만들어서 startActivity() 메소드를 호출할 경우 시스템이 찾아서 실행해 줍니다. (Intent클래스의 ACTION_SEND가 android.intent.action.SEND입니다.)

자세한 내용은 Intent와 Intent Filter에서 학습하실 수 있습니다.


앱의 요구조건 선언하기

안드로이드 디바이스의 종류는 다양하고, 제공되는 기능들이 모두 같지는 않습니다. 따라서, 필요로 하는 기능을 제공하지 않는 디바이스에 내 앱이 설치되는 것을 막기 위해서는 매니페스트 파일에 디바이스의 지원 기능과 소프트웨어 요구조건을 선언하는 것이 중요합니다. 하지만 이렇게 선언된 요구조건은 시스템에서 앱을 설치하거나 실행할 때 읽지 않습니다. 다시 말해서 앱을 설치하거나 실행할 때는 아무런 영향을 주지 않는 정보입니다. 그러나 이것은 구글플레이와 같은 외부 서비스에서 대상이 되는 사용자를 필터링하기 위해 사용됩니다. 바꿔 말하면, 어떤 앱에 대하여 필요한 기능이 없는 디바이스에서 구글플레이를 통해 검색하면 그 앱은 보이지 않는다는 말입니다.

예를 들어 카메라 기능을 필요로 하고 안드로이드 2.1버전(API 레벨 7)에서 추가된 API를 사용하는 앱의 경우 매니페스트 파일에 아래와 같이 요구조건을 선언해야 합니다.

<manifest ... >
   
<uses-feature android:name="android.hardware.camera.any"
                 
android:required="true" />
   
<uses-sdk android:minSdkVersion="7" android:targetSdkVersion="19" />
    ...
</manifest>

위와 같이 선언한 앱을 구글플레이에 출시했다면, 카메라 기능이 없거나 안드로이드 버전이 2.1 미만인 디바이스에서 구글플레이를 통해 그 앱을 설치할 수 없습니다. (발견할 수 없기 때문에 설치할 수도 없는 것입니다.)

카메라 기능을 사용하기는 하지만 그것이 필수는 아니라서 카메라 기능이 없는 디바이스에서도 앱을 사용할 수 있었으면 하는 경우가 있을 것입니다. 이러한 경우에는 android:requiredfalse로 설정하면 구글플레이를 통해서도 설치할 수 있으며, 시스템이 런타임에 카메라 기능의 지원 여부를 확인하여 적절히 비활성 처리를 해줍니다. 

다른 디바이스에서의 앱 호환성과 관련해서는 기기 호환성에서 학습하실 수 있습니다.


앱의 리소스 (App Resources)


안드로이드 앱은 자바 소스코드 외에도 이미지 파일, 오디오 파일, 애니메이션, 메뉴, 스타일, 컬러, 레이아웃 xml 파일 등의 리소스 파일들로 구성됩니다. 리소스를 사용함으로써 자바 소스코드를 수정하지 않고도 앱의 다양한 속성들을 업데이트할 수 있으며, 다양한 디바이스 환경에 대하여 각각에 최적화된 리소스 세트를 따로 정의할 수 있습니다. 예를 들면, 2개 이상의 언어를 지원해야 하는 경우나 다양한 스크린 사이즈를 지원해야 하는 경우 각각의 환경에 맞는 리소스 세트를 정의할 수 있습니다.

앱 프로젝트의 모든 리소스 파일들은 프로젝트가 빌드될 때 유니크한 정수형 ID로 정의되며, 그것들은 소스코드나 xml로 정의된 다른 리소스 파일에서 사용할 수 있습니다. 예를 들어 res/drawable/logo.png 파일이 있다고 가정하면, 빌드될 때 생성되는 R.java파일에 drawable 클래스의 정적 멤버 변수로 logo가 정의됩니다. 그러면 자바 소스코드에서는 R.drawable.logo 로 사용할 수 있고, 다른 리소스 xml파일에서는 @drawable/logo 로 사용할 수 있습니다. 

리소스를 소스코드와 분리해 놓은 것의 가장 큰 장점 중 하나는 다양한 환경에 최적화된 리소스를 따로 제공할 수 있다는 것입니다. 예를 들어 앱을 전세계 대상으로 런칭하려 한다면 한글과 영어를 모두 지원해야 할 것이고, 그러기 위해서 res/value/strings.xml 에는 영어 문자열들을 저장하고 res/value-kr/strings.xml 에는 한글 문자열들을 저장하면 됩니다. 그러면 안드로이드 시스템이 디바이스에 설정된 언어에 해당하는 문자열을 찾아서 사용자 UI에 출력해 줍니다.

위의 예에서 value뒤에 붙은 kr과 같은 것을 식별자(qualifier)라고 하는데, 안드로이드는 다양한 환경에 대하여 리소스를 제공할 수 있도록 많은 식별자들을 지원합니다. 다른 예로, 스크린의 가로/세로 여부 및 크기에 따라 다른 레이아웃을 생성할 수 있습니다. 스크린이 세로 모드라면 버튼들을 위아래로 나열하고 싶고 가로 모드라면 좌우로 나열하고 싶을 수 있겠죠. 이렇게 가로 및 세로 모드에 맞게 레이아웃이 변경되게 하고 싶다면 res/ 에 있는 layout 디렉토리에 적당한 식별자를 붙여주고 그에 해당하는 xml파일들을 저장하면 됩니다 (일반적으로 layout 디렉토리가 세로모드를 지원하고, layout-land 디렉토리가 가로모드를 지원합니다). 그러면 안드로이드 시스템이 현재 가로인지 세로인지를 확인하여 그에 맞는 레이아웃 xml파일을 적용하고 사용자에게 보여줍니다.

앱에 추가할 수 있는 다양한 종류의 리소스에 대해서나, 다양한 환경에 최적화된 리소스를 제공하는 방법에 대하여 더 자세한 내용은 리소스 제공하기에서 학습하실 수 있습니다. 


이어서 학습할 내용들:

Intent와 Intent Filter

인텐트를 사용하여 액티비티나 서비스와 같은 앱의 컴포넌트를 실행하는 방법과 다른 앱들이 내 앱의 컴포넌트를 실행할 수 있도록 하는 방법들에 대하여 자세히 학습합니다.

Activity에 대하여

액티비티를 만들고 화면에 사용자 인터페이스를 구현하는 방법에 대하여 자세히 학습합니다.

리소스 제공하기 (Providing Resources)

앱에서 리소스가 소스코드와 어떻게 분리되어 있는지와 다양한 디바이스 환경에 최적화된 리소스를 어떻게 제공할 수 있는지에 대하여 자세히 학습합니다.


추가로 학습할 내용들:

기기 호환성 (Device Compativility)

다양한 종류의 디바이스들에서 안드로이드가 어떻게 동작하는지에 대해서와, 그러한 디바이스들의 제공 가능한 기능들과 관련하여 어떻게 최적화시킬 수 있는지에 대해 자세히 학습합니다.

시스템 퍼미션 (System Permissions)

사용자의 동의를 필요로 하는 API들에 대하여 안드로이드가 어떻게 접근 제한을 하는지에 대해 자세히 학습합니다.


Posted by 개발자 김태우
,

(원문: http://developer.android.com/guide/index.html)

(위치: Develop > API Guides > Introduction)

(전체 번역 글 목록으로 이동하기)


안드로이드 입문


안드로이드는 모바일 디바이스용 앱과 게임의 개발을 지원하는 자바 언어 기반의 어플리케이션 프레임웍입니다. API가이드에서 제공하는 문서들은 안드로이드의 다양한 API들을 이용하여 앱을 개발하는 방법에 대해 설명해 드립니다. 

안드로이드 개발 초보분들은 안드로이드 프레임웍의 기본 개념을 이해하는 것이 중요합니다.

(앱이 어떻게 동작하는지를 알고 싶다면 기본 개념부터 학습하세요.)

(바로 코딩부터 시작하고 싶다면 나의 첫앱 만들기부터 학습하세요.)


앱의 시작점이 2개 이상일 수 있습니다.

안드로이드 앱은 독립적인 컴포넌트들의 조합으로 구성됩니다. 예를 들어, Activity는 사용자 인터페이스를 제공하는 하나의 독립적인 화면을 제공하고, Service는 백그라운드에서 독립적으로 실행됩니다.

컴포넌트에서 다른 컴포넌트를 실행할 때는 Intent를 사용합니다. 심지어는 서로 다른 앱 간에도 실행이 가능합니다. 지도앱이 그렇습니다. 지도앱은 독립적인 하나의 앱이지만 다른 앱들이 특정 주소를 지도위에서 보기 위해 지도앱을 실행할 수 있습니다.

관련 링크: 

앱의 기본 개념

Intent와 Intent Filter

Activity


앱은 다양한 디바이스에 대응해야 합니다.

안드로이드 프레임웍은 다양한 디바이스 환경에서 각각의 디바이스 환경에 맞는 리소스를 지정할 수 있도록 해줍니다. 예를 들어, 서로 다른 스크린 사이즈에 대응하는 XML 레이아웃 파일을 만들 수 있고, 해당 디바이스의 시스템이 자신의 스크린 사이즈에 맞는 XML 레이아웃 파일을 적용합니다.

앱이 카메라 같은 특정 하드웨어 기능을 필요로 할 경우 런타임에 해당 기능을 사용할 수 있는지 질의하여 확인할 수 있습니다. 앱을 설치한 디바이스에 카메라 모듈이 없는 경우 앱이 실행 중에 미리 알고 사용자에게 알려줄 수 있어야 하기 때문에 필요한 것입니다. 앱이 필요로 하는 기능들을 AndroidManifest.xml 에 정의해 두면, 구글 플레이 스토어에서 해당 기능을 지원하지 않는 디바이스에게는 앱을 보여주지 않도록 할 수 있습니다.

관련 링크: 

기기 호환성

리소스(Resource)

사용자 인터페이스

Posted by 개발자 김태우
,

(원문: http://developer.android.com/guide/topics/appwidgets/host.html)

(위치: Develop > API Guides > App Components > App Widgets 
> App Widget Host)

(전체 번역 글 목록으로 이동하기)


앱 위젯 호스트
(App Widget Host)






Posted by 개발자 김태우
,

(원문: http://developer.android.com/guide/topics/providers/contacts-provider.html)

(위치: Develop > API Guides > App Components > Content Providers 
> Contacts Provider)

(전체 번역 글 목록으로 이동하기)


주소록 프로바이더
(Contacts Provider)






Posted by 개발자 김태우
,

(원문: http://developer.android.com/guide/topics/providers/content-provider-creating.html)

(위치: Develop > API Guides > App Components > Content Providers 
> Creating a Content Provider)

(전체 번역 글 목록으로 이동하기)


컨텐트 프로바이더 만들기
(Creating a Content Provider)






Posted by 개발자 김태우
,